/* -*- 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) 2022 VMware, Inc. */ #include "config.h" #include #include #if defined WITH_UDEV # error udev found #endif #include "qfu-helpers.h" #include "qfu-helpers-sysfs.h" /******************************************************************************/ static const gchar *tty_subsys_list[] = { "tty", NULL }; static const gchar *cdc_wdm_subsys_list[] = { "usbmisc", "usb", NULL }; static gboolean has_sysfs_attribute (const gchar *path, const gchar *attribute) { g_autofree gchar *aux_filepath = NULL; aux_filepath = g_strdup_printf ("%s/%s", path, attribute); return g_file_test (aux_filepath, G_FILE_TEST_EXISTS); } static gchar * read_sysfs_attribute_as_string (const gchar *path, const gchar *attribute) { g_autofree gchar *aux = NULL; gchar *contents = NULL; aux = g_strdup_printf ("%s/%s", path, attribute); if (g_file_get_contents (aux, &contents, NULL, NULL)) { g_strdelimit (contents, "\r\n", ' '); g_strstrip (contents); } return contents; } static gulong read_sysfs_attribute_as_num (const gchar *path, const gchar *attribute, guint base) { g_autofree gchar *contents = NULL; gulong val = 0; contents = read_sysfs_attribute_as_string (path, attribute); if (contents) val = strtoul (contents, NULL, base); return val; } static gchar * read_sysfs_attribute_link_basename (const gchar *path, const gchar *attribute) { g_autofree gchar *aux_filepath = NULL; g_autofree gchar *canonicalized_path = NULL; aux_filepath = g_strdup_printf ("%s/%s", path, attribute); if (!g_file_test (aux_filepath, G_FILE_TEST_EXISTS)) return NULL; canonicalized_path = realpath (aux_filepath, NULL); return g_path_get_basename (canonicalized_path); } static gboolean get_device_details (const gchar *port_sysfs_path, gchar **out_sysfs_path, QfuHelpersDeviceMode *out_device_mode, guint16 *out_vid, guint16 *out_pid, guint *out_busnum, guint *out_devnum, GError **error) { g_autofree gchar *iter = NULL; g_autofree gchar *physdev_sysfs_path = NULL; gulong aux; if (out_vid) *out_vid = 0; if (out_pid) *out_pid = 0; if (out_busnum) *out_busnum = 0; if (out_devnum) *out_devnum = 0; if (out_sysfs_path) *out_sysfs_path = NULL; if (out_device_mode) *out_device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN; iter = realpath (port_sysfs_path, NULL); while (iter && (g_strcmp0 (iter, "/") != 0)) { gchar *parent; /* is this the USB physdev? */ if (has_sysfs_attribute (iter, "idVendor")) { /* stop traversing as soon as the physical device is found */ physdev_sysfs_path = g_steal_pointer (&iter); break; } parent = g_path_get_dirname (iter); g_clear_pointer (&iter, g_free); iter = parent; } if (!physdev_sysfs_path) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "couldn't find parent physical USB device"); return FALSE; } if (out_device_mode) { aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "bNumInterfaces", 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 = read_sysfs_attribute_as_num (physdev_sysfs_path, "idVendor", 16); if (aux <= G_MAXUINT16) *out_vid = (guint16) aux; } if (out_pid) { aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "idProduct", 16); if (aux <= G_MAXUINT16) *out_pid = (guint16) aux; } if (out_busnum) { aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "busnum", 10); if (aux <= G_MAXUINT) *out_busnum = (guint16) aux; } if (out_devnum) { aux = read_sysfs_attribute_as_num (physdev_sysfs_path, "devnum", 10); if (aux <= G_MAXUINT) *out_devnum = (guint16) aux; } if (out_sysfs_path) *out_sysfs_path = g_steal_pointer (&physdev_sysfs_path); return TRUE; } static gboolean get_interface_details (const gchar *port_sysfs_path, gchar **out_driver, GError **error) { g_autofree gchar *iter = NULL; g_autofree gchar *interface_sysfs_path = NULL; if (out_driver) *out_driver = NULL; iter = realpath (port_sysfs_path, NULL); while (iter && (g_strcmp0 (iter, "/") != 0)) { gchar *parent; /* is this the USB interface? */ if (has_sysfs_attribute (iter, "bInterfaceClass")) { /* stop traversing as soon as the physical device is found */ interface_sysfs_path = g_steal_pointer (&iter); break; } parent = g_path_get_dirname (iter); g_clear_pointer (&iter, g_free); iter = parent; } if (!interface_sysfs_path) { 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 = read_sysfs_attribute_link_basename (interface_sysfs_path, "driver"); return TRUE; } /******************************************************************************/ gchar * qfu_helpers_sysfs_find_by_file (GFile *file, GError **error) { const gchar **subsys_list = NULL; g_autofree gchar *physdev_sysfs_path = NULL; g_autofree gchar *found_port_sysfs_path = NULL; g_autofree gchar *basename = 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"); return 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"); return NULL; } for (i = 0; !found_port_sysfs_path && subsys_list[i]; i++) { g_autofree gchar *tmp = NULL; g_autofree gchar *port_sysfs_path = NULL; tmp = g_strdup_printf ("/sys/class/%s/%s", subsys_list[i], basename); port_sysfs_path = realpath (tmp, NULL); if (port_sysfs_path && g_file_test (port_sysfs_path, G_FILE_TEST_EXISTS)) found_port_sysfs_path = g_steal_pointer (&port_sysfs_path); } if (!found_port_sysfs_path) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "device not found"); return NULL; } if (!get_device_details (found_port_sysfs_path, &physdev_sysfs_path, NULL, NULL, NULL, NULL, NULL, error)) return NULL; g_debug ("[qfu-sysfs] sysfs path for '%s' found: %s", basename, physdev_sysfs_path); return g_steal_pointer (&physdev_sysfs_path); } /******************************************************************************/ static gboolean 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 * find_by_device_info_in_subsystem (GPtrArray *sysfs_paths, const gchar *subsystem, guint16 vid, guint16 pid, guint busnum, guint devnum) { g_autofree gchar *subsys_sysfs_path = NULL; g_autoptr(GFile) subsys_sysfs_file = NULL; g_autoptr(GFileEnumerator) direnum = NULL; subsys_sysfs_path = g_strdup_printf ("/sys/class/%s", subsystem); subsys_sysfs_file = g_file_new_for_path (subsys_sysfs_path); direnum = g_file_enumerate_children (subsys_sysfs_file, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (direnum) { while (TRUE) { GFileInfo *info = NULL; g_autoptr(GFile) child = NULL; g_autofree gchar *child_path = NULL; guint16 device_vid = 0; guint16 device_pid = 0; guint device_busnum = 0; guint device_devnum = 0; g_autofree gchar *device_sysfs_path = NULL; if (!g_file_enumerator_iterate (direnum, &info, NULL, NULL, NULL) || !info) break; child = g_file_enumerator_get_child (direnum, info); child_path = g_file_get_path (child); if (get_device_details (child_path, &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) && (!device_already_added (sysfs_paths, device_sysfs_path))) g_ptr_array_add (sysfs_paths, g_steal_pointer (&device_sysfs_path)); } } } return sysfs_paths; } gchar * qfu_helpers_sysfs_find_by_device_info (guint16 vid, guint16 pid, guint busnum, guint devnum, GError **error) { g_autoptr(GPtrArray) sysfs_paths = NULL; g_autoptr(GString) match_str = NULL; guint i; sysfs_paths = g_ptr_array_new_with_free_func (g_free); 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 = find_by_device_info_in_subsystem (sysfs_paths, tty_subsys_list[i], vid, pid, busnum, devnum); for (i = 0; cdc_wdm_subsys_list[i]; i++) sysfs_paths = find_by_device_info_in_subsystem (sysfs_paths, 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); return NULL; } 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); return NULL; } return g_strdup (g_ptr_array_index (sysfs_paths, 0)); } /******************************************************************************/ static GFile * device_matches (GFile *file, QfuHelpersDeviceType type, QfuHelpersDeviceMode mode, const gchar *sysfs_path) { g_autofree gchar *port_sysfs_path = NULL; g_autofree gchar *device_sysfs_path = NULL; g_autofree gchar *device_driver = NULL; g_autofree gchar *device_path = NULL; QfuHelpersDeviceMode device_mode = QFU_HELPERS_DEVICE_MODE_UNKNOWN; port_sysfs_path = g_file_get_path (file); if (!get_device_details (port_sysfs_path, &device_sysfs_path, &device_mode, NULL, NULL, NULL, NULL, NULL)) return NULL; if (!device_sysfs_path) return NULL; if (g_strcmp0 (device_sysfs_path, sysfs_path) != 0) return NULL; if (device_mode != mode) return NULL; if (!get_interface_details (port_sysfs_path, &device_driver, NULL)) return NULL; switch (type) { case QFU_HELPERS_DEVICE_TYPE_TTY: if (g_strcmp0 (device_driver, "qcserial") != 0) return NULL; break; case QFU_HELPERS_DEVICE_TYPE_CDC_WDM: if (g_strcmp0 (device_driver, "qmi_wwan") != 0 && g_strcmp0 (device_driver, "cdc_mbim") != 0) return NULL; break; case QFU_HELPERS_DEVICE_TYPE_LAST: default: g_assert_not_reached (); } device_path = g_strdup_printf ("/dev/%s", g_file_get_basename (file)); return g_file_new_for_path (device_path); } GList * qfu_helpers_sysfs_list_devices (QfuHelpersDeviceType device_type, QfuHelpersDeviceMode device_mode, const gchar *sysfs_path) { const gchar **subsys_list = NULL; guint i; GList *files = 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++) { g_autofree gchar *subsys_sysfs_path = NULL; g_autoptr(GFile) subsys_sysfs_file = NULL; g_autoptr(GFileEnumerator) direnum = NULL; subsys_sysfs_path = g_strdup_printf ("/sys/class/%s", subsys_list[i]); subsys_sysfs_file = g_file_new_for_path (subsys_sysfs_path); direnum = g_file_enumerate_children (subsys_sysfs_file, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (!direnum) continue; while (TRUE) { GFileInfo *info = NULL; g_autoptr(GFile) child = NULL; g_autoptr(GFile) devfile = NULL; if (!g_file_enumerator_iterate (direnum, &info, NULL, NULL, NULL) || !info) break; child = g_file_enumerator_get_child (direnum, info); devfile = device_matches (child, device_type, device_mode, sysfs_path); if (devfile) files = g_list_prepend (files, g_steal_pointer (&devfile)); } } return files; } /******************************************************************************/ /* Check for the new port addition every 10s */ #define WAIT_FOR_DEVICE_CHECK_SECS 10 /* And up to 12 attempts to check (so 120s in total) */ #define WAIT_FOR_DEVICE_CHECK_ATTEMPTS 12 typedef struct { QfuHelpersDeviceType device_type; QfuHelpersDeviceMode device_mode; gchar *sysfs_path; gchar *peer_port; guint check_attempts; guint timeout_id; gulong cancellable_id; } WaitForDeviceContext; static void wait_for_device_context_free (WaitForDeviceContext *ctx) { g_assert (!ctx->timeout_id); g_assert (!ctx->cancellable_id); g_free (ctx->sysfs_path); g_free (ctx->peer_port); g_slice_free (WaitForDeviceContext, ctx); } GFile * qfu_helpers_sysfs_wait_for_device_finish (GAsyncResult *res, GError **error) { return G_FILE (g_task_propagate_pointer (G_TASK (res), error)); } static GFile * wait_for_device_lookup (QfuHelpersDeviceType device_type, QfuHelpersDeviceMode device_mode, const gchar *sysfs_path, const gchar *peer_port) { GList *devices; GFile *file; devices = qfu_helpers_sysfs_list_devices (device_type, device_mode, sysfs_path); if (!devices) { g_autofree gchar *tmp = NULL; g_autofree gchar *path = NULL; if (!peer_port) return NULL; /* Check with peer port */ tmp = g_build_filename (peer_port, "device", NULL); path = realpath (tmp, NULL); if (path) { g_debug ("[qfu-sysfs] peer lookup: %s => %s", peer_port, path); devices = qfu_helpers_sysfs_list_devices (device_type, device_mode, path); if (!devices) return NULL; } } if (g_list_length (devices) > 1) g_warning ("[qfu-sysfs] waiting device (%s) matched multiple times", qfu_helpers_device_type_to_string (device_type)); /* Take the first one from the list */ file = G_FILE (g_object_ref (devices->data)); g_list_free_full (devices, g_object_unref); return file; } static gboolean wait_for_device_check (GTask *task) { WaitForDeviceContext *ctx; g_autoptr(GFile) device = NULL; g_autofree gchar *device_name = NULL; ctx = (WaitForDeviceContext *) g_task_get_task_data (task); /* Check devices matching */ device = wait_for_device_lookup (ctx->device_type, ctx->device_mode, ctx->sysfs_path, ctx->peer_port); if (!device) { /* No devices found */ if (ctx->check_attempts == WAIT_FOR_DEVICE_CHECK_ATTEMPTS) { /* 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_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 G_SOURCE_REMOVE; } /* go on with next attempt */ ctx->check_attempts++; return G_SOURCE_CONTINUE; } device_name = g_file_get_basename (device); g_debug ("[qfu-sysfs] waiting device (%s) matched: %s", qfu_helpers_device_type_to_string (ctx->device_type), device_name); /* Disconnect the other handlers */ g_cancellable_disconnect (g_task_get_cancellable (task), ctx->cancellable_id); ctx->cancellable_id = 0; /* Disconnect this handler */ ctx->timeout_id = 0; g_task_return_pointer (task, g_steal_pointer (&device), g_object_unref); g_object_unref (task); return G_SOURCE_REMOVE; } 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_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_sysfs_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); task = g_task_new (NULL, cancellable, callback, user_data); g_task_set_task_data (task, ctx, (GDestroyNotify) wait_for_device_context_free); /* Allow cancellation */ ctx->cancellable_id = g_cancellable_connect (cancellable, (GCallback) wait_for_device_cancelled, task, NULL); /* Schedule lookup of of port every once in a while */ ctx->timeout_id = g_timeout_add_seconds (WAIT_FOR_DEVICE_CHECK_SECS, (GSourceFunc) wait_for_device_check, task); /* Note: task ownership is shared between the signals and the timeout */ }