/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* qmi-firmware-update -- Command line tool to update firmware in QMI devices
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Copyright (C) 2016 Zodiac Inflight Innovations
* Copyright (C) 2016 Aleksander Morgado
*/
#include
#include
#include
#include "qfu-udev-helpers.h"
static const gchar *tty_subsys_list[] = { "tty", NULL };
static const gchar *cdc_wdm_subsys_list[] = { "usbmisc", "usb", NULL };
/******************************************************************************/
static const gchar *device_type_str[] = {
[QFU_UDEV_HELPER_DEVICE_TYPE_TTY] = "tty",
[QFU_UDEV_HELPER_DEVICE_TYPE_CDC_WDM] = "cdc-wdm",
};
G_STATIC_ASSERT (G_N_ELEMENTS (device_type_str) == QFU_UDEV_HELPER_DEVICE_TYPE_LAST);
const gchar *
qfu_udev_helper_device_type_to_string (QfuUdevHelperDeviceType type)
{
return device_type_str[type];
}
/******************************************************************************/
static GUdevDevice *
find_udev_device_for_file (GFile *file,
GError **error)
{
GUdevClient *client = NULL;
GUdevDevice *device = NULL;
gchar *basename = NULL;
const gchar **subsys_list = NULL;
guint i;
basename = g_file_get_basename (file);
if (!basename) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't get filename");
goto out;
}
client = g_udev_client_new (NULL);
if (g_str_has_prefix (basename, "tty"))
subsys_list = tty_subsys_list;
else if (g_str_has_prefix (basename, "cdc-wdm"))
subsys_list = cdc_wdm_subsys_list;
else {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unknown device file type");
goto out;
}
for (i = 0; !device && subsys_list[i]; i++)
device = g_udev_client_query_by_subsystem_and_name (client, subsys_list[i], basename);
if (!device) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "device not found");
goto out;
}
out:
g_free (basename);
if (client)
g_object_unref (client);
return device;
}
static gboolean
udev_helper_get_udev_device_details (GUdevDevice *device,
gchar **out_sysfs_path,
guint16 *out_vid,
guint16 *out_pid,
guint *out_busnum,
guint *out_devnum,
GError **error)
{
GUdevDevice *parent;
gulong aux;
if (out_vid)
*out_vid = 0;
if (out_pid)
*out_pid = 0;
if (out_sysfs_path)
*out_sysfs_path = NULL;
if (out_busnum)
*out_busnum = 0;
if (out_devnum)
*out_devnum = 0;
/* We need to look for the parent GUdevDevice which has a "usb_device"
* devtype. */
parent = g_udev_device_get_parent (device);
while (parent) {
GUdevDevice *next;
if (g_strcmp0 (g_udev_device_get_devtype (parent), "usb_device") == 0)
break;
/* Check next parent */
next = g_udev_device_get_parent (parent);
g_object_unref (parent);
parent = next;
}
if (!parent) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't find parent physical USB device");
return FALSE;
}
if (out_sysfs_path)
*out_sysfs_path = g_strdup (g_udev_device_get_sysfs_path (parent));
if (out_vid) {
aux = strtoul (g_udev_device_get_property (parent, "ID_VENDOR_ID"), NULL, 16);
if (aux <= G_MAXUINT16)
*out_vid = (guint16) aux;
}
if (out_pid) {
aux = strtoul (g_udev_device_get_property (parent, "ID_MODEL_ID"), NULL, 16);
if (aux <= G_MAXUINT16)
*out_pid = (guint16) aux;
}
if (out_busnum) {
aux = strtoul (g_udev_device_get_property (parent, "BUSNUM"), NULL, 10);
if (aux <= G_MAXUINT)
*out_busnum = (guint16) aux;
}
if (out_devnum) {
aux = strtoul (g_udev_device_get_property (parent, "DEVNUM"), NULL, 10);
if (aux <= G_MAXUINT)
*out_devnum = (guint16) aux;
}
g_object_unref (parent);
return TRUE;
}
static gboolean
udev_helper_get_udev_interface_details (GUdevDevice *device,
gchar **out_driver,
GError **error)
{
GUdevDevice *parent;
/* We need to look for the parent GUdevDevice which has a "usb_interface"
* devtype. */
parent = g_udev_device_get_parent (device);
while (parent) {
GUdevDevice *next;
if (g_strcmp0 (g_udev_device_get_devtype (parent), "usb_interface") == 0)
break;
/* Check next parent */
next = g_udev_device_get_parent (parent);
g_object_unref (parent);
parent = next;
}
if (!parent) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't find parent interface USB device");
return FALSE;
}
if (out_driver)
*out_driver = g_strdup (g_udev_device_get_driver (parent));
g_object_unref (parent);
return TRUE;
}
/******************************************************************************/
gchar *
qfu_udev_helper_find_by_file (GFile *file,
GError **error)
{
GUdevDevice *device;
gchar *sysfs_path = NULL;
device = find_udev_device_for_file (file, error);
if (device) {
udev_helper_get_udev_device_details (device,
&sysfs_path, NULL, NULL, NULL, NULL,
error);
g_object_unref (device);
}
return sysfs_path;
}
gchar *
qfu_udev_helper_find_by_file_path (const gchar *path,
GError **error)
{
GFile *file;
gchar *sysfs_path;
file = g_file_new_for_path (path);
sysfs_path = qfu_udev_helper_find_by_file (file, error);
g_object_unref (file);
return sysfs_path;
}
/******************************************************************************/
static gboolean
udev_helper_device_already_added (GPtrArray *ptr,
const gchar *sysfs_path)
{
guint i;
for (i = 0; i < ptr->len; i++) {
if (g_strcmp0 (g_ptr_array_index (ptr, i), sysfs_path) == 0)
return TRUE;
}
return FALSE;
}
static GPtrArray *
udev_helper_find_by_device_info_in_subsystem (GPtrArray *sysfs_paths,
GUdevClient *udev,
const gchar *subsystem,
guint16 vid,
guint16 pid,
guint busnum,
guint devnum)
{
GList *devices;
GList *iter;
devices = g_udev_client_query_by_subsystem (udev, subsystem);
for (iter = devices; iter; iter = g_list_next (iter)) {
GUdevDevice *device;
guint16 device_vid = 0;
guint16 device_pid = 0;
guint device_busnum = 0;
guint device_devnum = 0;
gchar *device_sysfs_path = NULL;
device = G_UDEV_DEVICE (iter->data);
if (udev_helper_get_udev_device_details (device,
&device_sysfs_path,
&device_vid,
&device_pid,
&device_busnum,
&device_devnum,
NULL)) {
if ((vid == 0 || vid == device_vid) &&
(pid == 0 || pid == device_pid) &&
(busnum == 0 || busnum == device_busnum) &&
(devnum == 0 || devnum == device_devnum) &&
(!udev_helper_device_already_added (sysfs_paths, device_sysfs_path)))
g_ptr_array_add (sysfs_paths, device_sysfs_path);
else
g_free (device_sysfs_path);
}
g_object_unref (device);
}
g_list_free (devices);
return sysfs_paths;
}
gchar *
qfu_udev_helper_find_by_device_info (guint16 vid,
guint16 pid,
guint busnum,
guint devnum,
GError **error)
{
GUdevClient *udev;
guint i;
GPtrArray *sysfs_paths;
GString *match_str;
gchar *sysfs_path = NULL;
sysfs_paths = g_ptr_array_new ();
udev = g_udev_client_new (NULL);
match_str = g_string_new ("");
if (vid != 0)
g_string_append_printf (match_str, "vid 0x%04x", vid);
if (pid != 0)
g_string_append_printf (match_str, "%spid 0x%04x", match_str->len > 0 ? ", " : "", pid);
if (busnum != 0)
g_string_append_printf (match_str, "%sbus %03u", match_str->len > 0 ? ", " : "", busnum);
if (devnum != 0)
g_string_append_printf (match_str, "%sdev %03u", match_str->len > 0 ? ", " : "", devnum);
g_assert (match_str->len > 0);
for (i = 0; tty_subsys_list[i]; i++)
sysfs_paths = udev_helper_find_by_device_info_in_subsystem (sysfs_paths,
udev,
tty_subsys_list[i],
vid, pid, busnum, devnum);
for (i = 0; cdc_wdm_subsys_list[i]; i++)
sysfs_paths = udev_helper_find_by_device_info_in_subsystem (sysfs_paths,
udev,
cdc_wdm_subsys_list[i],
vid, pid, busnum, devnum);
for (i = 0; i < sysfs_paths->len; i++)
g_debug ("[%s] sysfs path: %s", match_str->str, (gchar *) g_ptr_array_index (sysfs_paths, i));
if (sysfs_paths->len == 0) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"no device found with matching criteria: %s",
match_str->str);
goto out;
}
if (sysfs_paths->len > 1) {
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"multiple devices (%u) found with matching criteria: %s",
sysfs_paths->len, match_str->str);
goto out;
}
sysfs_path = g_strdup (g_ptr_array_index (sysfs_paths, 0));
out:
g_ptr_array_unref (sysfs_paths);
g_string_free (match_str, TRUE);
g_object_unref (udev);
return sysfs_path;
}
/******************************************************************************/
static GFile *
device_matches_sysfs_and_type (GUdevDevice *device,
const gchar *sysfs_path,
QfuUdevHelperDeviceType type)
{
GFile *file = NULL;
gchar *device_sysfs_path = NULL;
gchar *device_driver = NULL;
gchar *device_path = NULL;
if (!udev_helper_get_udev_device_details (device,
&device_sysfs_path, NULL, NULL, NULL, NULL,
NULL))
goto out;
if (!device_sysfs_path)
goto out;
if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0)
goto out;
if (!udev_helper_get_udev_interface_details (device,
&device_driver,
NULL))
goto out;
switch (type) {
case QFU_UDEV_HELPER_DEVICE_TYPE_TTY:
if (g_strcmp0 (device_driver, "qcserial") != 0)
goto out;
break;
case QFU_UDEV_HELPER_DEVICE_TYPE_CDC_WDM:
if (g_strcmp0 (device_driver, "qmi_wwan") != 0 && g_strcmp0 (device_driver, "cdc_mbim") != 0)
goto out;
break;
default:
g_assert_not_reached ();
}
device_path = g_strdup_printf ("/dev/%s", g_udev_device_get_name (device));
file = g_file_new_for_path (device_path);
g_free (device_path);
out:
g_free (device_sysfs_path);
g_free (device_driver);
return file;
}
GList *
qfu_udev_helper_list_devices (QfuUdevHelperDeviceType device_type,
const gchar *sysfs_path)
{
GUdevClient *udev;
const gchar **subsys_list = NULL;
guint i;
GList *files = NULL;
udev = g_udev_client_new (NULL);
switch (device_type) {
case QFU_UDEV_HELPER_DEVICE_TYPE_TTY:
subsys_list = tty_subsys_list;
break;
case QFU_UDEV_HELPER_DEVICE_TYPE_CDC_WDM:
subsys_list = cdc_wdm_subsys_list;
break;
default:
g_assert_not_reached ();
}
for (i = 0; subsys_list[i]; i++) {
GList *devices, *iter;
devices = g_udev_client_query_by_subsystem (udev, subsys_list[i]);
for (iter = devices; iter; iter = g_list_next (iter)) {
GFile *file;
file = device_matches_sysfs_and_type (G_UDEV_DEVICE (iter->data), sysfs_path, device_type);
if (file)
files = g_list_prepend (files, file);
g_object_unref (G_OBJECT (iter->data));
}
g_list_free (devices);
}
g_object_unref (udev);
return files;
}
/******************************************************************************/
#define WAIT_FOR_DEVICE_TIMEOUT_SECS 120
typedef struct {
QfuUdevHelperDeviceType device_type;
GUdevClient *udev;
gchar *sysfs_path;
guint timeout_id;
gulong uevent_id;
gulong cancellable_id;
} WaitForDeviceContext;
static void
wait_for_device_context_free (WaitForDeviceContext *ctx)
{
g_assert (!ctx->timeout_id);
g_assert (!ctx->uevent_id);
g_assert (!ctx->cancellable_id);
g_object_unref (ctx->udev);
g_free (ctx->sysfs_path);
g_slice_free (WaitForDeviceContext, ctx);
}
GFile *
qfu_udev_helper_wait_for_device_finish (GAsyncResult *res,
GError **error)
{
return G_FILE (g_task_propagate_pointer (G_TASK (res), error));
}
static void
handle_uevent (GUdevClient *client,
const char *action,
GUdevDevice *device,
GTask *task)
{
WaitForDeviceContext *ctx;
GFile *file;
ctx = (WaitForDeviceContext *) g_task_get_task_data (task);
g_debug ("[qfu-udev] event: %s %s", action, g_udev_device_get_name (device));
if (!g_str_equal (action, "add") && !g_str_equal (action, "move") && !g_str_equal (action, "change"))
return;
file = device_matches_sysfs_and_type (device, ctx->sysfs_path, ctx->device_type);
if (!file)
return;
g_debug ("[qfu-udev] waiting device matched");
/* Disconnect this handler */
g_signal_handler_disconnect (ctx->udev, ctx->uevent_id);
ctx->uevent_id = 0;
/* Disconnect the other handlers */
g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
ctx->cancellable_id = 0;
g_source_remove (ctx->timeout_id);
ctx->timeout_id = 0;
g_task_return_pointer (task, file, g_object_unref);
}
static gboolean
wait_for_device_timed_out (GTask *task)
{
WaitForDeviceContext *ctx;
ctx = (WaitForDeviceContext *) g_task_get_task_data (task);
/* Disconnect this handler */
ctx->timeout_id = 0;
/* Disconnect the other handlers */
g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
ctx->cancellable_id = 0;
g_signal_handler_disconnect (ctx->udev, ctx->uevent_id);
ctx->uevent_id = 0;
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
"waiting for device at '%s' timed out",
ctx->sysfs_path);
g_object_unref (task);
return FALSE;
}
static void
wait_for_device_cancelled (GCancellable *cancellable,
GTask *task)
{
WaitForDeviceContext *ctx;
ctx = (WaitForDeviceContext *) g_task_get_task_data (task);
/* Disconnect this handler */
g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id);
ctx->cancellable_id = 0;
/* Disconnect the other handlers */
g_source_remove (ctx->timeout_id);
ctx->timeout_id = 0;
g_signal_handler_disconnect (ctx->udev, ctx->uevent_id);
ctx->uevent_id = 0;
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED,
"waiting for device at '%s' cancelled",
ctx->sysfs_path);
g_object_unref (task);
}
void
qfu_udev_helper_wait_for_device (QfuUdevHelperDeviceType device_type,
const gchar *sysfs_path,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GTask *task;
WaitForDeviceContext *ctx;
ctx = g_slice_new0 (WaitForDeviceContext);
ctx->device_type = device_type;
ctx->sysfs_path = g_strdup (sysfs_path);
if (ctx->device_type == QFU_UDEV_HELPER_DEVICE_TYPE_TTY)
ctx->udev = g_udev_client_new (tty_subsys_list);
else if (ctx->device_type == QFU_UDEV_HELPER_DEVICE_TYPE_CDC_WDM)
ctx->udev = g_udev_client_new (cdc_wdm_subsys_list);
else
g_assert_not_reached ();
task = g_task_new (NULL, cancellable, callback, user_data);
g_task_set_task_data (task, ctx, (GDestroyNotify) wait_for_device_context_free);
/* Monitor for device additions. */
ctx->uevent_id = g_signal_connect (ctx->udev,
"uevent",
G_CALLBACK (handle_uevent),
task);
/* Allow cancellation */
ctx->cancellable_id = g_cancellable_connect (cancellable,
(GCallback) wait_for_device_cancelled,
task,
NULL);
/* And also, setup a timeout to avoid waiting forever. */
ctx->timeout_id = g_timeout_add_seconds (WAIT_FOR_DEVICE_TIMEOUT_SECS,
(GSourceFunc) wait_for_device_timed_out,
task);
/* Note: task ownership is shared between the signals and the timeout */
}