blob: 961e79e6df17cd92d5527307a629d15b16e7b64e [file] [log] [blame]
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* libmbim-glib -- GLib/GIO based library to control MBIM devices
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
* Copyright (C) 2013-2014 Aleksander Morgado <aleksander@aleksander.es>
* Copyright (C) 2014 Smith Micro Software, Inc.
*
* Implementation based on the 'QmiDevice' GObject from libqmi-glib.
*/
#include <config.h>
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <gio/gio.h>
#include <gio/gunixsocketaddress.h>
#include <sys/ioctl.h>
#define IOCTL_WDM_MAX_COMMAND _IOR('H', 0xA0, guint16)
#define RETRY_TIMEOUT_SECS 5
#if WITH_UDEV
# include <gudev/gudev.h>
#endif
#include "mbim-utils.h"
#include "mbim-device.h"
#include "mbim-message.h"
#include "mbim-message-private.h"
#include "mbim-error-types.h"
#include "mbim-enum-types.h"
#include "mbim-proxy.h"
#include "mbim-proxy-control.h"
/**
* SECTION:mbim-device
* @title: MbimDevice
* @short_description: Generic MBIM device handling routines
*
* #MbimDevice is a generic type in charge of controlling the access to the
* managed MBIM port.
*
* A #MbimDevice can only handle one single MBIM port.
*/
static void async_initable_iface_init (GAsyncInitableIface *iface);
G_DEFINE_TYPE_EXTENDED (MbimDevice, mbim_device, G_TYPE_OBJECT, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init))
enum {
PROP_0,
PROP_FILE,
PROP_TRANSACTION_ID,
PROP_IN_SESSION,
PROP_LAST
};
static GParamSpec *properties[PROP_LAST];
enum {
SIGNAL_INDICATE_STATUS,
SIGNAL_ERROR,
SIGNAL_REMOVED,
SIGNAL_LAST
};
static guint signals[SIGNAL_LAST] = { 0 };
typedef enum {
TRANSACTION_TYPE_HOST = 0,
TRANSACTION_TYPE_MODEM = 1,
TRANSACTION_TYPE_LAST = 2
} TransactionType;
typedef enum {
OPEN_STATUS_CLOSED = 0,
OPEN_STATUS_OPENING = 1,
OPEN_STATUS_OPEN = 2
} OpenStatus;
struct _MbimDevicePrivate {
/* File */
GFile *file;
gchar *path;
gchar *path_display;
/* I/O channel, set when the file is open */
GIOChannel *iochannel;
GSource *iochannel_source;
GByteArray *response;
OpenStatus open_status;
/* Support for mbim-proxy */
GSocketClient *socket_client;
GSocketConnection *socket_connection;
/* HT to keep track of ongoing host/function transactions
* Host transactions: created by us
* Modem transactions: modem-created indications with multiple fragments
*/
GHashTable *transactions[TRANSACTION_TYPE_LAST];
/* Transaction ID in the device */
guint32 transaction_id;
/* Flag to specify whether we're in a session */
gboolean in_session;
/* message size */
guint16 max_control_transfer;
};
#define MAX_SPAWN_RETRIES 10
#define MAX_CONTROL_TRANSFER 4096
#define MAX_TIME_BETWEEN_FRAGMENTS_MS 1250
static void device_report_error (MbimDevice *self,
guint32 transaction_id,
const GError *error);
/*****************************************************************************/
/* Message transactions (private) */
typedef struct {
MbimDevice *self;
guint32 transaction_id;
TransactionType type;
} TransactionWaitContext;
typedef struct {
MbimDevice *self;
MbimMessage *fragments;
MbimMessageType type;
guint32 transaction_id;
GSimpleAsyncResult *result;
GSource *timeout_source;
GCancellable *cancellable;
gulong cancellable_id;
TransactionWaitContext *wait_ctx;
} Transaction;
/* #define TRACE_TRANSACTION 1 */
#ifdef TRACE_TRANSACTION
static void
trace_transaction (Transaction *tr,
const gchar *state)
{
g_debug ("[%s,%u] transaction %s: %s",
tr->self->priv->path_display,
tr->transaction_id,
mbim_message_type_get_string (tr->type),
state);
}
#else
# define trace_transaction(...)
#endif
static Transaction *
transaction_new (MbimDevice *self,
MbimMessageType type,
guint32 transaction_id,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
Transaction *tr;
tr = g_slice_new0 (Transaction);
tr->type = type;
tr->transaction_id = transaction_id;
tr->self = g_object_ref (self);
tr->result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
transaction_new);
if (cancellable)
tr->cancellable = g_object_ref (cancellable);
trace_transaction (tr, "new");
return tr;
}
static void
transaction_complete_and_free (Transaction *tr,
const GError *error)
{
if (tr->timeout_source)
g_source_destroy (tr->timeout_source);
if (tr->cancellable) {
if (tr->cancellable_id)
g_cancellable_disconnect (tr->cancellable, tr->cancellable_id);
g_object_unref (tr->cancellable);
}
if (tr->wait_ctx)
g_slice_free (TransactionWaitContext, tr->wait_ctx);
if (error) {
trace_transaction (tr, "complete: response");
g_simple_async_result_set_from_error (tr->result, error);
if (tr->fragments)
mbim_message_unref (tr->fragments);
} else {
trace_transaction (tr, "complete: error");
g_assert (tr->fragments != NULL);
g_simple_async_result_set_op_res_gpointer (tr->result,
tr->fragments,
(GDestroyNotify) mbim_message_unref);
}
g_simple_async_result_complete_in_idle (tr->result);
g_object_unref (tr->result);
g_object_unref (tr->self);
g_slice_free (Transaction, tr);
}
static Transaction *
device_release_transaction (MbimDevice *self,
TransactionType type,
MbimMessageType expected_type,
guint32 transaction_id)
{
Transaction *tr = NULL;
/* Only return transaction if it was released from the HT */
if (self->priv->transactions[type]) {
tr = g_hash_table_lookup (self->priv->transactions[type], GUINT_TO_POINTER (transaction_id));
if (tr && ((tr->type == expected_type) || (expected_type == MBIM_MESSAGE_TYPE_INVALID))) {
/* If found, remove it from the HT */
trace_transaction (tr, "release");
g_hash_table_remove (self->priv->transactions[type], GUINT_TO_POINTER (transaction_id));
return tr;
}
}
return NULL;
}
static gboolean
transaction_timed_out (TransactionWaitContext *ctx)
{
Transaction *tr;
GError *error = NULL;
tr = device_release_transaction (ctx->self,
ctx->type,
MBIM_MESSAGE_TYPE_INVALID,
ctx->transaction_id);
if (!tr)
/* transaction already completed */
return FALSE;
tr->timeout_source = NULL;
/* If no fragment was received, complete transaction with a timeout error */
if (!tr->fragments)
error = g_error_new (MBIM_CORE_ERROR,
MBIM_CORE_ERROR_TIMEOUT,
"Transaction timed out");
else {
/* Fragment timeout... */
error = g_error_new (MBIM_PROTOCOL_ERROR,
MBIM_PROTOCOL_ERROR_TIMEOUT_FRAGMENT,
"Fragment timed out");
/* Also notify to the modem */
device_report_error (ctx->self,
tr->transaction_id,
error);
}
transaction_complete_and_free (tr, error);
g_error_free (error);
return FALSE;
}
static void
transaction_cancelled (GCancellable *cancellable,
TransactionWaitContext *ctx)
{
Transaction *tr;
GError *error = NULL;
tr = device_release_transaction (ctx->self,
ctx->type,
MBIM_MESSAGE_TYPE_INVALID,
ctx->transaction_id);
/* The transaction may have already been cancelled before we stored it in
* the tracking table */
if (!tr)
return;
tr->cancellable_id = 0;
/* Complete transaction with an abort error */
error = g_error_new (MBIM_CORE_ERROR,
MBIM_CORE_ERROR_ABORTED,
"Transaction aborted");
transaction_complete_and_free (tr, error);
g_error_free (error);
}
static gboolean
device_store_transaction (MbimDevice *self,
TransactionType type,
Transaction *tr,
guint timeout_ms,
GError **error)
{
trace_transaction (tr, "store");
if (G_UNLIKELY (!self->priv->transactions[type]))
self->priv->transactions[type] = g_hash_table_new (g_direct_hash, g_direct_equal);
tr->wait_ctx = g_slice_new (TransactionWaitContext);
tr->wait_ctx->self = self;
/* valid as long as the transaction is in the HT */
tr->wait_ctx->transaction_id = tr->transaction_id;
tr->wait_ctx->type = type;
/* don't add timeout if one already exists */
if (!tr->timeout_source) {
tr->timeout_source = g_timeout_source_new (timeout_ms);
g_source_set_callback (tr->timeout_source, (GSourceFunc)transaction_timed_out, tr->wait_ctx, NULL);
g_source_attach (tr->timeout_source, g_main_context_get_thread_default ());
g_source_unref (tr->timeout_source);
}
if (tr->cancellable && !tr->cancellable_id) {
/* Note: transaction_cancelled() will also be called directly if the
* cancellable is already cancelled */
tr->cancellable_id = g_cancellable_connect (tr->cancellable,
(GCallback)transaction_cancelled,
tr->wait_ctx,
NULL);
if (!tr->cancellable_id) {
g_set_error_literal (error,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_ABORTED,
"Request is already cancelled");
return FALSE;
}
}
/* Keep in the HT */
g_hash_table_insert (self->priv->transactions[type], GUINT_TO_POINTER (tr->transaction_id), tr);
return TRUE;
}
/*****************************************************************************/
/**
* mbim_device_get_file:
* @self: a #MbimDevice.
*
* Get the #GFile associated with this #MbimDevice.
*
* Returns: a #GFile that must be freed with g_object_unref().
*/
GFile *
mbim_device_get_file (MbimDevice *self)
{
GFile *file = NULL;
g_return_val_if_fail (MBIM_IS_DEVICE (self), NULL);
g_object_get (G_OBJECT (self),
MBIM_DEVICE_FILE, &file,
NULL);
return file;
}
/**
* mbim_device_peek_file:
* @self: a #MbimDevice.
*
* Get the #GFile associated with this #MbimDevice, without increasing the reference count
* on the returned object.
*
* Returns: a #GFile. Do not free the returned object, it is owned by @self.
*/
GFile *
mbim_device_peek_file (MbimDevice *self)
{
g_return_val_if_fail (MBIM_IS_DEVICE (self), NULL);
return self->priv->file;
}
/**
* mbim_device_get_path:
* @self: a #MbimDevice.
*
* Get the system path of the underlying MBIM device.
*
* Returns: the system path of the device.
*/
const gchar *
mbim_device_get_path (MbimDevice *self)
{
g_return_val_if_fail (MBIM_IS_DEVICE (self), NULL);
return self->priv->path;
}
/**
* mbim_device_get_path_display:
* @self: a #MbimDevice.
*
* Get the system path of the underlying MBIM device in UTF-8.
*
* Returns: UTF-8 encoded system path of the device.
*/
const gchar *
mbim_device_get_path_display (MbimDevice *self)
{
g_return_val_if_fail (MBIM_IS_DEVICE (self), NULL);
return self->priv->path_display;
}
/**
* mbim_device_is_open:
* @self: a #MbimDevice.
*
* Checks whether the #MbimDevice is open for I/O.
*
* Returns: %TRUE if @self is open, %FALSE otherwise.
*/
gboolean
mbim_device_is_open (MbimDevice *self)
{
g_return_val_if_fail (MBIM_IS_DEVICE (self), FALSE);
return (self->priv->open_status == OPEN_STATUS_OPEN);
}
/*****************************************************************************/
/* Open device */
static void
indication_ready (MbimDevice *self,
GAsyncResult *res)
{
GError *error = NULL;
MbimMessage *indication;
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), &error)) {
g_debug ("[%s] Error processing indication message: %s",
self->priv->path_display,
error->message);
g_error_free (error);
return;
}
indication = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (res));
g_signal_emit (self, signals[SIGNAL_INDICATE_STATUS], 0, indication);
}
static void
process_message (MbimDevice *self,
const MbimMessage *message)
{
gboolean is_partial_fragment;
is_partial_fragment = (_mbim_message_is_fragment (message) &&
_mbim_message_fragment_get_total (message) > 1);
if (mbim_utils_get_traces_enabled ()) {
gchar *printable;
printable = __mbim_utils_str_hex (((GByteArray *)message)->data,
((GByteArray *)message)->len,
':');
g_debug ("[%s] Received message...%s\n"
">>>>>> RAW:\n"
">>>>>> length = %u\n"
">>>>>> data = %s\n",
self->priv->path_display,
is_partial_fragment ? " (partial fragment)" : "",
((GByteArray *)message)->len,
printable);
g_free (printable);
if (is_partial_fragment) {
printable = mbim_message_get_printable (message, ">>>>>> ", TRUE);
g_debug ("[%s] Received message fragment (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
}
}
switch (MBIM_MESSAGE_GET_MESSAGE_TYPE (message)) {
case MBIM_MESSAGE_TYPE_OPEN_DONE:
case MBIM_MESSAGE_TYPE_CLOSE_DONE:
case MBIM_MESSAGE_TYPE_COMMAND_DONE:
case MBIM_MESSAGE_TYPE_INDICATE_STATUS: {
GError *error = NULL;
Transaction *tr;
if (MBIM_MESSAGE_GET_MESSAGE_TYPE (message) == MBIM_MESSAGE_TYPE_INDICATE_STATUS) {
/* Grab transaction */
tr = device_release_transaction (self,
TRANSACTION_TYPE_MODEM,
MBIM_MESSAGE_TYPE_INDICATE_STATUS,
mbim_message_get_transaction_id (message));
if (!tr)
/* Create new transaction for the indication */
tr = transaction_new (self,
MBIM_MESSAGE_TYPE_INDICATE_STATUS,
mbim_message_get_transaction_id (message),
NULL, /* no cancellable */
(GAsyncReadyCallback)indication_ready,
NULL);
} else {
/* Grab transaction. This is a _DONE message, so look for the request
* that generated the _DONE */
tr = device_release_transaction (self,
TRANSACTION_TYPE_HOST,
(MBIM_MESSAGE_GET_MESSAGE_TYPE (message) - 0x80000000),
mbim_message_get_transaction_id (message));
if (!tr) {
gchar *printable;
g_debug ("[%s] No transaction matched in received message",
self->priv->path_display);
/* Attempt to print a user friendly dump of the packet anyway */
printable = mbim_message_get_printable (message, ">>>>>> ", is_partial_fragment);
if (printable) {
g_debug ("[%s] Received unexpected message (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
}
return;
}
/* If the message doesn't have fragments, we're done */
if (!_mbim_message_is_fragment (message)) {
g_assert (tr->fragments == NULL);
tr->fragments = mbim_message_dup (message);
transaction_complete_and_free (tr, NULL);
return;
}
}
/* More than one fragment expected; is this the first one? */
if (!tr->fragments)
tr->fragments = _mbim_message_fragment_collector_init (message, &error);
else
_mbim_message_fragment_collector_add (tr->fragments, message, &error);
if (error) {
device_report_error (self,
tr->transaction_id,
error);
transaction_complete_and_free (tr, error);
g_error_free (error);
return;
}
/* Did we get all needed fragments? */
if (_mbim_message_fragment_collector_complete (tr->fragments)) {
/* Now, translate the whole message */
if (mbim_utils_get_traces_enabled ()) {
gchar *printable;
printable = mbim_message_get_printable (tr->fragments, ">>>>>> ", FALSE);
g_debug ("[%s] Received message (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
}
transaction_complete_and_free (tr, NULL);
return;
}
/* Need more fragments, store transaction */
g_assert (device_store_transaction (self,
TRANSACTION_TYPE_HOST,
tr,
MAX_TIME_BETWEEN_FRAGMENTS_MS,
NULL));
return;
}
case MBIM_MESSAGE_TYPE_FUNCTION_ERROR: {
Transaction *tr;
GError *error_indication;
/* Try to match this transaction just per transaction ID */
tr = device_release_transaction (self,
TRANSACTION_TYPE_HOST,
MBIM_MESSAGE_TYPE_INVALID,
mbim_message_get_transaction_id (message));
if (!tr)
g_debug ("[%s] No transaction matched in received function error message",
self->priv->path_display);
if (mbim_utils_get_traces_enabled ()) {
gchar *printable;
printable = mbim_message_get_printable (message, ">>>>>> ", FALSE);
g_debug ("[%s] Received message (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
}
if (tr) {
if (tr->fragments)
mbim_message_unref (tr->fragments);
tr->fragments = mbim_message_dup (message);
transaction_complete_and_free (tr, NULL);
}
/* Signals are emitted regardless of whether the transaction matched or not */
error_indication = mbim_message_error_get_error (message);
g_signal_emit (self, signals[SIGNAL_ERROR], 0, error_indication);
g_error_free (error_indication);
return;
}
case MBIM_MESSAGE_TYPE_INVALID:
case MBIM_MESSAGE_TYPE_OPEN:
case MBIM_MESSAGE_TYPE_CLOSE:
case MBIM_MESSAGE_TYPE_COMMAND:
case MBIM_MESSAGE_TYPE_HOST_ERROR:
/* Shouldn't expect host-generated messages as replies */
g_message ("[%s] Host-generated message received: ignoring",
self->priv->path_display);
return;
}
}
static void
parse_response (MbimDevice *self)
{
do {
const MbimMessage *message;
guint32 in_length;
/* If not even the MBIM header available, just return */
if (self->priv->response->len < 12)
return;
message = (const MbimMessage *)self->priv->response;
/* No full message yet */
in_length = mbim_message_get_message_length (message);
if (self->priv->response->len < in_length)
return;
/* Play with the received message */
process_message (self, message);
/* If we were force-closed during the processing of a message, we'd be
* losing the response array directly, so check just in case */
if (!self->priv->response)
break;
/* Remove message from buffer */
g_byte_array_remove_range (self->priv->response, 0, in_length);
} while (self->priv->response->len > 0);
}
static gboolean
data_available (GIOChannel *source,
GIOCondition condition,
MbimDevice *self)
{
gsize bytes_read;
GIOStatus status;
gchar buffer[MAX_CONTROL_TRANSFER + 1];
if (condition & G_IO_HUP) {
g_debug ("[%s] unexpected port hangup!",
self->priv->path_display);
if (self->priv->response &&
self->priv->response->len)
g_byte_array_remove_range (self->priv->response, 0, self->priv->response->len);
mbim_device_close_force (self, NULL);
g_signal_emit (self, signals[SIGNAL_REMOVED], 0 );
return FALSE;
}
if (condition & G_IO_ERR) {
if (self->priv->response &&
self->priv->response->len)
g_byte_array_remove_range (self->priv->response, 0, self->priv->response->len);
return TRUE;
}
/* If not ready yet, prepare the response with default initial size. */
if (G_UNLIKELY (!self->priv->response))
self->priv->response = g_byte_array_sized_new (500);
do {
GError *error = NULL;
status = g_io_channel_read_chars (source,
buffer,
self->priv->max_control_transfer,
&bytes_read,
&error);
if (status == G_IO_STATUS_ERROR) {
if (error) {
g_warning ("[%s] error reading from the IOChannel: '%s'",
self->priv->path_display,
error->message);
g_error_free (error);
}
/* Port is closed; we're done */
if (!self->priv->iochannel_source)
break;
}
/* If no bytes read, just let g_io_channel wait for more data */
if (bytes_read == 0)
break;
if (bytes_read > 0)
g_byte_array_append (self->priv->response, (const guint8 *)buffer, bytes_read);
/* Try to parse what we already got */
parse_response (self);
/* And keep on if we were told to keep on */
} while (bytes_read == self->priv->max_control_transfer || status == G_IO_STATUS_AGAIN);
return TRUE;
}
/* "MBIM Control Model Functional Descriptor" */
struct usb_cdc_mbim_desc {
guint8 bLength;
guint8 bDescriptorType;
guint8 bDescriptorSubType;
guint16 bcdMBIMVersion;
guint16 wMaxControlMessage;
guint8 bNumberFilters;
guint8 bMaxFilterSize;
guint16 wMaxSegmentSize;
guint8 bmNetworkCapabilities;
} __attribute__ ((packed));
#if WITH_UDEV
static gchar *
get_descriptors_filepath (MbimDevice *self)
{
GUdevClient *client;
GUdevDevice *device = NULL;
GUdevDevice *parent_device = NULL;
GUdevDevice *grandparent_device = NULL;
gchar *descriptors_path = NULL;
gchar *device_basename = NULL;
client = g_udev_client_new (NULL);
if (!G_UDEV_IS_CLIENT (client)) {
g_warning ("[%s] Couldn't get udev client",
self->priv->path_display);
goto out;
}
/* We need to get the sysfs of the cdc-wdm's grandfather:
*
* * Device's sysfs path is like:
* /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.5/2-1.5:2.0/usbmisc/cdc-wdm0
* * Parent's sysfs path is like:
* /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.5/2-1.5:2.0
* * Grandparent's sysfs path is like:
* /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.5
*
* Which is the one with the descriptors file.
*/
device_basename = g_path_get_basename (self->priv->path);
device = g_udev_client_query_by_subsystem_and_name (client, "usb", device_basename);
if (!device) {
device = g_udev_client_query_by_subsystem_and_name (client, "usbmisc", device_basename);
if (!device) {
g_warning ("[%s] Couldn't find udev device",
self->priv->path_display);
goto out;
}
}
parent_device = g_udev_device_get_parent (device);
if (!parent_device) {
g_warning ("[%s] Couldn't find parent udev device",
self->priv->path_display);
goto out;
}
grandparent_device = g_udev_device_get_parent (parent_device);
if (!grandparent_device) {
g_warning ("[%s] Couldn't find grandparent udev device",
self->priv->path_display);
goto out;
}
descriptors_path = g_build_path (G_DIR_SEPARATOR_S,
g_udev_device_get_sysfs_path (grandparent_device),
"descriptors",
NULL);
out:
g_free (device_basename);
if (parent_device)
g_object_unref (parent_device);
if (grandparent_device)
g_object_unref (grandparent_device);
if (device)
g_object_unref (device);
if (client)
g_object_unref (client);
return descriptors_path;
}
#else
static gchar *
get_descriptors_filepath (MbimDevice *self)
{
static const gchar *subsystems[] = { "usbmisc", "usb" };
guint i;
gchar *device_basename;
gchar *descriptors_path = NULL;
device_basename = g_path_get_basename (self->priv->path);
for (i = 0; !descriptors_path && i < G_N_ELEMENTS (subsystems); i++) {
gchar *tmp;
gchar *path;
/* parent sysfs can be built directly using subsystem and name; e.g. for subsystem
* usbmisc and name cdc-wdm0:
* $ realpath /sys/class/usbmisc/cdc-wdm0/device
* /sys/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.5/2-1.5:2.0
*/
tmp = g_strdup_printf ("/sys/class/%s/%s/device", subsystems[i], device_basename);
path = canonicalize_file_name (tmp);
g_free (tmp);
if (g_file_test (path, G_FILE_TEST_EXISTS)) {
/* Now look for the parent dir with descriptors file. */
gchar *dirname;
dirname = g_path_get_dirname (path);
descriptors_path = g_build_path (G_DIR_SEPARATOR_S,
dirname,
"descriptors",
NULL);
g_free (dirname);
}
g_free (path);
}
g_free (device_basename);
if (descriptors_path && !g_file_test (descriptors_path, G_FILE_TEST_EXISTS)) {
g_warning ("[%s] Descriptors file doesn't exist",
self->priv->path_display);
g_free (descriptors_path);
descriptors_path = NULL;
}
return descriptors_path;
}
#endif
static guint16
read_max_control_transfer (MbimDevice *self)
{
static const guint8 mbim_signature[4] = { 0x0c, 0x24, 0x1b, 0x00 };
guint16 max = MAX_CONTROL_TRANSFER;
gchar *descriptors_path;
GError *error = NULL;
gchar *contents = NULL;
gsize length = 0;
guint i;
/* Build descriptors filepath */
descriptors_path = get_descriptors_filepath (self);
if (!descriptors_path) {
g_warning ("[%s] Couldn't get descriptors file path",
self->priv->path_display);
goto out;
}
if (!g_file_get_contents (descriptors_path,
&contents,
&length,
&error)) {
g_warning ("[%s] Couldn't read descriptors file: %s",
self->priv->path_display,
error->message);
g_error_free (error);
goto out;
}
i = 0;
while (i <= (length - sizeof (struct usb_cdc_mbim_desc))) {
/* Try to match the MBIM descriptor signature */
if ((memcmp (&contents[i], mbim_signature, sizeof (mbim_signature)) == 0)) {
/* Found! */
max = GUINT16_FROM_LE (((struct usb_cdc_mbim_desc *)&contents[i])->wMaxControlMessage);
g_debug ("[%s] Read max control message size from descriptors file: %" G_GUINT16_FORMAT,
self->priv->path_display,
max);
goto out;
}
/* The first byte of the descriptor info is the length; so keep on
* skipping descriptors until we match the MBIM one */
i += contents[i];
}
g_warning ("[%s] Couldn't find MBIM signature in descriptors file",
self->priv->path_display);
out:
g_free (contents);
return max;
}
typedef struct {
MbimDevice *self;
GSimpleAsyncResult *result;
guint spawn_retries;
} CreateIoChannelContext;
static void
create_iochannel_context_complete_and_free (CreateIoChannelContext *ctx)
{
g_simple_async_result_complete_in_idle (ctx->result);
g_object_unref (ctx->result);
g_object_unref (ctx->self);
g_slice_free (CreateIoChannelContext, ctx);
}
static gboolean
create_iochannel_finish (MbimDevice *self,
GAsyncResult *res,
GError **error)
{
return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error);
}
static void
setup_iochannel (CreateIoChannelContext *ctx)
{
GError *inner_error = NULL;
/* We don't want UTF-8 encoding, we're playing with raw binary data */
g_io_channel_set_encoding (ctx->self->priv->iochannel, NULL, NULL);
/* We don't want to get the channel buffered */
g_io_channel_set_buffered (ctx->self->priv->iochannel, FALSE);
/* Let the GIOChannel own the FD */
g_io_channel_set_close_on_unref (ctx->self->priv->iochannel, TRUE);
/* We don't want to get blocked while writing stuff */
if (!g_io_channel_set_flags (ctx->self->priv->iochannel,
G_IO_FLAG_NONBLOCK,
&inner_error)) {
g_simple_async_result_take_error (ctx->result, inner_error);
g_io_channel_shutdown (ctx->self->priv->iochannel, FALSE, NULL);
g_io_channel_unref (ctx->self->priv->iochannel);
ctx->self->priv->iochannel = NULL;
g_clear_object (&ctx->self->priv->socket_connection);
g_clear_object (&ctx->self->priv->socket_client);
create_iochannel_context_complete_and_free (ctx);
return;
}
ctx->self->priv->iochannel_source = g_io_create_watch (ctx->self->priv->iochannel,
G_IO_IN | G_IO_ERR | G_IO_HUP);
g_source_set_callback (ctx->self->priv->iochannel_source,
(GSourceFunc)data_available,
ctx->self,
NULL);
g_source_attach (ctx->self->priv->iochannel_source, g_main_context_get_thread_default ());
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
create_iochannel_context_complete_and_free (ctx);
}
static void
create_iochannel_with_fd (CreateIoChannelContext *ctx)
{
gint fd;
guint16 max;
errno = 0;
fd = open (ctx->self->priv->path, O_RDWR | O_EXCL | O_NONBLOCK | O_NOCTTY);
if (fd < 0) {
g_simple_async_result_set_error (
ctx->result,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_FAILED,
"Cannot open device file '%s': %s",
ctx->self->priv->path_display,
strerror (errno));
create_iochannel_context_complete_and_free (ctx);
return;
}
/* Query message size */
if (ioctl (fd, IOCTL_WDM_MAX_COMMAND, &max) < 0) {
g_debug ("[%s] Couldn't query maximum message size: "
"IOCTL_WDM_MAX_COMMAND failed: %s",
ctx->self->priv->path_display,
strerror (errno));
/* Fallback, try to read the descriptor file */
max = read_max_control_transfer (ctx->self);
} else {
g_debug ("[%s] Queried max control message size: %" G_GUINT16_FORMAT,
ctx->self->priv->path_display,
max);
}
ctx->self->priv->max_control_transfer = max;
/* Create new GIOChannel */
ctx->self->priv->iochannel = g_io_channel_unix_new (fd);
setup_iochannel (ctx);
}
static void create_iochannel_with_socket (CreateIoChannelContext *ctx);
static gboolean
wait_for_proxy_cb (CreateIoChannelContext *ctx)
{
create_iochannel_with_socket (ctx);
return FALSE;
}
static void
create_iochannel_with_socket (CreateIoChannelContext *ctx)
{
GSocketAddress *socket_address;
GError *error = NULL;
/* Create socket client */
if (ctx->self->priv->socket_client)
g_object_unref (ctx->self->priv->socket_client);
ctx->self->priv->socket_client = g_socket_client_new ();
g_socket_client_set_family (ctx->self->priv->socket_client, G_SOCKET_FAMILY_UNIX);
g_socket_client_set_socket_type (ctx->self->priv->socket_client, G_SOCKET_TYPE_STREAM);
g_socket_client_set_protocol (ctx->self->priv->socket_client, G_SOCKET_PROTOCOL_DEFAULT);
/* Setup socket address */
socket_address = (g_unix_socket_address_new_with_type (
MBIM_PROXY_SOCKET_PATH,
-1,
G_UNIX_SOCKET_ADDRESS_ABSTRACT));
/* Connect to address */
if (ctx->self->priv->socket_connection)
g_object_unref (ctx->self->priv->socket_connection);
ctx->self->priv->socket_connection = (g_socket_client_connect (
ctx->self->priv->socket_client,
G_SOCKET_CONNECTABLE (socket_address),
NULL,
&error));
g_object_unref (socket_address);
if (!ctx->self->priv->socket_connection) {
gchar **argc;
GSource *source;
g_debug ("cannot connect to proxy: %s", error->message);
g_clear_error (&error);
g_clear_object (&ctx->self->priv->socket_client);
/* Don't retry forever */
ctx->spawn_retries++;
if (ctx->spawn_retries > MAX_SPAWN_RETRIES) {
g_simple_async_result_set_error (ctx->result,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_FAILED,
"Couldn't spawn the mbim-proxy");
create_iochannel_context_complete_and_free (ctx);
return;
}
g_debug ("spawning new mbim-proxy (try %u)...", ctx->spawn_retries);
argc = g_new0 (gchar *, 2);
argc[0] = g_strdup (LIBEXEC_PATH "/mbim-proxy");
if (!g_spawn_async (NULL, /* working directory */
argc,
NULL, /* envp */
G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL,
NULL, /* child_setup */
NULL, /* child_setup_user_data */
NULL,
&error)) {
g_debug ("error spawning mbim-proxy: %s", error->message);
g_clear_error (&error);
}
g_strfreev (argc);
/* Wait some ms and retry */
source = g_timeout_source_new (100);
g_source_set_callback (source, (GSourceFunc)wait_for_proxy_cb, ctx, NULL);
g_source_attach (source, g_main_context_get_thread_default ());
g_source_unref (source);
return;
}
ctx->self->priv->iochannel = g_io_channel_unix_new (
g_socket_get_fd (
g_socket_connection_get_socket (ctx->self->priv->socket_connection)));
/* try to read the descriptor file */
ctx->self->priv->max_control_transfer = read_max_control_transfer (ctx->self);
setup_iochannel (ctx);
}
static void
create_iochannel (MbimDevice *self,
gboolean proxy,
GAsyncReadyCallback callback,
gpointer user_data)
{
CreateIoChannelContext *ctx;
ctx = g_slice_new (CreateIoChannelContext);
ctx->self = g_object_ref (self);
ctx->result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
create_iochannel);
ctx->spawn_retries = 0;
if (self->priv->iochannel) {
g_simple_async_result_set_error (ctx->result,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_WRONG_STATE,
"Already open");
create_iochannel_context_complete_and_free (ctx);
return;
}
g_assert (self->priv->file);
g_assert (self->priv->path);
if (proxy)
create_iochannel_with_socket (ctx);
else
create_iochannel_with_fd (ctx);
}
typedef enum {
DEVICE_OPEN_CONTEXT_STEP_FIRST = 0,
DEVICE_OPEN_CONTEXT_STEP_CREATE_IOCHANNEL,
DEVICE_OPEN_CONTEXT_STEP_FLAGS_PROXY,
DEVICE_OPEN_CONTEXT_STEP_OPEN_MESSAGE,
DEVICE_OPEN_CONTEXT_STEP_LAST
} DeviceOpenContextStep;
typedef struct {
MbimDevice *self;
GSimpleAsyncResult *result;
GCancellable *cancellable;
DeviceOpenContextStep step;
MbimDeviceOpenFlags flags;
gint timeout;
} DeviceOpenContext;
static void
device_open_context_complete_and_free (DeviceOpenContext *ctx,
GError *error)
{
if (error)
g_simple_async_result_take_error (ctx->result, error);
else
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
g_simple_async_result_complete_in_idle (ctx->result);
g_object_unref (ctx->result);
if (ctx->cancellable)
g_object_unref (ctx->cancellable);
g_object_unref (ctx->self);
g_slice_free (DeviceOpenContext, ctx);
}
static void device_open_context_step (DeviceOpenContext *ctx);
/**
* mbim_device_open_full_finish:
* @self: a #MbimDevice.
* @res: a #GAsyncResult.
* @error: Return location for error or %NULL.
*
* Finishes an asynchronous open operation started with mbim_device_open_full().
*
* Returns: %TRUE if successful, %FALSE if @error is set.
*/
gboolean
mbim_device_open_full_finish (MbimDevice *self,
GAsyncResult *res,
GError **error)
{
return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error);
}
/**
* mbim_device_open_finish:
* @self: a #MbimDevice.
* @res: a #GAsyncResult.
* @error: Return location for error or %NULL.
*
* Finishes an asynchronous open operation started with mbim_device_open().
*
* Returns: %TRUE if successful, %FALSE if @error is set.
*/
gboolean
mbim_device_open_finish (MbimDevice *self,
GAsyncResult *res,
GError **error)
{
return mbim_device_open_full_finish (self, res, error);
}
static void open_message (DeviceOpenContext *ctx);
static void
open_message_ready (MbimDevice *self,
GAsyncResult *res,
DeviceOpenContext *ctx)
{
MbimMessage *response;
GError *error = NULL;
response = mbim_device_command_finish (self, res, &error);
if (!response) {
/* Check if we should be retrying */
if (g_error_matches (error, MBIM_CORE_ERROR, MBIM_CORE_ERROR_TIMEOUT)) {
/* The timeout will tell us how many retries we should do */
ctx->timeout -= RETRY_TIMEOUT_SECS;
if (ctx->timeout > 0) {
g_error_free (error);
open_message (ctx);
return;
}
/* No more seconds left in the timeout... return error */
}
g_debug ("open operation timed out: closed");
self->priv->open_status = OPEN_STATUS_CLOSED;
device_open_context_complete_and_free (ctx, error);
return;
}
if (!mbim_message_response_get_result (response, MBIM_MESSAGE_TYPE_OPEN_DONE, &error)) {
g_debug ("getting open done result failed: closed");
self->priv->open_status = OPEN_STATUS_CLOSED;
device_open_context_complete_and_free (ctx, error);
mbim_message_unref (response);
return;
}
mbim_message_unref (response);
/* go on */
ctx->step++;
device_open_context_step (ctx);
}
static void
open_message (DeviceOpenContext *ctx)
{
MbimMessage *request;
/* Launch 'Open' command */
request = mbim_message_open_new (mbim_device_get_next_transaction_id (ctx->self),
ctx->self->priv->max_control_transfer);
mbim_device_command (ctx->self,
request,
RETRY_TIMEOUT_SECS,
ctx->cancellable,
(GAsyncReadyCallback)open_message_ready,
ctx);
mbim_message_unref (request);
}
static void
proxy_cfg_message_ready (MbimDevice *self,
GAsyncResult *res,
DeviceOpenContext *ctx)
{
MbimMessage *response;
GError *error = NULL;
response = mbim_device_command_finish (self, res, &error);
if (!response) {
/* Hard error if proxy cfg command fails */
g_debug ("proxy configuration failed: closed");
self->priv->open_status = OPEN_STATUS_CLOSED;
device_open_context_complete_and_free (ctx, error);
return;
}
mbim_message_unref (response);
ctx->step++;
device_open_context_step (ctx);
}
static void
proxy_cfg_message (DeviceOpenContext *ctx)
{
GError *error = NULL;
MbimMessage *request;
request = mbim_message_proxy_control_configuration_set_new (ctx->self->priv->path, ctx->timeout, &error);
/* This message is no longer a direct reply; as the proxy will also try to open the device
* directly. If it cannot open the device, it will return an error. */
mbim_device_command (ctx->self,
request,
ctx->timeout,
ctx->cancellable,
(GAsyncReadyCallback)proxy_cfg_message_ready,
ctx);
mbim_message_unref (request);
}
static void
create_iochannel_ready (MbimDevice *self,
GAsyncResult *res,
DeviceOpenContext *ctx)
{
GError *error = NULL;
if (!create_iochannel_finish (self, res, &error)) {
g_debug ("creating iochannel failed: closed");
self->priv->open_status = OPEN_STATUS_CLOSED;
device_open_context_complete_and_free (ctx, error);
return;
}
/* Go on */
ctx->step++;
device_open_context_step (ctx);
}
static void
device_open_context_step (DeviceOpenContext *ctx)
{
switch (ctx->step) {
case DEVICE_OPEN_CONTEXT_STEP_FIRST:
if (ctx->self->priv->open_status == OPEN_STATUS_OPEN) {
GError *error;
error = g_error_new (MBIM_CORE_ERROR,
MBIM_CORE_ERROR_WRONG_STATE,
"Already open");
device_open_context_complete_and_free (ctx, error);
return;
}
if (ctx->self->priv->open_status == OPEN_STATUS_OPENING) {
GError *error;
error = g_error_new (MBIM_CORE_ERROR,
MBIM_CORE_ERROR_WRONG_STATE,
"Already opening");
device_open_context_complete_and_free (ctx, error);
return;
}
g_debug ("opening device...");
g_assert (ctx->self->priv->open_status == OPEN_STATUS_CLOSED);
ctx->self->priv->open_status = OPEN_STATUS_OPENING;
ctx->step++;
/* Fall down */
case DEVICE_OPEN_CONTEXT_STEP_CREATE_IOCHANNEL:
create_iochannel (ctx->self,
!!(ctx->flags & MBIM_DEVICE_OPEN_FLAGS_PROXY),
(GAsyncReadyCallback)create_iochannel_ready,
ctx);
return;
case DEVICE_OPEN_CONTEXT_STEP_FLAGS_PROXY:
if (ctx->flags & MBIM_DEVICE_OPEN_FLAGS_PROXY) {
proxy_cfg_message (ctx);
return;
}
ctx->step++;
/* Fall down */
case DEVICE_OPEN_CONTEXT_STEP_OPEN_MESSAGE:
/* If the device is already in-session, avoid the open message */
if (!ctx->self->priv->in_session) {
open_message (ctx);
return;
}
ctx->step++;
/* Fall down */
case DEVICE_OPEN_CONTEXT_STEP_LAST:
/* Nothing else to process, complete without error */
ctx->self->priv->open_status = OPEN_STATUS_OPEN;
device_open_context_complete_and_free (ctx, NULL);
return;
default:
break;
}
g_assert_not_reached ();
}
/**
* mbim_device_open_full:
* @self: a #MbimDevice.
* @flags: a set of #MbimDeviceOpenFlags.
* @timeout: maximum time, in seconds, to wait for the device to be opened.
* @cancellable: optional #GCancellable object, #NULL to ignore.
* @callback: a #GAsyncReadyCallback to call when the operation is finished.
* @user_data: the data to pass to callback function.
*
* Asynchronously opens a #MbimDevice for I/O.
*
* This method is an extension of the generic mbim_device_open(), which allows
* launching the #MbimDevice with proxy support.
*
* When the operation is finished @callback will be called. You can then call
* mbim_device_open_full_finish() to get the result of the operation.
*/
void
mbim_device_open_full (MbimDevice *self,
MbimDeviceOpenFlags flags,
guint timeout,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
DeviceOpenContext *ctx;
g_return_if_fail (MBIM_IS_DEVICE (self));
g_return_if_fail (timeout > 0);
ctx = g_slice_new (DeviceOpenContext);
ctx->self = g_object_ref (self);
ctx->result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
mbim_device_open_full);
ctx->step = DEVICE_OPEN_CONTEXT_STEP_FIRST;
ctx->flags = flags;
ctx->timeout = timeout;
ctx->cancellable = (cancellable ? g_object_ref (cancellable) : NULL);
/* Start processing */
device_open_context_step (ctx);
}
/**
* mbim_device_open:
* @self: a #MbimDevice.
* @timeout: maximum time, in seconds, to wait for the device to be opened.
* @cancellable: optional #GCancellable object, #NULL to ignore.
* @callback: a #GAsyncReadyCallback to call when the operation is finished.
* @user_data: the data to pass to callback function.
*
* Asynchronously opens a #MbimDevice for I/O.
*
* When the operation is finished @callback will be called. You can then call
* mbim_device_open_finish() to get the result of the operation.
*/
void
mbim_device_open (MbimDevice *self,
guint timeout,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
mbim_device_open_full (self,
MBIM_DEVICE_OPEN_FLAGS_NONE,
timeout,
cancellable,
callback,
user_data);
}
/*****************************************************************************/
/* Close channel */
static gboolean
destroy_iochannel (MbimDevice *self,
GError **error)
{
GError *inner_error = NULL;
self->priv->open_status = OPEN_STATUS_CLOSED;
/* Already closed? */
if (!self->priv->iochannel && !self->priv->socket_connection && !self->priv->socket_client)
return TRUE;
if (self->priv->iochannel) {
g_io_channel_shutdown (self->priv->iochannel, TRUE, &inner_error);
g_io_channel_unref (self->priv->iochannel);
self->priv->iochannel = NULL;
}
/* Failures when closing still make the device to get closed */
g_clear_object (&self->priv->socket_connection);
g_clear_object (&self->priv->socket_client);
if (self->priv->iochannel_source) {
g_source_destroy (self->priv->iochannel_source);
g_source_unref (self->priv->iochannel_source);
self->priv->iochannel_source = NULL;
}
if (self->priv->response) {
g_byte_array_unref (self->priv->response);
self->priv->response = NULL;
}
if (inner_error) {
g_propagate_error (error, inner_error);
return FALSE;
}
return TRUE;
}
/**
* mbim_device_close_force:
* @self: a #MbimDevice.
* @error: Return location for error or %NULL.
*
* Forces the #MbimDevice to be closed.
*
* Returns: %TRUE if @self if no error happens, otherwise %FALSE and @error is set.
*/
gboolean
mbim_device_close_force (MbimDevice *self,
GError **error)
{
g_return_val_if_fail (MBIM_IS_DEVICE (self), FALSE);
return destroy_iochannel (self, error);
}
typedef struct {
MbimDevice *self;
GSimpleAsyncResult *result;
GCancellable *cancellable;
guint timeout;
} DeviceCloseContext;
static void
device_close_context_complete_and_free (DeviceCloseContext *ctx)
{
g_simple_async_result_complete_in_idle (ctx->result);
g_object_unref (ctx->result);
if (ctx->cancellable)
g_object_unref (ctx->cancellable);
g_object_unref (ctx->self);
g_slice_free (DeviceCloseContext, ctx);
}
/**
* mbim_device_close_finish:
* @self: a #MbimDevice.
* @res: a #GAsyncResult.
* @error: Return location for error or %NULL.
*
* Finishes an asynchronous close operation started with mbim_device_close().
*
* Returns: %TRUE if successful, %FALSE if @error is set.
*/
gboolean
mbim_device_close_finish (MbimDevice *self,
GAsyncResult *res,
GError **error)
{
return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error);
}
static void
close_message_ready (MbimDevice *self,
GAsyncResult *res,
DeviceCloseContext *ctx)
{
MbimMessage *response;
GError *error = NULL;
response = mbim_device_command_finish (self, res, &error);
if (!response)
g_simple_async_result_take_error (ctx->result, error);
else if (!mbim_message_response_get_result (response, MBIM_MESSAGE_TYPE_CLOSE_DONE, &error))
g_simple_async_result_take_error (ctx->result, error);
else
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
if (response)
mbim_message_unref (response);
device_close_context_complete_and_free (ctx);
}
/**
* mbim_device_close:
* @self: a #MbimDevice.
* @timeout: maximum time, in seconds, to wait for the device to be closed.
* @cancellable: optional #GCancellable object, #NULL to ignore.
* @callback: a #GAsyncReadyCallback to call when the operation is finished.
* @user_data: the data to pass to callback function.
*
* Asynchronously closes a #MbimDevice for I/O.
*
* When the operation is finished @callback will be called. You can then call
* mbim_device_close_finish() to get the result of the operation.
*/
void
mbim_device_close (MbimDevice *self,
guint timeout,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
MbimMessage *request;
DeviceCloseContext *ctx;
g_return_if_fail (MBIM_IS_DEVICE (self));
ctx = g_slice_new (DeviceCloseContext);
ctx->self = g_object_ref (self);
ctx->result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
mbim_device_close);
ctx->timeout = timeout;
ctx->cancellable = (cancellable ? g_object_ref (cancellable) : NULL);
/* Already closed? */
if (!self->priv->iochannel) {
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
device_close_context_complete_and_free (ctx);
return;
}
/* If the device is in-session, avoid the close message */
if (self->priv->in_session) {
GError *error = NULL;
if (!destroy_iochannel (self, &error))
g_simple_async_result_take_error (ctx->result, error);
else
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
device_close_context_complete_and_free (ctx);
return;
}
/* Launch 'Close' command */
request = mbim_message_close_new (mbim_device_get_next_transaction_id (self));
mbim_device_command (self,
request,
10,
ctx->cancellable,
(GAsyncReadyCallback) close_message_ready,
ctx);
mbim_message_unref (request);
}
/*****************************************************************************/
/**
* mbim_device_get_next_transaction_id:
* @self: A #MbimDevice.
*
* Acquire the next transaction ID of this #MbimDevice.
* The internal transaction ID gets incremented.
*
* Returns: the next transaction ID.
*/
guint32
mbim_device_get_next_transaction_id (MbimDevice *self)
{
guint32 next;
g_return_val_if_fail (MBIM_IS_DEVICE (self), 0);
next = self->priv->transaction_id;
/* Don't go further than 8bits in the CTL service */
if (self->priv->transaction_id == G_MAXUINT32)
/* Reset! */
self->priv->transaction_id = 0x01;
else
self->priv->transaction_id++;
return next;
}
/*****************************************************************************/
static gboolean
device_write (MbimDevice *self,
const guint8 *data,
guint32 data_length,
GError **error)
{
gsize written;
GIOStatus write_status;
written = 0;
write_status = G_IO_STATUS_AGAIN;
while (write_status == G_IO_STATUS_AGAIN) {
write_status = g_io_channel_write_chars (self->priv->iochannel,
(gconstpointer)data,
(gssize)data_length,
&written,
error);
switch (write_status) {
case G_IO_STATUS_ERROR:
g_prefix_error (error, "Cannot write message: ");
return FALSE;
case G_IO_STATUS_EOF:
/* We shouldn't get EOF when writing */
g_assert_not_reached ();
break;
case G_IO_STATUS_NORMAL:
/* All good, we'll exit the loop now */
break;
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. TODO: in an idle? */
break;
}
}
return TRUE;
}
static gboolean
device_send (MbimDevice *self,
MbimMessage *message,
GError **error)
{
const guint8 *raw_message;
guint32 raw_message_len;
struct fragment_info *fragments;
guint n_fragments;
guint i;
raw_message = mbim_message_get_raw (message, &raw_message_len, NULL);
g_assert (raw_message);
if (mbim_utils_get_traces_enabled ()) {
gchar *printable;
printable = __mbim_utils_str_hex (raw_message, raw_message_len, ':');
g_debug ("[%s] Sent message...\n"
"<<<<<< RAW:\n"
"<<<<<< length = %u\n"
"<<<<<< data = %s\n",
self->priv->path_display,
((GByteArray *)message)->len,
printable);
g_free (printable);
printable = mbim_message_get_printable (message, "<<<<<< ", FALSE);
g_debug ("[%s] Sent message (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
}
/* Single fragment? Send it! */
if (raw_message_len <= MAX_CONTROL_TRANSFER)
return device_write (self, raw_message, raw_message_len, error);
/* The message to send must be able to handle fragments */
g_assert (_mbim_message_is_fragment (message));
fragments = _mbim_message_split_fragments (message,
MAX_CONTROL_TRANSFER,
&n_fragments);
for (i = 0; i < n_fragments; i++) {
if (mbim_utils_get_traces_enabled ()) {
GByteArray *bytearray;
gchar *printable;
gchar *printable_h;
gchar *printable_fh;
gchar *printable_d;
printable_h = __mbim_utils_str_hex (&fragments[i].header, sizeof (fragments[i].header), ':');
printable_fh = __mbim_utils_str_hex (&fragments[i].fragment_header, sizeof (fragments[i].fragment_header), ':');
printable_d = __mbim_utils_str_hex (fragments[i].data, fragments[i].data_length, ':');
g_debug ("[%s] Sent fragment (%u)...\n"
"<<<<<< RAW:\n"
"<<<<<< length = %u\n"
"<<<<<< data = %s%s%s\n",
self->priv->path_display, i,
(guint)(sizeof (fragments[i].header) +
sizeof (fragments[i].fragment_header) +
fragments[i].data_length),
printable_h, printable_fh, printable_d);
g_free (printable_h);
g_free (printable_fh);
g_free (printable_d);
/* Dummy message for printable purposes only */
bytearray = g_byte_array_new ();
g_byte_array_append (bytearray, (guint8 *)&fragments[i].header, sizeof (fragments[i].header));
g_byte_array_append (bytearray, (guint8 *)&fragments[i].fragment_header, sizeof (fragments[i].fragment_header));
printable = mbim_message_get_printable ((MbimMessage *)bytearray, "<<<<<< ", TRUE);
g_debug ("[%s] Sent fragment (translated)...\n%s",
self->priv->path_display,
printable);
g_free (printable);
g_byte_array_unref (bytearray);
}
/* Write fragment headers */
if (!device_write (self,
(guint8 *)&fragments[i].header,
sizeof (fragments[i].header),
error))
return FALSE;
if (!device_write (self,
(guint8 *)&fragments[i].fragment_header,
sizeof (fragments[i].fragment_header),
error))
return FALSE;
/* Write fragment data */
if (!device_write (self,
fragments[i].data,
fragments[i].data_length,
error))
return FALSE;
}
g_free (fragments);
return TRUE;
}
/*****************************************************************************/
/* Report error */
typedef struct {
MbimDevice *self;
MbimMessage *message;
} ReportErrorContext;
static void
device_report_error_context_free (ReportErrorContext *ctx)
{
mbim_message_unref (ctx->message);
g_object_unref (ctx->self);
g_slice_free (ReportErrorContext, ctx);
}
static gboolean
device_report_error_in_idle (ReportErrorContext *ctx)
{
/* Device must be open */
if (ctx->self->priv->iochannel) {
GError *error = NULL;
if (!device_send (ctx->self, ctx->message, &error)) {
g_warning ("[%s] Couldn't send host error message: %s",
ctx->self->priv->path_display,
error->message);
g_error_free (error);
}
}
device_report_error_context_free (ctx);
return FALSE;
}
static void
device_report_error (MbimDevice *self,
guint32 transaction_id,
const GError *error)
{
ReportErrorContext *ctx;
GSource *source;
/* Only protocol errors to be reported to the modem */
if (error->domain != MBIM_PROTOCOL_ERROR)
return;
ctx = g_slice_new (ReportErrorContext);
ctx->self = g_object_ref (self);
ctx->message = mbim_message_error_new (transaction_id, error->code);
source = g_idle_source_new ();
g_source_set_callback (source, (GSourceFunc)device_report_error_in_idle, ctx, NULL);
g_source_attach (source, g_main_context_get_thread_default ());
g_source_unref (source);
}
/*****************************************************************************/
/* Command */
/**
* mbim_device_command_finish:
* @self: a #MbimDevice.
* @res: a #GAsyncResult.
* @error: Return location for error or %NULL.
*
* Finishes an operation started with mbim_device_command().
*
* Returns: a #MbimMessage response, or #NULL if @error is set. The returned value should be freed with mbim_message_unref().
*/
MbimMessage *
mbim_device_command_finish (MbimDevice *self,
GAsyncResult *res,
GError **error)
{
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), error))
return NULL;
return mbim_message_ref (g_simple_async_result_get_op_res_gpointer (
G_SIMPLE_ASYNC_RESULT (res)));
}
/**
* mbim_device_command:
* @self: a #MbimDevice.
* @message: the message to send.
* @timeout: maximum time, in seconds, to wait for the response.
* @cancellable: a #GCancellable, or %NULL.
* @callback: a #GAsyncReadyCallback to call when the operation is finished.
* @user_data: the data to pass to callback function.
*
* Asynchronously sends a #MbimMessage to the device.
*
* When the operation is finished @callback will be called. You can then call
* mbim_device_command_finish() to get the result of the operation.
*/
void
mbim_device_command (MbimDevice *self,
MbimMessage *message,
guint timeout,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GError *error = NULL;
Transaction *tr;
guint32 transaction_id;
g_return_if_fail (MBIM_IS_DEVICE (self));
g_return_if_fail (message != NULL);
/* If the message comes without a explicit transaction ID, add one
* ourselves */
transaction_id = mbim_message_get_transaction_id (message);
if (!transaction_id) {
transaction_id = mbim_device_get_next_transaction_id (self);
mbim_message_set_transaction_id (message, transaction_id);
}
tr = transaction_new (self,
MBIM_MESSAGE_GET_MESSAGE_TYPE (message),
transaction_id,
cancellable,
callback,
user_data);
/* Device must be open */
if (!self->priv->iochannel) {
error = g_error_new (MBIM_CORE_ERROR,
MBIM_CORE_ERROR_WRONG_STATE,
"Device must be open to send commands");
transaction_complete_and_free (tr, error);
g_error_free (error);
return;
}
/* Setup context to match response */
if (!device_store_transaction (self, TRANSACTION_TYPE_HOST, tr, timeout * 1000, &error)) {
g_prefix_error (&error, "Cannot store transaction: ");
transaction_complete_and_free (tr, error);
g_error_free (error);
return;
}
if (!device_send (self, message, &error)) {
/* Match transaction so that we remove it from our tracking table */
tr = device_release_transaction (self,
TRANSACTION_TYPE_HOST,
MBIM_MESSAGE_GET_MESSAGE_TYPE (message),
mbim_message_get_transaction_id (message));
transaction_complete_and_free (tr, error);
g_error_free (error);
return;
}
/* Just return, we'll get response asynchronously */
}
/*****************************************************************************/
/* New MBIM device */
/**
* mbim_device_new_finish:
* @res: a #GAsyncResult.
* @error: Return location for error or %NULL.
*
* Finishes an operation started with mbim_device_new().
*
* Returns: A newly created #MbimDevice, or #NULL if @error is set.
*/
MbimDevice *
mbim_device_new_finish (GAsyncResult *res,
GError **error)
{
GObject *ret;
GObject *source_object;
source_object = g_async_result_get_source_object (res);
ret = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), res, error);
g_object_unref (source_object);
return (ret ? MBIM_DEVICE (ret) : NULL);
}
/**
* mbim_device_new:
* @file: a #GFile.
* @cancellable: optional #GCancellable object, #NULL to ignore.
* @callback: a #GAsyncReadyCallback to call when the initialization is finished.
* @user_data: the data to pass to callback function.
*
* Asynchronously creates a #MbimDevice object to manage @file.
* When the operation is finished, @callback will be invoked. You can then call
* mbim_device_new_finish() to get the result of the operation.
*/
void
mbim_device_new (GFile *file,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_async_initable_new_async (MBIM_TYPE_DEVICE,
G_PRIORITY_DEFAULT,
cancellable,
callback,
user_data,
MBIM_DEVICE_FILE, file,
NULL);
}
/*****************************************************************************/
/* Async init */
typedef struct {
MbimDevice *self;
GSimpleAsyncResult *result;
GCancellable *cancellable;
} InitContext;
static void
init_context_complete_and_free (InitContext *ctx)
{
g_simple_async_result_complete_in_idle (ctx->result);
if (ctx->cancellable)
g_object_unref (ctx->cancellable);
g_object_unref (ctx->result);
g_object_unref (ctx->self);
g_slice_free (InitContext, ctx);
}
static gboolean
initable_init_finish (GAsyncInitable *initable,
GAsyncResult *result,
GError **error)
{
return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error);
}
static void
query_info_async_ready (GFile *file,
GAsyncResult *res,
InitContext *ctx)
{
GError *error = NULL;
GFileInfo *info;
info = g_file_query_info_finish (file, res, &error);
if (!info) {
g_prefix_error (&error,
"Couldn't query file info: ");
g_simple_async_result_take_error (ctx->result, error);
init_context_complete_and_free (ctx);
return;
}
/* Our MBIM device must be of SPECIAL type */
if (g_file_info_get_file_type (info) != G_FILE_TYPE_SPECIAL) {
g_simple_async_result_set_error (ctx->result,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_FAILED,
"Wrong file type");
init_context_complete_and_free (ctx);
return;
}
g_object_unref (info);
/* Done we are */
g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE);
init_context_complete_and_free (ctx);
}
static void
initable_init_async (GAsyncInitable *initable,
int io_priority,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
InitContext *ctx;
ctx = g_slice_new0 (InitContext);
ctx->self = g_object_ref (initable);
if (cancellable)
ctx->cancellable = g_object_ref (cancellable);
ctx->result = g_simple_async_result_new (G_OBJECT (initable),
callback,
user_data,
initable_init_async);
/* We need a proper file to initialize */
if (!ctx->self->priv->file) {
g_simple_async_result_set_error (ctx->result,
MBIM_CORE_ERROR,
MBIM_CORE_ERROR_INVALID_ARGS,
"Cannot initialize MBIM device: No file given");
init_context_complete_and_free (ctx);
return;
}
/* Check the file type. Note that this is just a quick check to avoid
* creating MbimDevices pointing to a location already known not to be a MBIM
* device. */
g_file_query_info_async (ctx->self->priv->file,
G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NONE,
G_PRIORITY_DEFAULT,
ctx->cancellable,
(GAsyncReadyCallback)query_info_async_ready,
ctx);
}
/*****************************************************************************/
static void
set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
MbimDevice *self = MBIM_DEVICE (object);
switch (prop_id) {
case PROP_FILE:
g_assert (self->priv->file == NULL);
self->priv->file = g_value_dup_object (value);
self->priv->path = g_file_get_path (self->priv->file);
self->priv->path_display = g_filename_display_name (self->priv->path);
break;
case PROP_TRANSACTION_ID:
self->priv->transaction_id = g_value_get_uint (value);
break;
case PROP_IN_SESSION:
self->priv->in_session = 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)
{
MbimDevice *self = MBIM_DEVICE (object);
switch (prop_id) {
case PROP_FILE:
g_value_set_object (value, self->priv->file);
break;
case PROP_TRANSACTION_ID:
g_value_set_uint (value, self->priv->transaction_id);
break;
case PROP_IN_SESSION:
g_value_set_boolean (value, self->priv->in_session);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
mbim_device_init (MbimDevice *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE ((self),
MBIM_TYPE_DEVICE,
MbimDevicePrivate);
/* Initialize transaction ID */
self->priv->transaction_id = 0x01;
self->priv->open_status = OPEN_STATUS_CLOSED;
}
static void
dispose (GObject *object)
{
MbimDevice *self = MBIM_DEVICE (object);
g_clear_object (&self->priv->file);
destroy_iochannel (self, NULL);
G_OBJECT_CLASS (mbim_device_parent_class)->dispose (object);
}
static void
finalize (GObject *object)
{
MbimDevice *self = MBIM_DEVICE (object);
guint i;
/* Transactions keep refs to the device, so it's actually
* impossible to have any content in the HT */
for (i = 0; i < TRANSACTION_TYPE_LAST; i++) {
if (self->priv->transactions[i]) {
g_assert (g_hash_table_size (self->priv->transactions[i]) == 0);
g_hash_table_unref (self->priv->transactions[i]);
self->priv->transactions[i] = NULL;
}
}
g_free (self->priv->path);
g_free (self->priv->path_display);
G_OBJECT_CLASS (mbim_device_parent_class)->finalize (object);
}
static void
async_initable_iface_init (GAsyncInitableIface *iface)
{
iface->init_async = initable_init_async;
iface->init_finish = initable_init_finish;
}
static void
mbim_device_class_init (MbimDeviceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
g_type_class_add_private (object_class, sizeof (MbimDevicePrivate));
object_class->get_property = get_property;
object_class->set_property = set_property;
object_class->finalize = finalize;
object_class->dispose = dispose;
properties[PROP_FILE] =
g_param_spec_object (MBIM_DEVICE_FILE,
"Device file",
"File to the underlying MBIM device",
G_TYPE_FILE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
g_object_class_install_property (object_class, PROP_FILE, properties[PROP_FILE]);
properties[PROP_TRANSACTION_ID] =
g_param_spec_uint (MBIM_DEVICE_TRANSACTION_ID,
"Transaction ID",
"Current transaction ID",
0x01,
G_MAXUINT32,
0x01,
G_PARAM_READWRITE);
g_object_class_install_property (object_class, PROP_TRANSACTION_ID, properties[PROP_TRANSACTION_ID]);
properties[PROP_IN_SESSION] =
g_param_spec_boolean (MBIM_DEVICE_IN_SESSION,
"In session",
"Flag to specify if the device is within a session",
FALSE,
G_PARAM_READWRITE);
g_object_class_install_property (object_class, PROP_IN_SESSION, properties[PROP_IN_SESSION]);
/**
* MbimDevice::device-indicate-status:
* @self: the #MbimDevice
* @message: the #MbimMessage indication
*
* The ::device-indication-status signal is emitted when a MBIM indication is received.
*/
signals[SIGNAL_INDICATE_STATUS] =
g_signal_new (MBIM_DEVICE_SIGNAL_INDICATE_STATUS,
G_OBJECT_CLASS_TYPE (G_OBJECT_CLASS (klass)),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
MBIM_TYPE_MESSAGE);
/**
* MbimDevice::device-error:
* @self: the #MbimDevice
* @message: the #MbimMessage error
*
* The ::device-error signal is emitted when a MBIM error is received.
*/
signals[SIGNAL_ERROR] =
g_signal_new (MBIM_DEVICE_SIGNAL_ERROR,
G_OBJECT_CLASS_TYPE (G_OBJECT_CLASS (klass)),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_ERROR);
/**
* MbimDevice::device-removed:
* @self: the #MbimDevice
* @message: None
*
* The ::device-removed signal is emitted when an unexpected port hang-up is received.
*/
signals[SIGNAL_REMOVED] =
g_signal_new (MBIM_DEVICE_SIGNAL_REMOVED,
G_OBJECT_CLASS_TYPE (G_OBJECT_CLASS (klass)),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
}