| /* -*- 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 <http://www.gnu.org/licenses/>. |
| * |
| * Copyright (C) 2016 Zodiac Inflight Innovations |
| * Copyright (C) 2016-2017 Aleksander Morgado <aleksander@aleksander.es> |
| */ |
| |
| #include "config.h" |
| #include <stdlib.h> |
| #include <gio/gio.h> |
| |
| #if !defined WITH_UDEV |
| # error udev is required |
| #endif |
| |
| #include "qfu-helpers.h" |
| #include "qfu-helpers-udev.h" |
| #include <gudev/gudev.h> |
| |
| /******************************************************************************/ |
| |
| #if defined WITH_UDEV |
| |
| static const gchar *tty_subsys_list[] = { "tty", NULL }; |
| static const gchar *cdc_wdm_subsys_list[] = { "usbmisc", "usb", NULL }; |
| |
| static gboolean |
| get_device_details (GUdevDevice *device, |
| gchar **out_sysfs_path, |
| QfuHelpersDeviceMode *out_device_mode, |
| 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; |
| if (out_device_mode) |
| *out_device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN; |
| |
| /* 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_device_mode) { |
| aux = strtoul (g_udev_device_get_sysfs_attr (parent, "bNumInterfaces"), NULL, 10); |
| /* QDL download mode has exactly one single USB interface */ |
| if (aux == 1) |
| *out_device_mode = QFU_HELPERS_DEVICE_MODE_DOWNLOAD; |
| else if (aux > 1) |
| *out_device_mode = QFU_HELPERS_DEVICE_MODE_MODEM; |
| } |
| |
| 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_helpers_udev_find_by_file (GFile *file, |
| GError **error) |
| { |
| GUdevClient *client = NULL; |
| GUdevDevice *device = NULL; |
| gchar *basename = NULL; |
| const gchar **subsys_list = NULL; |
| gchar *sysfs_path = 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; |
| } |
| |
| if (!get_device_details (device, |
| &sysfs_path, NULL, NULL, NULL, NULL, NULL, |
| error)) |
| goto out; |
| |
| g_debug ("[qfu-udev] sysfs path for '%s' found: %s", basename, sysfs_path); |
| |
| out: |
| g_free (basename); |
| g_clear_object (&device); |
| g_clear_object (&client); |
| 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 (get_device_details (device, |
| &device_sysfs_path, |
| NULL, |
| &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_helpers_udev_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_with_free_func (g_free); |
| 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 (GUdevDevice *device, |
| QfuHelpersDeviceType type, |
| QfuHelpersDeviceMode mode, |
| const gchar *sysfs_path) |
| { |
| GFile *file = NULL; |
| gchar *device_sysfs_path = NULL; |
| gchar *device_driver = NULL; |
| gchar *device_path = NULL; |
| QfuHelpersDeviceMode device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN; |
| |
| if (!get_device_details (device, |
| &device_sysfs_path, &device_mode, 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 (device_mode != mode) |
| return NULL; |
| |
| if (!udev_helper_get_udev_interface_details (device, |
| &device_driver, |
| NULL)) |
| goto out; |
| |
| switch (type) { |
| case QFU_HELPERS_DEVICE_TYPE_TTY: |
| if (g_strcmp0 (device_driver, "qcserial") != 0) |
| goto out; |
| break; |
| case QFU_HELPERS_DEVICE_TYPE_CDC_WDM: |
| if (g_strcmp0 (device_driver, "qmi_wwan") != 0 && g_strcmp0 (device_driver, "cdc_mbim") != 0) |
| goto out; |
| break; |
| case QFU_HELPERS_DEVICE_TYPE_LAST: |
| 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_helpers_udev_list_devices (QfuHelpersDeviceType device_type, |
| QfuHelpersDeviceMode device_mode, |
| 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_HELPERS_DEVICE_TYPE_TTY: |
| subsys_list = tty_subsys_list; |
| break; |
| case QFU_HELPERS_DEVICE_TYPE_CDC_WDM: |
| subsys_list = cdc_wdm_subsys_list; |
| break; |
| case QFU_HELPERS_DEVICE_TYPE_LAST: |
| 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 (G_UDEV_DEVICE (iter->data), device_type, device_mode, sysfs_path); |
| 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 { |
| QfuHelpersDeviceType device_type; |
| QfuHelpersDeviceMode device_mode; |
| GUdevClient *udev; |
| gchar *sysfs_path; |
| gchar *peer_port; |
| 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_free (ctx->peer_port); |
| g_slice_free (WaitForDeviceContext, ctx); |
| } |
| |
| GFile * |
| qfu_helpers_udev_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); |
| |
| if (!g_str_equal (action, "add") && !g_str_equal (action, "move") && !g_str_equal (action, "change")) |
| return; |
| |
| file = device_matches (device, ctx->device_type, ctx->device_mode, ctx->sysfs_path); |
| if (!file && ctx->peer_port) { |
| gchar *tmp, *path; |
| |
| tmp = g_build_filename (ctx->peer_port, "device", NULL); |
| path = realpath (tmp, NULL); |
| g_free (tmp); |
| if (!path) |
| return; |
| |
| file = device_matches (device, ctx->device_type, ctx->device_mode, path); |
| g_debug ("[qfu-udev] peer lookup for %s: %s => %s", g_udev_device_get_name (device), ctx->peer_port, path); |
| g_free (path); |
| } |
| if (!file) |
| return; |
| |
| g_debug ("[qfu-udev] waiting device (%s) matched: %s", |
| qfu_helpers_device_type_to_string (ctx->device_type), |
| g_udev_device_get_name (device)); |
| |
| /* 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); |
| g_object_unref (task); |
| } |
| |
| 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_helpers_udev_wait_for_device (QfuHelpersDeviceType device_type, |
| QfuHelpersDeviceMode device_mode, |
| const gchar *sysfs_path, |
| const gchar *peer_port, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| GTask *task; |
| WaitForDeviceContext *ctx; |
| |
| ctx = g_slice_new0 (WaitForDeviceContext); |
| ctx->device_type = device_type; |
| ctx->device_mode = device_mode; |
| ctx->sysfs_path = g_strdup (sysfs_path); |
| ctx->peer_port = g_strdup (peer_port); |
| |
| if (ctx->device_type == QFU_HELPERS_DEVICE_TYPE_TTY) |
| ctx->udev = g_udev_client_new (tty_subsys_list); |
| else if (ctx->device_type == QFU_HELPERS_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 */ |
| } |
| |
| /******************************************************************************/ |
| |
| struct _QfuHelpersUdevGenericMonitor { |
| GUdevClient *udev; |
| }; |
| |
| void |
| qfu_helpers_udev_generic_monitor_free (QfuHelpersUdevGenericMonitor *self) |
| { |
| g_object_unref (self->udev); |
| g_slice_free (QfuHelpersUdevGenericMonitor, self); |
| } |
| |
| static void |
| handle_uevent_generic (GUdevClient *client, |
| const char *action, |
| GUdevDevice *device, |
| GTask *task) |
| { |
| g_debug ("[qfu-udev] event: %s %s", action, g_udev_device_get_name (device)); |
| } |
| |
| QfuHelpersUdevGenericMonitor * |
| qfu_helpers_udev_generic_monitor_new (const gchar *sysfs_path) |
| { |
| static const gchar *all_list[] = { |
| "usbmisc", "usb", |
| "tty", |
| "net", |
| NULL }; |
| |
| QfuHelpersUdevGenericMonitor *self; |
| |
| self = g_slice_new0 (QfuHelpersUdevGenericMonitor); |
| self->udev = g_udev_client_new (all_list); |
| |
| /* Monitor for device events. */ |
| g_signal_connect (self->udev, "uevent", G_CALLBACK (handle_uevent_generic), NULL); |
| return self; |
| } |
| |
| #endif /* WITH_UDEV */ |