| /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| /* |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details: |
| * |
| * Copyright (C) 2008 - 2009 Novell, Inc. |
| * Copyright (C) 2009 - 2010 Red Hat, Inc. |
| */ |
| |
| #define _GNU_SOURCE /* for strcasestr() */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <termios.h> |
| #include <unistd.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <fcntl.h> |
| #include <errno.h> |
| #include <sys/ioctl.h> |
| #include <string.h> |
| #include <linux/serial.h> |
| |
| #include <gio/gunixsocketaddress.h> |
| |
| #include <ModemManager.h> |
| #include <mm-errors-types.h> |
| |
| #include "mm-port-serial.h" |
| #include "mm-log.h" |
| #include "mm-helper-enums-types.h" |
| |
| static gboolean port_serial_queue_process (gpointer data); |
| static void port_serial_schedule_queue_process (MMPortSerial *self, |
| guint timeout_ms); |
| static void port_serial_close_force (MMPortSerial *self); |
| static void port_serial_reopen_cancel (MMPortSerial *self); |
| static void port_serial_set_cached_reply (MMPortSerial *self, |
| const GByteArray *command, |
| const GByteArray *response); |
| |
| G_DEFINE_TYPE (MMPortSerial, mm_port_serial, MM_TYPE_PORT) |
| |
| enum { |
| PROP_0, |
| PROP_BAUD, |
| PROP_BITS, |
| PROP_PARITY, |
| PROP_STOPBITS, |
| PROP_FLOW_CONTROL, |
| PROP_SEND_DELAY, |
| PROP_FD, |
| PROP_SPEW_CONTROL, |
| PROP_FLASH_OK, |
| |
| LAST_PROP |
| }; |
| |
| enum { |
| BUFFER_FULL, |
| TIMED_OUT, |
| FORCED_CLOSE, |
| |
| LAST_SIGNAL |
| }; |
| |
| static guint signals[LAST_SIGNAL] = { 0 }; |
| |
| #define SERIAL_BUF_SIZE 2048 |
| |
| struct _MMPortSerialPrivate { |
| guint32 open_count; |
| gboolean forced_close; |
| int fd; |
| GHashTable *reply_cache; |
| GQueue *queue; |
| GByteArray *response; |
| |
| /* For real ports, iochannel, and we implement the eagain limit */ |
| GIOChannel *iochannel; |
| guint iochannel_id; |
| |
| /* For unix-socket based ports, socket */ |
| GSocket *socket; |
| GSource *socket_source; |
| |
| |
| guint baud; |
| guint bits; |
| char parity; |
| guint stopbits; |
| MMFlowControl flow_control; |
| guint64 send_delay; |
| gboolean spew_control; |
| gboolean flash_ok; |
| |
| guint queue_id; |
| guint timeout_id; |
| |
| GCancellable *cancellable; |
| gulong cancellable_id; |
| |
| guint n_consecutive_timeouts; |
| |
| guint connected_id; |
| |
| GTask *flash_task; |
| GTask *reopen_task; |
| }; |
| |
| /*****************************************************************************/ |
| /* Command */ |
| |
| typedef struct { |
| MMPortSerial *self; |
| GSimpleAsyncResult *result; |
| GCancellable *cancellable; |
| GByteArray *command; |
| guint32 timeout; |
| gboolean allow_cached; |
| guint32 eagain_count; |
| |
| guint32 idx; |
| gboolean started; |
| gboolean done; |
| } CommandContext; |
| |
| static void |
| command_context_complete_and_free (CommandContext *ctx, gboolean idle) |
| { |
| if (idle) |
| g_simple_async_result_complete_in_idle (ctx->result); |
| else |
| g_simple_async_result_complete (ctx->result); |
| g_object_unref (ctx->result); |
| g_byte_array_unref (ctx->command); |
| if (ctx->cancellable) |
| g_object_unref (ctx->cancellable); |
| g_object_unref (ctx->self); |
| g_slice_free (CommandContext, ctx); |
| } |
| |
| GByteArray * |
| mm_port_serial_command_finish (MMPortSerial *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error)) |
| return NULL; |
| |
| return g_byte_array_ref (g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (res))); |
| } |
| |
| void |
| mm_port_serial_command (MMPortSerial *self, |
| GByteArray *command, |
| guint32 timeout_seconds, |
| gboolean allow_cached, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| CommandContext *ctx; |
| |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| g_return_if_fail (command != NULL); |
| |
| /* Setup command context */ |
| ctx = g_slice_new0 (CommandContext); |
| ctx->self = g_object_ref (self); |
| ctx->result = g_simple_async_result_new (G_OBJECT (self), |
| callback, |
| user_data, |
| mm_port_serial_command); |
| ctx->command = g_byte_array_ref (command); |
| ctx->allow_cached = allow_cached; |
| ctx->timeout = timeout_seconds; |
| ctx->cancellable = (cancellable ? g_object_ref (cancellable) : NULL); |
| |
| /* Only accept about 3 seconds of EAGAIN for this command */ |
| if (self->priv->send_delay && mm_port_get_subsys (MM_PORT (self)) == MM_PORT_SUBSYS_TTY) |
| ctx->eagain_count = 3000000 / self->priv->send_delay; |
| else |
| ctx->eagain_count = 1000; |
| |
| if (self->priv->open_count == 0) { |
| g_simple_async_result_set_error (ctx->result, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_SEND_FAILED, |
| "Sending command failed: device is not open"); |
| command_context_complete_and_free (ctx, TRUE); |
| return; |
| } |
| |
| /* Clear the cached value for this command if not asking for cached value */ |
| if (!allow_cached) |
| port_serial_set_cached_reply (self, ctx->command, NULL); |
| |
| g_queue_push_tail (self->priv->queue, ctx); |
| |
| if (g_queue_get_length (self->priv->queue) == 1) |
| port_serial_schedule_queue_process (self, 0); |
| } |
| |
| /*****************************************************************************/ |
| |
| static gboolean |
| parse_baudrate (guint baudrate_num, |
| guint *out_baudrate_speed) |
| { |
| guint speed; |
| |
| switch (baudrate_num) { |
| case 0: |
| speed = B0; |
| break; |
| case 50: |
| speed = B50; |
| break; |
| case 75: |
| speed = B75; |
| break; |
| case 110: |
| speed = B110; |
| break; |
| case 150: |
| speed = B150; |
| break; |
| case 300: |
| speed = B300; |
| break; |
| case 600: |
| speed = B600; |
| break; |
| case 1200: |
| speed = B1200; |
| break; |
| case 2400: |
| speed = B2400; |
| break; |
| case 4800: |
| speed = B4800; |
| break; |
| case 9600: |
| speed = B9600; |
| break; |
| case 19200: |
| speed = B19200; |
| break; |
| case 38400: |
| speed = B38400; |
| break; |
| case 57600: |
| speed = B57600; |
| break; |
| case 115200: |
| speed = B115200; |
| break; |
| case 230400: |
| speed = B230400; |
| break; |
| case 460800: |
| speed = B460800; |
| break; |
| case 921600: |
| speed = B921600; |
| break; |
| default: |
| return FALSE; |
| } |
| |
| if (out_baudrate_speed) |
| *out_baudrate_speed = speed; |
| return TRUE; |
| } |
| |
| static int |
| parse_bits (guint i) |
| { |
| int bits; |
| |
| switch (i) { |
| case 5: |
| bits = CS5; |
| break; |
| case 6: |
| bits = CS6; |
| break; |
| case 7: |
| bits = CS7; |
| break; |
| case 8: |
| bits = CS8; |
| break; |
| default: |
| mm_warn ("Invalid bits (%d). Valid values are 5, 6, 7, 8.", i); |
| bits = CS8; |
| } |
| |
| return bits; |
| } |
| |
| static int |
| parse_parity (char c) |
| { |
| int parity; |
| |
| switch (c) { |
| case 'n': |
| case 'N': |
| parity = 0; |
| break; |
| case 'e': |
| case 'E': |
| parity = PARENB; |
| break; |
| case 'o': |
| case 'O': |
| parity = PARENB | PARODD; |
| break; |
| default: |
| mm_warn ("Invalid parity (%c). Valid values are n, e, o", c); |
| parity = 0; |
| } |
| |
| return parity; |
| } |
| |
| static int |
| parse_stopbits (guint i) |
| { |
| int stopbits; |
| |
| switch (i) { |
| case 1: |
| stopbits = 0; |
| break; |
| case 2: |
| stopbits = CSTOPB; |
| break; |
| default: |
| mm_warn ("Invalid stop bits (%d). Valid values are 1 and 2)", i); |
| stopbits = 0; |
| } |
| |
| return stopbits; |
| } |
| |
| static gboolean |
| internal_tcsetattr (MMPortSerial *self, |
| gint fd, |
| const struct termios *options, |
| GError **error) |
| { |
| guint count; |
| struct termios other; |
| |
| #define MAX_TCSETATTR_RETRIES 4 |
| |
| for (count = 0; count < MAX_TCSETATTR_RETRIES; count++) { |
| /* try to set the new port attributes */ |
| errno = 0; |
| if (tcsetattr (fd, TCSANOW, options) == 0) |
| break; |
| |
| /* hard error if not EAGAIN */ |
| if (errno != EAGAIN) { |
| g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "couldn't set serial port attributes: %s", g_strerror (errno)); |
| return FALSE; |
| } |
| |
| /* try a few times if EAGAIN */ |
| g_usleep (100000); |
| } |
| |
| /* too many retries? */ |
| if (count == MAX_TCSETATTR_RETRIES) { |
| g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "couldn't set serial port attributes: too many retries (%u)", count); |
| return FALSE; |
| } |
| |
| /* tcsetattr() returns 0 if any of the requested attributes could be set, |
| * so we should double-check that all were set and log if not. Just with |
| * debug level, as we're ignoring this issue all together anyway. |
| */ |
| memset (&other, 0, sizeof (struct termios)); |
| errno = 0; |
| if (tcgetattr (fd, &other) != 0) |
| mm_dbg ("(%s): couldn't get serial port attributes after setting them: %s", |
| mm_port_get_device (MM_PORT (self)), g_strerror (errno)); |
| else if (memcmp (options, &other, sizeof (struct termios)) != 0) |
| mm_dbg ("(%s): port attributes not fully set", |
| mm_port_get_device (MM_PORT (self))); |
| |
| #undef MAX_TCSETATTR_RETRIES |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| set_flow_control_termios (MMPortSerial *self, |
| MMFlowControl flow_control, |
| struct termios *options) |
| { |
| gboolean had_xon_xoff; |
| gboolean had_rts_cts; |
| tcflag_t iflag_orig, cflag_orig; |
| |
| iflag_orig = options->c_iflag; |
| cflag_orig = options->c_cflag; |
| |
| had_xon_xoff = !!(options->c_iflag & (IXON | IXOFF)); |
| options->c_iflag &= ~(IXON | IXOFF | IXANY); |
| |
| had_rts_cts = !!(options->c_cflag & (CRTSCTS)); |
| options->c_cflag &= ~(CRTSCTS); |
| |
| /* setup the requested flags */ |
| switch (flow_control) { |
| case MM_FLOW_CONTROL_XON_XOFF: |
| mm_dbg ("(%s): enabling XON/XOFF flow control", mm_port_get_device (MM_PORT (self))); |
| options->c_iflag |= (IXON | IXOFF | IXANY); |
| break; |
| case MM_FLOW_CONTROL_RTS_CTS: |
| mm_dbg ("(%s): enabling RTS/CTS flow control", mm_port_get_device (MM_PORT (self))); |
| options->c_cflag |= (CRTSCTS); |
| break; |
| case MM_FLOW_CONTROL_NONE: |
| case MM_FLOW_CONTROL_UNKNOWN: |
| if (had_xon_xoff) |
| mm_dbg ("(%s): disabling XON/XOFF flow control", mm_port_get_device (MM_PORT (self))); |
| if (had_rts_cts) |
| mm_dbg ("(%s): disabling RTS/CTS flow control", mm_port_get_device (MM_PORT (self))); |
| break; |
| default: |
| g_assert_not_reached (); |
| } |
| |
| return iflag_orig != options->c_iflag || cflag_orig != options->c_cflag; |
| } |
| |
| static gboolean |
| real_config_fd (MMPortSerial *self, int fd, GError **error) |
| { |
| struct termios stbuf; |
| guint speed; |
| gint bits; |
| gint parity; |
| gint stopbits; |
| |
| /* No setup if not a tty */ |
| if (mm_port_get_subsys (MM_PORT (self)) != MM_PORT_SUBSYS_TTY) |
| return TRUE; |
| |
| mm_dbg ("(%s): setting up baudrate: %u", |
| mm_port_get_device (MM_PORT (self)), |
| self->priv->baud); |
| if (!parse_baudrate (self->priv->baud, &speed) || speed == B0) { |
| mm_warn ("(%s): baudrate invalid: %u; defaulting to 57600", |
| mm_port_get_device (MM_PORT (self)), |
| self->priv->baud); |
| speed = B57600; |
| } |
| |
| bits = parse_bits (self->priv->bits); |
| parity = parse_parity (self->priv->parity); |
| stopbits = parse_stopbits (self->priv->stopbits); |
| |
| memset (&stbuf, 0, sizeof (struct termios)); |
| if (tcgetattr (fd, &stbuf) != 0) { |
| mm_warn ("(%s): tcgetattr() error: %d", |
| mm_port_get_device (MM_PORT (self)), |
| errno); |
| } |
| |
| stbuf.c_cflag &= ~(CBAUD | CSIZE | CSTOPB | PARENB | PARODD | CRTSCTS); |
| stbuf.c_iflag &= ~(IGNCR | ICRNL | IUCLC | INPCK | IXON | IXOFF | IXANY ); |
| stbuf.c_oflag &= ~(OPOST | OLCUC | OCRNL | ONLCR | ONLRET); |
| stbuf.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL); |
| stbuf.c_cc[VMIN] = 1; |
| stbuf.c_cc[VTIME] = 0; |
| stbuf.c_cc[VEOF] = 1; |
| |
| /* Ignore parity/framing errors */ |
| stbuf.c_iflag |= IGNPAR; |
| |
| /* Set up port speed and serial attributes and enable receiver in local mode */ |
| stbuf.c_cflag |= (bits | parity | stopbits | CLOCAL | CREAD); |
| |
| errno = 0; |
| if (cfsetispeed (&stbuf, speed) != 0) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "%s: failed to set serial port input speed; errno %d", |
| __func__, errno); |
| return FALSE; |
| } |
| |
| errno = 0; |
| if (cfsetospeed (&stbuf, speed) != 0) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "%s: failed to set serial port output speed; errno %d", |
| __func__, errno); |
| return FALSE; |
| } |
| |
| if (self->priv->flow_control != MM_FLOW_CONTROL_UNKNOWN) { |
| gchar *str; |
| |
| str = mm_flow_control_build_string_from_mask (self->priv->flow_control); |
| mm_dbg ("(%s): flow control explicitly requested for device is: %s", |
| mm_port_get_device (MM_PORT (self)), |
| str ? str : "unknown"); |
| g_free (str); |
| } else |
| mm_dbg ("(%s): no flow control explicitly requested for device", |
| mm_port_get_device (MM_PORT (self))); |
| |
| set_flow_control_termios (self, self->priv->flow_control, &stbuf); |
| |
| return internal_tcsetattr (self, fd, &stbuf, error); |
| } |
| |
| static void |
| serial_debug (MMPortSerial *self, const char *prefix, const char *buf, gsize len) |
| { |
| g_return_if_fail (len > 0); |
| |
| if (MM_PORT_SERIAL_GET_CLASS (self)->debug_log) |
| MM_PORT_SERIAL_GET_CLASS (self)->debug_log (self, prefix, buf, len); |
| } |
| |
| static gboolean |
| port_serial_process_command (MMPortSerial *self, |
| CommandContext *ctx, |
| GError **error) |
| { |
| const gchar *p; |
| gsize written; |
| gssize send_len; |
| |
| if (self->priv->iochannel == NULL && self->priv->socket == NULL) { |
| g_set_error_literal (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_SEND_FAILED, |
| "Sending command failed: device is not enabled"); |
| return FALSE; |
| } |
| |
| if (mm_port_get_connected (MM_PORT (self))) { |
| g_set_error_literal (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_SEND_FAILED, |
| "Sending command failed: device is connected"); |
| return FALSE; |
| } |
| |
| /* Only print command the first time */ |
| if (ctx->started == FALSE) { |
| ctx->started = TRUE; |
| serial_debug (self, "-->", (const char *) ctx->command->data, ctx->command->len); |
| } |
| |
| if (self->priv->send_delay == 0 || mm_port_get_subsys (MM_PORT (self)) != MM_PORT_SUBSYS_TTY) { |
| /* Send the whole command in one write */ |
| send_len = (gssize)ctx->command->len; |
| p = (gchar *)ctx->command->data; |
| } else { |
| /* Send just one byte of the command */ |
| send_len = 1; |
| p = (gchar *)&ctx->command->data[ctx->idx]; |
| } |
| |
| /* GIOChannel based setup */ |
| if (self->priv->iochannel) { |
| GIOStatus write_status; |
| |
| /* Send N bytes of the command */ |
| write_status = g_io_channel_write_chars (self->priv->iochannel, p, send_len, &written, error); |
| switch (write_status) { |
| case G_IO_STATUS_ERROR: |
| g_prefix_error (error, "Sending command failed: "); |
| return FALSE; |
| |
| case G_IO_STATUS_EOF: |
| /* We shouldn't get EOF when writing */ |
| g_assert_not_reached (); |
| break; |
| |
| case G_IO_STATUS_NORMAL: |
| if (written > 0) { |
| ctx->idx += written; |
| break; |
| } |
| /* If written == 0, treat as EAGAIN, so fall down */ |
| |
| case G_IO_STATUS_AGAIN: |
| /* We're in a non-blocking channel and therefore we're up to receive |
| * EAGAIN; just retry in this case. */ |
| ctx->eagain_count--; |
| if (ctx->eagain_count <= 0) { |
| /* If we reach the limit of EAGAIN errors, treat as a timeout error. */ |
| self->priv->n_consecutive_timeouts++; |
| g_signal_emit (self, signals[TIMED_OUT], 0, self->priv->n_consecutive_timeouts); |
| |
| g_set_error (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_SEND_FAILED, |
| "Sending command failed: '%s'", strerror (errno)); |
| return FALSE; |
| } |
| break; |
| } |
| } |
| /* Socket based setup */ |
| else if (self->priv->socket) { |
| GError *inner_error = NULL; |
| gssize bytes_sent; |
| |
| /* Send N bytes of the command */ |
| bytes_sent = g_socket_send (self->priv->socket, p, send_len, NULL, &inner_error); |
| if (bytes_sent < 0) { |
| /* Non-EWOULDBLOCK error? */ |
| if (!g_error_matches (inner_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { |
| g_propagate_error (error, inner_error); |
| g_prefix_error (error, "Sending command failed: "); |
| return FALSE; |
| } |
| |
| /* We're in a non-blocking socket and therefore we're up to receive |
| * EWOULDBLOCK; just retry in this case. */ |
| |
| g_error_free (inner_error); |
| |
| ctx->eagain_count--; |
| if (ctx->eagain_count <= 0) { |
| /* If we reach the limit of EAGAIN errors, treat as a timeout error. */ |
| self->priv->n_consecutive_timeouts++; |
| g_signal_emit (self, signals[TIMED_OUT], 0, self->priv->n_consecutive_timeouts); |
| g_set_error (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_SEND_FAILED, |
| "Sending command failed: '%s'", strerror (errno)); |
| return FALSE; |
| } |
| |
| /* Just keep on, will retry... */ |
| written = 0; |
| } else |
| written = bytes_sent; |
| |
| ctx->idx += written; |
| } else |
| g_assert_not_reached (); |
| |
| if (ctx->idx >= ctx->command->len) |
| ctx->done = TRUE; |
| |
| return TRUE; |
| } |
| |
| static void |
| port_serial_set_cached_reply (MMPortSerial *self, |
| const GByteArray *command, |
| const GByteArray *response) |
| { |
| g_return_if_fail (self != NULL); |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| g_return_if_fail (command != NULL); |
| |
| if (response) { |
| GByteArray *cmd_copy = g_byte_array_sized_new (command->len); |
| GByteArray *rsp_copy = g_byte_array_sized_new (response->len); |
| |
| g_byte_array_append (cmd_copy, command->data, command->len); |
| g_byte_array_append (rsp_copy, response->data, response->len); |
| g_hash_table_insert (self->priv->reply_cache, cmd_copy, rsp_copy); |
| } else |
| g_hash_table_remove (self->priv->reply_cache, command); |
| } |
| |
| static const GByteArray * |
| port_serial_get_cached_reply (MMPortSerial *self, |
| GByteArray *command) |
| { |
| return (const GByteArray *)g_hash_table_lookup (self->priv->reply_cache, command); |
| } |
| |
| static void |
| port_serial_schedule_queue_process (MMPortSerial *self, guint timeout_ms) |
| { |
| if (self->priv->timeout_id) { |
| /* A command is already in progress */ |
| return; |
| } |
| |
| if (self->priv->queue_id) { |
| /* Already scheduled */ |
| return; |
| } |
| |
| if (timeout_ms) |
| self->priv->queue_id = g_timeout_add (timeout_ms, port_serial_queue_process, self); |
| else |
| self->priv->queue_id = g_idle_add (port_serial_queue_process, self); |
| } |
| |
| static void |
| port_serial_got_response (MMPortSerial *self, |
| GByteArray *parsed_response, |
| const GError *error) |
| { |
| /* Either one or the other, not both */ |
| g_assert ((parsed_response && !error) || (!parsed_response && error)); |
| |
| if (self->priv->timeout_id) { |
| g_source_remove (self->priv->timeout_id); |
| self->priv->timeout_id = 0; |
| } |
| |
| if (self->priv->cancellable_id) { |
| g_assert (self->priv->cancellable != NULL); |
| g_cancellable_disconnect (self->priv->cancellable, |
| self->priv->cancellable_id); |
| self->priv->cancellable_id = 0; |
| } |
| |
| g_clear_object (&self->priv->cancellable); |
| |
| /* The completion of the command context may end up fully disposing the |
| * serial port object. In order to cope with that, we make sure we have |
| * our own reference to the object while the completion and follow up |
| * setup runs. */ |
| g_object_ref (self); |
| { |
| CommandContext *ctx; |
| |
| ctx = (CommandContext *) g_queue_pop_head (self->priv->queue); |
| if (ctx) { |
| /* Complete the command context with the appropriate result */ |
| if (error) |
| g_simple_async_result_set_from_error (ctx->result, error); |
| else { |
| if (ctx->allow_cached) |
| port_serial_set_cached_reply (self, ctx->command, parsed_response); |
| g_simple_async_result_set_op_res_gpointer (ctx->result, |
| g_byte_array_ref (parsed_response), |
| (GDestroyNotify) g_byte_array_unref); |
| } |
| |
| /* Don't complete in idle. We need the caller remove the response range which |
| * was processed, and that must be done before processing any new queued command */ |
| command_context_complete_and_free (ctx, FALSE); |
| } |
| |
| if (!g_queue_is_empty (self->priv->queue)) |
| port_serial_schedule_queue_process (self, 0); |
| } |
| g_object_unref (self); |
| } |
| |
| static gboolean |
| port_serial_timed_out (gpointer data) |
| { |
| MMPortSerial *self = MM_PORT_SERIAL (data); |
| GError *error; |
| |
| self->priv->timeout_id = 0; |
| |
| /* Update number of consecutive timeouts found */ |
| self->priv->n_consecutive_timeouts++; |
| |
| /* FIXME: This is not completely correct - if the response finally arrives and there's |
| * some other command waiting for response right now, the other command will |
| * get the output of the timed out command. Not sure what to do here. */ |
| error = g_error_new_literal (MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_RESPONSE_TIMEOUT, |
| "Serial command timed out"); |
| |
| /* Make sure we have a valid reference when emitting the signal */ |
| g_object_ref (self); |
| { |
| port_serial_got_response (self, NULL, error); |
| |
| /* Emit a timed out signal, used by upper layers to identify a disconnected |
| * serial port */ |
| g_signal_emit (self, signals[TIMED_OUT], 0, self->priv->n_consecutive_timeouts); |
| } |
| g_object_unref (self); |
| |
| g_error_free (error); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| port_serial_response_wait_cancelled (GCancellable *cancellable, |
| MMPortSerial *self) |
| { |
| GError *error; |
| |
| /* We don't want to call disconnect () while in the signal handler */ |
| self->priv->cancellable_id = 0; |
| |
| /* FIXME: This is not completely correct - if the response finally arrives and there's |
| * some other command waiting for response right now, the other command will |
| * get the output of the cancelled command. Not sure what to do here. */ |
| error = g_error_new_literal (MM_CORE_ERROR, |
| MM_CORE_ERROR_CANCELLED, |
| "Waiting for the reply cancelled"); |
| /* Note: may complete last operation and unref the MMPortSerial */ |
| port_serial_got_response (self, NULL, error); |
| g_error_free (error); |
| } |
| |
| static gboolean |
| port_serial_queue_process (gpointer data) |
| { |
| MMPortSerial *self = MM_PORT_SERIAL (data); |
| CommandContext *ctx; |
| GError *error = NULL; |
| |
| self->priv->queue_id = 0; |
| |
| ctx = (CommandContext *) g_queue_peek_head (self->priv->queue); |
| if (!ctx) |
| return G_SOURCE_REMOVE; |
| |
| if (ctx->allow_cached) { |
| const GByteArray *cached; |
| |
| cached = port_serial_get_cached_reply (self, ctx->command); |
| if (cached) { |
| GByteArray *parsed_response; |
| |
| parsed_response = g_byte_array_sized_new (cached->len); |
| g_byte_array_append (parsed_response, cached->data, cached->len); |
| /* Note: may complete last operation and unref the MMPortSerial */ |
| port_serial_got_response (self, parsed_response, NULL); |
| g_byte_array_unref (parsed_response); |
| return G_SOURCE_REMOVE; |
| } |
| |
| /* Cached reply wasn't found, keep on */ |
| } |
| |
| /* If error, report it */ |
| if (!port_serial_process_command (self, ctx, &error)) { |
| /* Note: may complete last operation and unref the MMPortSerial */ |
| port_serial_got_response (self, NULL, error); |
| g_error_free (error); |
| return G_SOURCE_REMOVE; |
| } |
| |
| /* Schedule the next byte of the command to be sent */ |
| if (!ctx->done) { |
| port_serial_schedule_queue_process (self, |
| (mm_port_get_subsys (MM_PORT (self)) == MM_PORT_SUBSYS_TTY ? |
| self->priv->send_delay / 1000 : |
| 0)); |
| return G_SOURCE_REMOVE; |
| } |
| |
| /* Setup the cancellable so that we can stop waiting for a response */ |
| if (ctx->cancellable) { |
| gulong cancellable_id; |
| |
| self->priv->cancellable = g_object_ref (ctx->cancellable); |
| |
| /* If the GCancellable is already cancelled here, the callback will be |
| * called right away, and a GError will be propagated as response. In |
| * this case we need to completely avoid doing anything else with the |
| * MMPortSerial, as it may already be disposed. |
| * So, use an intermediate variable to store the cancellable id, and |
| * just return without further processing if we're already cancelled. |
| */ |
| cancellable_id = g_cancellable_connect (ctx->cancellable, |
| (GCallback)port_serial_response_wait_cancelled, |
| self, |
| NULL); |
| if (!cancellable_id) |
| return G_SOURCE_REMOVE; |
| |
| self->priv->cancellable_id = cancellable_id; |
| } |
| |
| /* If the command is finished being sent, schedule the timeout */ |
| self->priv->timeout_id = g_timeout_add_seconds (ctx->timeout, |
| port_serial_timed_out, |
| self); |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| parse_response_buffer (MMPortSerial *self) |
| { |
| GError *error = NULL; |
| GByteArray *parsed_response = NULL; |
| |
| /* Parse unsolicited messages in the subclass. |
| * |
| * If any message found, it's processed immediately and the message is |
| * removed from the response buffer. |
| */ |
| if (MM_PORT_SERIAL_GET_CLASS (self)->parse_unsolicited) |
| MM_PORT_SERIAL_GET_CLASS (self)->parse_unsolicited (self, |
| self->priv->response); |
| |
| /* Parse response in the subclass. |
| * |
| * Returns TRUE either if an error is provided or if we really have the |
| * response to process. The parsed string is returned already out of the |
| * response buffer, and the response buffer is cleaned up accordingly. |
| */ |
| g_assert (MM_PORT_SERIAL_GET_CLASS (self)->parse_response != NULL); |
| switch (MM_PORT_SERIAL_GET_CLASS (self)->parse_response (self, |
| self->priv->response, |
| &parsed_response, |
| &error)) { |
| case MM_PORT_SERIAL_RESPONSE_BUFFER: |
| /* We have a valid response to process */ |
| g_assert (parsed_response); |
| self->priv->n_consecutive_timeouts = 0; |
| /* Note: may complete last operation and unref the MMPortSerial */ |
| port_serial_got_response (self, parsed_response, NULL); |
| g_byte_array_unref (parsed_response); |
| break; |
| case MM_PORT_SERIAL_RESPONSE_ERROR: |
| /* We have an error to process */ |
| g_assert (error); |
| self->priv->n_consecutive_timeouts = 0; |
| /* Note: may complete last operation and unref the MMPortSerial */ |
| port_serial_got_response (self, NULL, error); |
| g_error_free (error); |
| break; |
| case MM_PORT_SERIAL_RESPONSE_NONE: |
| /* Nothing to do this time */ |
| break; |
| } |
| } |
| |
| static gboolean |
| common_input_available (MMPortSerial *self, |
| GIOCondition condition) |
| { |
| char buf[SERIAL_BUF_SIZE + 1]; |
| gsize bytes_read; |
| GIOStatus status = G_IO_STATUS_NORMAL; |
| CommandContext *ctx; |
| const char *device; |
| GError *error = NULL; |
| gboolean iterate = TRUE; |
| gboolean keep_source = G_SOURCE_CONTINUE; |
| |
| if (condition & G_IO_HUP) { |
| device = mm_port_get_device (MM_PORT (self)); |
| mm_dbg ("(%s) unexpected port hangup!", device); |
| |
| if (self->priv->response->len) |
| g_byte_array_remove_range (self->priv->response, 0, self->priv->response->len); |
| port_serial_close_force (self); |
| return G_SOURCE_REMOVE; |
| } |
| |
| if (condition & G_IO_ERR) { |
| if (self->priv->response->len) |
| g_byte_array_remove_range (self->priv->response, 0, self->priv->response->len); |
| return G_SOURCE_CONTINUE; |
| } |
| |
| /* Don't read any input if the current command isn't done being sent yet */ |
| ctx = g_queue_peek_nth (self->priv->queue, 0); |
| if (ctx && (ctx->started == TRUE) && (ctx->done == FALSE)) |
| return G_SOURCE_CONTINUE; |
| |
| while (iterate) { |
| bytes_read = 0; |
| |
| if (self->priv->iochannel) { |
| status = g_io_channel_read_chars (self->priv->iochannel, |
| buf, |
| SERIAL_BUF_SIZE, |
| &bytes_read, |
| &error); |
| if (status == G_IO_STATUS_ERROR) { |
| if (error) { |
| mm_warn ("(%s): read error: %s", |
| mm_port_get_device (MM_PORT (self)), |
| error->message); |
| } |
| g_clear_error (&error); |
| } |
| } else if (self->priv->socket) { |
| gssize sbytes_read; |
| |
| sbytes_read = g_socket_receive (self->priv->socket, |
| buf, |
| SERIAL_BUF_SIZE, |
| NULL, /* cancellable */ |
| &error); |
| if (sbytes_read < 0) { |
| bytes_read = 0; |
| if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) |
| status = G_IO_STATUS_AGAIN; |
| else |
| status = G_IO_STATUS_ERROR; |
| mm_warn ("(%s): receive error: %s", |
| mm_port_get_device (MM_PORT (self)), |
| error->message); |
| g_clear_error (&error); |
| } else { |
| bytes_read = (gsize) sbytes_read; |
| status = G_IO_STATUS_NORMAL; |
| } |
| } |
| |
| /* If no bytes read, just wait for more data */ |
| if (bytes_read == 0) |
| break; |
| |
| g_assert (bytes_read > 0); |
| serial_debug (self, "<--", buf, bytes_read); |
| g_byte_array_append (self->priv->response, (const guint8 *) buf, bytes_read); |
| |
| /* Make sure the response doesn't grow too long */ |
| if ((self->priv->response->len > SERIAL_BUF_SIZE) && self->priv->spew_control) { |
| /* Notify listeners and then trim the buffer */ |
| g_signal_emit (self, signals[BUFFER_FULL], 0, self->priv->response); |
| g_byte_array_remove_range (self->priv->response, 0, (SERIAL_BUF_SIZE / 2)); |
| } |
| |
| /* See if we can parse anything. The response parsing may actually |
| * schedule the completion of a serial command, and that in turn may end |
| * up fully disposing this serial port object. In order to cope with |
| * that we make sure we have our own reference to the object while the |
| * response buffer operation is run, and then we check ourselves whether |
| * we should be keeping this socket/iochannel source or not. */ |
| g_object_ref (self); |
| { |
| parse_response_buffer (self); |
| |
| /* If we didn't end up closing the iochannel/socket in the previous |
| * operation, we keep this source. */ |
| keep_source = ((self->priv->iochannel_id > 0 || self->priv->socket_source != NULL) ? |
| G_SOURCE_CONTINUE : G_SOURCE_REMOVE); |
| |
| /* If we're keeping the source and we still may have bytes to read, |
| * iterate. */ |
| iterate = ((keep_source == G_SOURCE_CONTINUE) && |
| (bytes_read == SERIAL_BUF_SIZE || status == G_IO_STATUS_AGAIN)); |
| } |
| g_object_unref (self); |
| } |
| |
| return keep_source; |
| } |
| |
| static gboolean |
| iochannel_input_available (GIOChannel *iochannel, |
| GIOCondition condition, |
| gpointer data) |
| { |
| return common_input_available (MM_PORT_SERIAL (data), condition); |
| } |
| |
| static gboolean |
| socket_input_available (GSocket *socket, |
| GIOCondition condition, |
| gpointer data) |
| { |
| return common_input_available (MM_PORT_SERIAL (data), condition); |
| } |
| |
| static void |
| data_watch_enable (MMPortSerial *self, gboolean enable) |
| { |
| if (self->priv->iochannel_id) { |
| if (enable) |
| g_warn_if_fail (self->priv->iochannel_id == 0); |
| g_source_remove (self->priv->iochannel_id); |
| self->priv->iochannel_id = 0; |
| } |
| |
| if (self->priv->socket_source) { |
| if (enable) |
| g_warn_if_fail (self->priv->socket_source == NULL); |
| g_source_destroy (self->priv->socket_source); |
| g_source_unref (self->priv->socket_source); |
| self->priv->socket_source = NULL; |
| } |
| |
| if (enable) { |
| if (self->priv->iochannel) { |
| self->priv->iochannel_id = g_io_add_watch (self->priv->iochannel, |
| G_IO_IN | G_IO_ERR | G_IO_HUP, |
| iochannel_input_available, |
| self); |
| } else if (self->priv->socket) { |
| self->priv->socket_source = g_socket_create_source (self->priv->socket, |
| G_IO_IN | G_IO_ERR | G_IO_HUP, |
| NULL); |
| g_source_set_callback (self->priv->socket_source, |
| (GSourceFunc)socket_input_available, |
| self, |
| NULL); |
| g_source_attach (self->priv->socket_source, NULL); |
| } |
| else |
| g_warn_if_reached (); |
| } |
| } |
| |
| static void |
| port_connected (MMPortSerial *self, GParamSpec *pspec, gpointer user_data) |
| { |
| gboolean connected; |
| |
| if (!self->priv->iochannel && !self->priv->socket) |
| return; |
| |
| /* When the port is connected, drop the serial port lock so PPP can do |
| * something with the port. When the port is disconnected, grab the lock |
| * again. |
| */ |
| connected = mm_port_get_connected (MM_PORT (self)); |
| |
| if (self->priv->fd >= 0 && ioctl (self->priv->fd, (connected ? TIOCNXCL : TIOCEXCL)) < 0) { |
| mm_warn ("(%s): could not %s serial port lock: (%d) %s", |
| mm_port_get_device (MM_PORT (self)), |
| connected ? "drop" : "re-acquire", |
| errno, |
| strerror (errno)); |
| if (!connected) { |
| // FIXME: do something here, maybe try again in a few seconds or |
| // close the port and error out? |
| } |
| } |
| |
| /* When connected ignore let PPP have all the data */ |
| data_watch_enable (self, !connected); |
| } |
| |
| gboolean |
| mm_port_serial_open (MMPortSerial *self, GError **error) |
| { |
| char *devfile; |
| const char *device; |
| struct serial_struct sinfo = { 0 }; |
| GTimeVal tv_start, tv_end; |
| int errno_save = 0; |
| |
| g_return_val_if_fail (MM_IS_PORT_SERIAL (self), FALSE); |
| |
| device = mm_port_get_device (MM_PORT (self)); |
| |
| if (self->priv->forced_close) { |
| g_set_error (error, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_OPEN_FAILED, |
| "Could not open serial device %s: it has been forced close", |
| device); |
| return FALSE; |
| } |
| |
| if (self->priv->reopen_task) { |
| g_set_error (error, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_OPEN_FAILED, |
| "Could not open serial device %s: reopen operation in progress", |
| device); |
| return FALSE; |
| } |
| |
| if (mm_port_get_connected (MM_PORT (self))) { |
| g_set_error (error, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_OPEN_FAILED, |
| "Could not open serial device %s: port is connected", |
| device); |
| return FALSE; |
| } |
| |
| if (self->priv->open_count) { |
| /* Already open */ |
| goto success; |
| } |
| |
| mm_dbg ("(%s) opening serial port...", device); |
| |
| g_get_current_time (&tv_start); |
| |
| /* Non-socket setup needs the fd open */ |
| if (mm_port_get_subsys (MM_PORT (self)) != MM_PORT_SUBSYS_UNIX) { |
| /* Only open a new file descriptor if we weren't given one already */ |
| if (self->priv->fd < 0) { |
| devfile = g_strdup_printf ("/dev/%s", device); |
| errno = 0; |
| self->priv->fd = open (devfile, O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY); |
| errno_save = errno; |
| g_free (devfile); |
| } |
| |
| if (self->priv->fd < 0) { |
| /* nozomi isn't ready yet when the port appears, and it'll return |
| * ENODEV when open(2) is called on it. Make sure we can handle this |
| * by returning a special error in that case. |
| */ |
| g_set_error (error, |
| MM_SERIAL_ERROR, |
| (errno == ENODEV) ? MM_SERIAL_ERROR_OPEN_FAILED_NO_DEVICE : MM_SERIAL_ERROR_OPEN_FAILED, |
| "Could not open serial device %s: %s", device, strerror (errno_save)); |
| mm_warn ("(%s) could not open serial device (%d)", device, errno_save); |
| return FALSE; |
| } |
| } |
| |
| /* Serial port specific setup */ |
| if (mm_port_get_subsys (MM_PORT (self)) == MM_PORT_SUBSYS_TTY) { |
| /* Try to lock serial device */ |
| if (ioctl (self->priv->fd, TIOCEXCL) < 0) { |
| errno_save = errno; |
| g_set_error (error, MM_SERIAL_ERROR, MM_SERIAL_ERROR_OPEN_FAILED, |
| "Could not lock serial device %s: %s", device, strerror (errno_save)); |
| mm_warn ("(%s) could not lock serial device (%d)", device, errno_save); |
| goto error; |
| } |
| |
| /* Flush any waiting IO */ |
| tcflush (self->priv->fd, TCIOFLUSH); |
| |
| /* Don't wait for pending data when closing the port; this can cause some |
| * stupid devices that don't respond to URBs on a particular port to hang |
| * for 30 seconds when probing fails. See GNOME bug #630670. |
| */ |
| if (ioctl (self->priv->fd, TIOCGSERIAL, &sinfo) == 0) { |
| sinfo.closing_wait = ASYNC_CLOSING_WAIT_NONE; |
| if (ioctl (self->priv->fd, TIOCSSERIAL, &sinfo) < 0) |
| mm_warn ("(%s): couldn't set serial port closing_wait to none: %s", |
| device, g_strerror (errno)); |
| } |
| } |
| |
| g_warn_if_fail (MM_PORT_SERIAL_GET_CLASS (self)->config_fd); |
| if (self->priv->fd >= 0 && !MM_PORT_SERIAL_GET_CLASS (self)->config_fd (self, self->priv->fd, error)) { |
| mm_dbg ("(%s) failed to configure serial device", device); |
| goto error; |
| } |
| |
| g_get_current_time (&tv_end); |
| |
| if (tv_end.tv_sec - tv_start.tv_sec > 7) |
| mm_warn ("(%s): open blocked by driver for more than 7 seconds!", device); |
| |
| if (mm_port_get_subsys (MM_PORT (self)) != MM_PORT_SUBSYS_UNIX) { |
| /* Create new GIOChannel */ |
| self->priv->iochannel = g_io_channel_unix_new (self->priv->fd); |
| |
| /* We don't want UTF-8 encoding, we're playing with raw binary data */ |
| g_io_channel_set_encoding (self->priv->iochannel, NULL, NULL); |
| |
| /* We don't want to get the channel buffered */ |
| g_io_channel_set_buffered (self->priv->iochannel, FALSE); |
| |
| /* We don't want to get blocked while writing stuff */ |
| if (!g_io_channel_set_flags (self->priv->iochannel, |
| G_IO_FLAG_NONBLOCK, |
| error)) { |
| g_prefix_error (error, "Cannot set non-blocking channel: "); |
| goto error; |
| } |
| } else { |
| GSocketAddress *address; |
| |
| /* Create new GSocket */ |
| self->priv->socket = g_socket_new (G_SOCKET_FAMILY_UNIX, |
| G_SOCKET_TYPE_STREAM, |
| G_SOCKET_PROTOCOL_DEFAULT, |
| error); |
| if (!self->priv->socket) { |
| g_prefix_error (error, "Cannot create socket: "); |
| goto error; |
| } |
| |
| /* Non-blocking socket */ |
| g_socket_set_blocking (self->priv->socket, FALSE); |
| |
| /* By default, abstract socket */ |
| address = (g_unix_socket_address_new_with_type ( |
| device, |
| -1, |
| (g_str_has_prefix (device, "abstract:") ? |
| G_UNIX_SOCKET_ADDRESS_ABSTRACT : |
| G_UNIX_SOCKET_ADDRESS_PATH))); |
| |
| /* Connect to address */ |
| if (!g_socket_connect (self->priv->socket, |
| address, |
| NULL, |
| error)) { |
| g_prefix_error (error, "Cannot connect socket: "); |
| g_object_unref (address); |
| goto error; |
| } |
| g_object_unref (address); |
| } |
| |
| /* Reading watch enable */ |
| data_watch_enable (self, TRUE); |
| |
| g_warn_if_fail (self->priv->connected_id == 0); |
| self->priv->connected_id = g_signal_connect (self, |
| "notify::" MM_PORT_CONNECTED, |
| G_CALLBACK (port_connected), |
| NULL); |
| |
| success: |
| self->priv->open_count++; |
| mm_dbg ("(%s) device open count is %d (open)", device, self->priv->open_count); |
| |
| /* Run additional port config if just opened */ |
| if (self->priv->open_count == 1 && MM_PORT_SERIAL_GET_CLASS (self)->config) |
| MM_PORT_SERIAL_GET_CLASS (self)->config (self); |
| |
| return TRUE; |
| |
| error: |
| mm_warn ("(%s) failed to open serial device", device); |
| |
| if (self->priv->iochannel) { |
| g_io_channel_unref (self->priv->iochannel); |
| self->priv->iochannel = NULL; |
| } |
| |
| if (self->priv->socket) { |
| g_socket_close (self->priv->socket, NULL); |
| g_object_unref (self->priv->socket); |
| self->priv->socket = NULL; |
| } |
| |
| if (self->priv->fd >= 0) { |
| close (self->priv->fd); |
| self->priv->fd = -1; |
| } |
| |
| return FALSE; |
| } |
| |
| gboolean |
| mm_port_serial_is_open (MMPortSerial *self) |
| { |
| g_return_val_if_fail (self != NULL, FALSE); |
| g_return_val_if_fail (MM_IS_PORT_SERIAL (self), FALSE); |
| |
| return !!self->priv->open_count; |
| } |
| |
| static void |
| _close_internal (MMPortSerial *self, gboolean force) |
| { |
| const char *device; |
| int i; |
| |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| |
| if (force) |
| self->priv->open_count = 0; |
| else { |
| g_return_if_fail (self->priv->open_count > 0); |
| self->priv->open_count--; |
| } |
| |
| device = mm_port_get_device (MM_PORT (self)); |
| |
| mm_dbg ("(%s) device open count is %d (close)", device, self->priv->open_count); |
| |
| if (self->priv->open_count > 0) |
| return; |
| |
| if (self->priv->connected_id) { |
| g_signal_handler_disconnect (self, self->priv->connected_id); |
| self->priv->connected_id = 0; |
| } |
| |
| mm_port_serial_flash_cancel (self); |
| |
| if (self->priv->iochannel || self->priv->socket) { |
| GTimeVal tv_start, tv_end; |
| struct serial_struct sinfo = { 0 }; |
| |
| mm_dbg ("(%s) closing serial port...", device); |
| |
| mm_port_set_connected (MM_PORT (self), FALSE); |
| |
| g_get_current_time (&tv_start); |
| |
| /* Serial port specific setup */ |
| if (self->priv->fd >= 0 && mm_port_get_subsys (MM_PORT (self)) == MM_PORT_SUBSYS_TTY) { |
| /* Paranoid: ensure our closing_wait value is still set so we ignore |
| * pending data when closing the port. See GNOME bug #630670. |
| */ |
| if (ioctl (self->priv->fd, TIOCGSERIAL, &sinfo) == 0) { |
| if (sinfo.closing_wait != ASYNC_CLOSING_WAIT_NONE) { |
| mm_warn ("(%s): serial port closing_wait was reset!", device); |
| sinfo.closing_wait = ASYNC_CLOSING_WAIT_NONE; |
| if (ioctl (self->priv->fd, TIOCSSERIAL, &sinfo) < 0) |
| mm_warn ("(%s): couldn't set serial port closing_wait to none: %s", |
| device, g_strerror (errno)); |
| } |
| } |
| |
| tcflush (self->priv->fd, TCIOFLUSH); |
| } |
| |
| /* Destroy channel */ |
| if (self->priv->iochannel) { |
| data_watch_enable (self, FALSE); |
| /* unref() without g_io_channel_shutdown() to destroy the channel |
| * without closing the fd. The close() is called explicitly after. |
| */ |
| g_io_channel_unref (self->priv->iochannel); |
| self->priv->iochannel = NULL; |
| } |
| |
| /* Close fd, if any */ |
| if (self->priv->fd >= 0) { |
| close (self->priv->fd); |
| self->priv->fd = -1; |
| } |
| |
| /* Destroy socket */ |
| if (self->priv->socket) { |
| data_watch_enable (self, FALSE); |
| g_socket_close (self->priv->socket, NULL); |
| g_object_unref (self->priv->socket); |
| self->priv->socket = NULL; |
| } |
| |
| g_get_current_time (&tv_end); |
| |
| mm_dbg ("(%s) serial port closed", device); |
| |
| /* Some ports don't respond to data and when close is called |
| * the serial layer waits up to 30 second (closing_wait) for |
| * that data to send before giving up and returning from close(). |
| * Log that. See GNOME bug #630670 for more details. |
| */ |
| if (tv_end.tv_sec - tv_start.tv_sec > 7) |
| mm_warn ("(%s): close blocked by driver for more than 7 seconds!", device); |
| } |
| |
| /* Clear the command queue */ |
| for (i = 0; i < g_queue_get_length (self->priv->queue); i++) { |
| CommandContext *ctx; |
| |
| ctx = g_queue_peek_nth (self->priv->queue, i); |
| g_simple_async_result_set_error (ctx->result, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_SEND_FAILED, |
| "Serial port is now closed"); |
| command_context_complete_and_free (ctx, TRUE); |
| } |
| g_queue_clear (self->priv->queue); |
| |
| if (self->priv->timeout_id) { |
| g_source_remove (self->priv->timeout_id); |
| self->priv->timeout_id = 0; |
| } |
| |
| if (self->priv->queue_id) { |
| g_source_remove (self->priv->queue_id); |
| self->priv->queue_id = 0; |
| } |
| |
| if (self->priv->cancellable_id) { |
| g_assert (self->priv->cancellable != NULL); |
| g_cancellable_disconnect (self->priv->cancellable, |
| self->priv->cancellable_id); |
| self->priv->cancellable_id = 0; |
| } |
| |
| g_clear_object (&self->priv->cancellable); |
| } |
| |
| void |
| mm_port_serial_close (MMPortSerial *self) |
| { |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| |
| if (!self->priv->forced_close) |
| _close_internal (self, FALSE); |
| } |
| |
| static void |
| port_serial_close_force (MMPortSerial *self) |
| { |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| |
| /* If already forced to close, return */ |
| if (self->priv->forced_close) |
| return; |
| |
| mm_dbg ("(%s) forced to close port", mm_port_get_device (MM_PORT (self))); |
| |
| /* Mark as having forced the close, so that we don't warn about incorrect |
| * open counts */ |
| self->priv->forced_close = TRUE; |
| |
| /* Cancel port reopening if one is running */ |
| port_serial_reopen_cancel (self); |
| |
| /* If already closed, done */ |
| if (self->priv->open_count > 0) { |
| _close_internal (self, TRUE); |
| |
| /* Notify about the forced close status */ |
| g_signal_emit (self, signals[FORCED_CLOSE], 0); |
| } |
| } |
| |
| /*****************************************************************************/ |
| /* Reopen */ |
| |
| typedef struct { |
| guint initial_open_count; |
| guint reopen_id; |
| } ReopenContext; |
| |
| static void |
| reopen_context_free (ReopenContext *ctx) |
| { |
| if (ctx->reopen_id) |
| g_source_remove (ctx->reopen_id); |
| g_slice_free (ReopenContext, ctx); |
| } |
| |
| gboolean |
| mm_port_serial_reopen_finish (MMPortSerial *port, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return g_task_propagate_boolean (G_TASK (res), error); |
| } |
| |
| static void |
| port_serial_reopen_cancel (MMPortSerial *self) |
| { |
| GTask *task; |
| |
| if (!self->priv->reopen_task) |
| return; |
| |
| /* Recover task */ |
| task = self->priv->reopen_task; |
| self->priv->reopen_task = NULL; |
| |
| g_task_return_new_error (task, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_CANCELLED, |
| "Reopen cancelled"); |
| g_object_unref (task); |
| } |
| |
| static gboolean |
| reopen_do (MMPortSerial *self) |
| { |
| GTask *task; |
| ReopenContext *ctx; |
| GError *error = NULL; |
| guint i; |
| |
| /* Recover task */ |
| g_assert (self->priv->reopen_task != NULL); |
| task = self->priv->reopen_task; |
| self->priv->reopen_task = NULL; |
| |
| ctx = g_task_get_task_data (task); |
| ctx->reopen_id = 0; |
| |
| for (i = 0; i < ctx->initial_open_count; i++) { |
| if (!mm_port_serial_open (self, &error)) { |
| g_prefix_error (&error, "Couldn't reopen port (%u): ", i); |
| break; |
| } |
| } |
| |
| if (error) |
| g_task_return_error (task, error); |
| else |
| g_task_return_boolean (task, TRUE); |
| g_object_unref (task); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| void |
| mm_port_serial_reopen (MMPortSerial *self, |
| guint32 reopen_time, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| ReopenContext *ctx; |
| GTask *task; |
| guint i; |
| |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| |
| /* Setup context */ |
| ctx = g_slice_new0 (ReopenContext); |
| ctx->initial_open_count = self->priv->open_count; |
| |
| task = g_task_new (self, NULL, callback, user_data); |
| g_task_set_task_data (task, ctx, (GDestroyNotify)reopen_context_free); |
| |
| if (self->priv->forced_close) { |
| g_task_return_new_error (task, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "Serial port has been forced close."); |
| g_object_unref (task); |
| return; |
| } |
| |
| /* If already reopening, halt */ |
| if (self->priv->reopen_task) { |
| g_task_return_new_error (task, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_IN_PROGRESS, |
| "Modem is already being reopened"); |
| g_object_unref (task); |
| return; |
| } |
| |
| mm_dbg ("(%s) reopening port (%u)", |
| mm_port_get_device (MM_PORT (self)), |
| ctx->initial_open_count); |
| |
| for (i = 0; i < ctx->initial_open_count; i++) |
| mm_port_serial_close (self); |
| |
| if (reopen_time > 0) |
| ctx->reopen_id = g_timeout_add (reopen_time, (GSourceFunc)reopen_do, self); |
| else |
| ctx->reopen_id = g_idle_add ((GSourceFunc)reopen_do, self); |
| |
| /* Store context in private info */ |
| self->priv->reopen_task = task; |
| } |
| |
| static gboolean |
| get_speed (MMPortSerial *self, speed_t *speed, GError **error) |
| { |
| struct termios options; |
| |
| g_assert (self->priv->fd >= 0); |
| |
| memset (&options, 0, sizeof (struct termios)); |
| if (tcgetattr (self->priv->fd, &options) != 0) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "%s: tcgetattr() error %d", |
| __func__, errno); |
| return FALSE; |
| } |
| |
| *speed = cfgetospeed (&options); |
| return TRUE; |
| } |
| |
| static gboolean |
| set_speed (MMPortSerial *self, speed_t speed, GError **error) |
| { |
| struct termios options; |
| |
| g_assert (self->priv->fd >= 0); |
| |
| memset (&options, 0, sizeof (struct termios)); |
| if (tcgetattr (self->priv->fd, &options) != 0) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "%s: tcgetattr() error %d", |
| __func__, errno); |
| return FALSE; |
| } |
| |
| cfsetispeed (&options, speed); |
| cfsetospeed (&options, speed); |
| options.c_cflag |= (CLOCAL | CREAD); |
| |
| return internal_tcsetattr (self, self->priv->fd, &options, error); |
| } |
| |
| /*****************************************************************************/ |
| /* Flash */ |
| |
| typedef struct { |
| speed_t current_speed; |
| guint flash_id; |
| } FlashContext; |
| |
| static void |
| flash_context_free (FlashContext *ctx) |
| { |
| if (ctx->flash_id) |
| g_source_remove (ctx->flash_id); |
| g_slice_free (FlashContext, ctx); |
| } |
| |
| gboolean |
| mm_port_serial_flash_finish (MMPortSerial *port, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return g_task_propagate_boolean (G_TASK (res), error); |
| } |
| |
| void |
| mm_port_serial_flash_cancel (MMPortSerial *self) |
| { |
| GTask *task; |
| |
| if (!self->priv->flash_task) |
| return; |
| |
| /* Recover task */ |
| task = self->priv->flash_task; |
| self->priv->flash_task = NULL; |
| |
| g_task_return_new_error (task, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_CANCELLED, |
| "Flash cancelled"); |
| g_object_unref (task); |
| } |
| |
| static gboolean |
| flash_do (MMPortSerial *self) |
| { |
| GTask *task; |
| FlashContext *ctx; |
| GError *error = NULL; |
| |
| /* Recover task */ |
| g_assert (self->priv->flash_task != NULL); |
| task = self->priv->flash_task; |
| self->priv->flash_task = NULL; |
| |
| ctx = g_task_get_task_data (task); |
| ctx->flash_id = 0; |
| |
| if (self->priv->flash_ok && mm_port_get_subsys (MM_PORT (self)) == MM_PORT_SUBSYS_TTY) { |
| if (ctx->current_speed) { |
| if (!set_speed (self, ctx->current_speed, &error)) |
| g_assert (error); |
| } else { |
| error = g_error_new_literal (MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_FLASH_FAILED, |
| "Failed to retrieve current speed"); |
| } |
| } |
| |
| if (error) |
| g_task_return_error (task, error); |
| else |
| g_task_return_boolean (task, TRUE); |
| g_object_unref (task); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| void |
| mm_port_serial_flash (MMPortSerial *self, |
| guint32 flash_time, |
| gboolean ignore_errors, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| FlashContext *ctx; |
| GTask *task; |
| GError *error = NULL; |
| gboolean success; |
| |
| g_return_if_fail (MM_IS_PORT_SERIAL (self)); |
| |
| /* Setup context */ |
| ctx = g_slice_new0 (FlashContext); |
| |
| task = g_task_new (self, NULL, callback, user_data); |
| g_task_set_task_data (task, ctx, (GDestroyNotify)flash_context_free); |
| |
| if (!mm_port_serial_is_open (self)) { |
| g_task_return_new_error (task, |
| MM_SERIAL_ERROR, |
| MM_SERIAL_ERROR_NOT_OPEN, |
| "The serial port is not open."); |
| g_object_unref (task); |
| return; |
| } |
| |
| if (self->priv->flash_task) { |
| g_task_return_new_error (task, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_IN_PROGRESS, |
| "Modem is already being flashed."); |
| g_object_unref (task); |
| return; |
| } |
| |
| /* Flashing only in TTY */ |
| if (!self->priv->flash_ok || mm_port_get_subsys (MM_PORT (self)) != MM_PORT_SUBSYS_TTY) { |
| self->priv->flash_task = task; |
| ctx->flash_id = g_idle_add ((GSourceFunc)flash_do, self); |
| return; |
| } |
| |
| /* Grab current speed so we can reset it after flashing */ |
| success = get_speed (self, &ctx->current_speed, &error); |
| if (!success && !ignore_errors) { |
| g_task_return_error (task, error); |
| g_object_unref (task); |
| return; |
| } |
| g_clear_error (&error); |
| |
| success = set_speed (self, B0, &error); |
| if (!success && !ignore_errors) { |
| g_task_return_error (task, error); |
| g_object_unref (task); |
| return; |
| } |
| g_clear_error (&error); |
| |
| self->priv->flash_task = task; |
| ctx->flash_id = g_timeout_add (flash_time, (GSourceFunc)flash_do, self); |
| } |
| |
| /*****************************************************************************/ |
| |
| gboolean |
| mm_port_serial_set_flow_control (MMPortSerial *self, |
| MMFlowControl flow_control, |
| GError **error) |
| { |
| struct termios options; |
| gchar *flow_control_str = NULL; |
| GError *inner_error = NULL; |
| |
| /* retrieve current settings */ |
| memset (&options, 0, sizeof (struct termios)); |
| if (tcgetattr (self->priv->fd, &options) != 0) { |
| inner_error = g_error_new (MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "couldn't get serial port attributes: %s", g_strerror (errno)); |
| goto out; |
| } |
| |
| flow_control_str = mm_flow_control_build_string_from_mask (flow_control); |
| |
| /* Return if current settings are already what we want */ |
| if (!set_flow_control_termios (self, flow_control, &options)) { |
| mm_dbg ("(%s): no need to change flow control settings: already %s", |
| mm_port_get_device (MM_PORT (self)), flow_control_str); |
| goto out; |
| } |
| |
| if (!internal_tcsetattr (self, self->priv->fd, &options, &inner_error)) |
| goto out; |
| |
| mm_dbg ("(%s): flow control settings updated to %s", |
| mm_port_get_device (MM_PORT (self)), flow_control_str); |
| |
| out: |
| g_free (flow_control_str); |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| MMFlowControl |
| mm_port_serial_get_flow_control (MMPortSerial *self) |
| { |
| return self->priv->flow_control; |
| } |
| |
| /*****************************************************************************/ |
| |
| MMPortSerial * |
| mm_port_serial_new (const char *name, MMPortType ptype) |
| { |
| return MM_PORT_SERIAL (g_object_new (MM_TYPE_PORT_SERIAL, |
| MM_PORT_DEVICE, name, |
| MM_PORT_SUBSYS, MM_PORT_SUBSYS_TTY, |
| MM_PORT_TYPE, ptype, |
| NULL)); |
| } |
| |
| static gboolean |
| ba_equal (gconstpointer v1, gconstpointer v2) |
| { |
| const GByteArray *a = v1; |
| const GByteArray *b = v2; |
| |
| if (!a && b) |
| return -1; |
| else if (a && !b) |
| return 1; |
| else if (!a && !b) |
| return 0; |
| |
| g_assert (a && b); |
| if (a->len < b->len) |
| return -1; |
| else if (a->len > b->len) |
| return 1; |
| |
| g_assert (a->len == b->len); |
| return !memcmp (a->data, b->data, a->len); |
| } |
| |
| static guint |
| ba_hash (gconstpointer v) |
| { |
| /* 31 bit hash function */ |
| const GByteArray *array = v; |
| guint32 i, h = (const signed char) array->data[0]; |
| |
| for (i = 1; i < array->len; i++) |
| h = (h << 5) - h + (const signed char) array->data[i]; |
| |
| return h; |
| } |
| |
| static void |
| ba_free (gpointer v) |
| { |
| g_byte_array_unref ((GByteArray *) v); |
| } |
| |
| static void |
| mm_port_serial_init (MMPortSerial *self) |
| { |
| self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_PORT_SERIAL, MMPortSerialPrivate); |
| |
| self->priv->reply_cache = g_hash_table_new_full (ba_hash, ba_equal, ba_free, ba_free); |
| |
| self->priv->fd = -1; |
| self->priv->baud = 57600; |
| self->priv->bits = 8; |
| self->priv->parity = 'n'; |
| self->priv->stopbits = 1; |
| self->priv->flow_control = MM_FLOW_CONTROL_UNKNOWN; |
| self->priv->send_delay = 1000; |
| |
| self->priv->queue = g_queue_new (); |
| self->priv->response = g_byte_array_sized_new (500); |
| } |
| |
| static void |
| set_property (GObject *object, |
| guint prop_id, |
| const GValue *value, |
| GParamSpec *pspec) |
| { |
| MMPortSerial *self = MM_PORT_SERIAL (object); |
| |
| switch (prop_id) { |
| case PROP_FD: |
| self->priv->fd = g_value_get_int (value); |
| break; |
| case PROP_BAUD: |
| self->priv->baud = g_value_get_uint (value); |
| break; |
| case PROP_BITS: |
| self->priv->bits = g_value_get_uint (value); |
| break; |
| case PROP_PARITY: |
| self->priv->parity = g_value_get_schar (value); |
| break; |
| case PROP_STOPBITS: |
| self->priv->stopbits = g_value_get_uint (value); |
| break; |
| case PROP_FLOW_CONTROL: |
| self->priv->flow_control = g_value_get_flags (value); |
| break; |
| case PROP_SEND_DELAY: |
| self->priv->send_delay = g_value_get_uint64 (value); |
| break; |
| case PROP_SPEW_CONTROL: |
| self->priv->spew_control = g_value_get_boolean (value); |
| break; |
| case PROP_FLASH_OK: |
| self->priv->flash_ok = g_value_get_boolean (value); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| get_property (GObject *object, |
| guint prop_id, |
| GValue *value, |
| GParamSpec *pspec) |
| { |
| MMPortSerial *self = MM_PORT_SERIAL (object); |
| |
| switch (prop_id) { |
| case PROP_FD: |
| g_value_set_int (value, self->priv->fd); |
| break; |
| case PROP_BAUD: |
| g_value_set_uint (value, self->priv->baud); |
| break; |
| case PROP_BITS: |
| g_value_set_uint (value, self->priv->bits); |
| break; |
| case PROP_PARITY: |
| g_value_set_schar (value, self->priv->parity); |
| break; |
| case PROP_STOPBITS: |
| g_value_set_uint (value, self->priv->stopbits); |
| break; |
| case PROP_FLOW_CONTROL: |
| g_value_set_flags (value, self->priv->flow_control); |
| break; |
| case PROP_SEND_DELAY: |
| g_value_set_uint64 (value, self->priv->send_delay); |
| break; |
| case PROP_SPEW_CONTROL: |
| g_value_set_boolean (value, self->priv->spew_control); |
| break; |
| case PROP_FLASH_OK: |
| g_value_set_boolean (value, self->priv->flash_ok); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| finalize (GObject *object) |
| { |
| MMPortSerial *self = MM_PORT_SERIAL (object); |
| |
| port_serial_close_force (MM_PORT_SERIAL (object)); |
| mm_port_serial_flash_cancel (MM_PORT_SERIAL (object)); |
| |
| /* These are disposed during port closing */ |
| g_assert (self->priv->iochannel == NULL); |
| g_assert (self->priv->iochannel_id == 0); |
| g_assert (self->priv->socket == NULL); |
| g_assert (self->priv->socket_source == NULL); |
| |
| if (self->priv->timeout_id) |
| g_source_remove (self->priv->timeout_id); |
| |
| if (self->priv->queue_id) |
| g_source_remove (self->priv->queue_id); |
| |
| g_hash_table_destroy (self->priv->reply_cache); |
| g_byte_array_unref (self->priv->response); |
| g_queue_free (self->priv->queue); |
| |
| G_OBJECT_CLASS (mm_port_serial_parent_class)->finalize (object); |
| } |
| |
| static void |
| mm_port_serial_class_init (MMPortSerialClass *klass) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS (klass); |
| |
| g_type_class_add_private (object_class, sizeof (MMPortSerialPrivate)); |
| |
| /* Virtual methods */ |
| object_class->set_property = set_property; |
| object_class->get_property = get_property; |
| object_class->finalize = finalize; |
| |
| klass->config_fd = real_config_fd; |
| |
| /* Properties */ |
| g_object_class_install_property |
| (object_class, PROP_FD, |
| g_param_spec_int (MM_PORT_SERIAL_FD, |
| "File descriptor", |
| "File descriptor", |
| -1, G_MAXINT, -1, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); |
| |
| g_object_class_install_property |
| (object_class, PROP_BAUD, |
| g_param_spec_uint (MM_PORT_SERIAL_BAUD, |
| "Baud", |
| "Baud rate", |
| 0, G_MAXUINT, 57600, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_BITS, |
| g_param_spec_uint (MM_PORT_SERIAL_BITS, |
| "Bits", |
| "Bits", |
| 5, 8, 8, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_PARITY, |
| g_param_spec_char (MM_PORT_SERIAL_PARITY, |
| "Parity", |
| "Parity", |
| 'E', 'o', 'n', |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_STOPBITS, |
| g_param_spec_uint (MM_PORT_SERIAL_STOPBITS, |
| "Stopbits", |
| "Stopbits", |
| 1, 2, 1, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_FLOW_CONTROL, |
| g_param_spec_flags (MM_PORT_SERIAL_FLOW_CONTROL, |
| "FlowControl", |
| "Select flow control", |
| MM_TYPE_FLOW_CONTROL, |
| MM_FLOW_CONTROL_UNKNOWN, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_SEND_DELAY, |
| g_param_spec_uint64 (MM_PORT_SERIAL_SEND_DELAY, |
| "SendDelay", |
| "Send delay for each byte in microseconds", |
| 0, G_MAXUINT64, 0, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_SPEW_CONTROL, |
| g_param_spec_boolean (MM_PORT_SERIAL_SPEW_CONTROL, |
| "SpewControl", |
| "Spew control", |
| FALSE, |
| G_PARAM_READWRITE)); |
| |
| g_object_class_install_property |
| (object_class, PROP_FLASH_OK, |
| g_param_spec_boolean (MM_PORT_SERIAL_FLASH_OK, |
| "FlashOk", |
| "Flashing the port (0 baud for a short period) " |
| "is allowed.", |
| TRUE, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); |
| |
| /* Signals */ |
| signals[BUFFER_FULL] = |
| g_signal_new ("buffer-full", |
| G_OBJECT_CLASS_TYPE (object_class), |
| G_SIGNAL_RUN_FIRST, |
| G_STRUCT_OFFSET (MMPortSerialClass, buffer_full), |
| NULL, NULL, |
| g_cclosure_marshal_generic, |
| G_TYPE_NONE, 1, G_TYPE_POINTER); |
| |
| signals[TIMED_OUT] = |
| g_signal_new ("timed-out", |
| G_OBJECT_CLASS_TYPE (object_class), |
| G_SIGNAL_RUN_FIRST, |
| G_STRUCT_OFFSET (MMPortSerialClass, timed_out), |
| NULL, NULL, |
| g_cclosure_marshal_generic, |
| G_TYPE_NONE, 1, G_TYPE_UINT); |
| |
| signals[FORCED_CLOSE] = |
| g_signal_new ("forced-close", |
| G_OBJECT_CLASS_TYPE (object_class), |
| G_SIGNAL_RUN_FIRST, |
| G_STRUCT_OFFSET (MMPortSerialClass, forced_close), |
| NULL, NULL, |
| g_cclosure_marshal_generic, |
| G_TYPE_NONE, 0); |
| } |