summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAleksander Morgado <aleksander@aleksander.es>2022-03-09 16:05:46 +0100
committerAleksander Morgado <aleksander@aleksander.es>2022-03-09 23:09:48 +0100
commit20a9420b7617dcbbdce193d244d13b1585bc5282 (patch)
tree8e616d816bebdc7444f32518eb5869521ebd8c56 /src
parent1077691ac1eb6e192725885e88ab1c7fd43e3044 (diff)
downloadlibqmi-20a9420b7617dcbbdce193d244d13b1585bc5282.tar.gz
qmi-firmware-update: implement filesystem helpers support using sysfs
For systems without udev, like openwrt. Fixes https://gitlab.freedesktop.org/mobile-broadband/libqmi/-/issues/18
Diffstat (limited to 'src')
-rw-r--r--src/qmi-firmware-update/qfu-helpers-sysfs.c593
1 files changed, 584 insertions, 9 deletions
diff --git a/src/qmi-firmware-update/qfu-helpers-sysfs.c b/src/qmi-firmware-update/qfu-helpers-sysfs.c
index ecfd12ca..6e8b9118 100644
--- a/src/qmi-firmware-update/qfu-helpers-sysfs.c
+++ b/src/qmi-firmware-update/qfu-helpers-sysfs.c
@@ -30,12 +30,300 @@
#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,
+ 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;
+
+ 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_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)
{
- g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "not implemented");
- return NULL;
+ 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,
+ 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,
+ &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 *
@@ -45,26 +333,295 @@ qfu_helpers_sysfs_find_by_device_info (guint16 vid,
guint devnum,
GError **error)
{
- g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "not implemented");
- return NULL;
+ 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_sysfs_and_type (GFile *file,
+ const gchar *sysfs_path,
+ QfuHelpersDeviceType type)
+{
+ 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;
+
+ port_sysfs_path = g_file_get_path (file);
+ if (!get_device_details (port_sysfs_path,
+ &device_sysfs_path, 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 (!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,
const gchar *sysfs_path)
{
- return NULL;
+ 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_sysfs_and_type (child, sysfs_path, device_type);
+ 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;
+ 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_task_propagate_pointer (G_TASK (res), error);
+ return G_FILE (g_task_propagate_pointer (G_TASK (res), error));
+}
+
+static GFile *
+wait_for_device_lookup (QfuHelpersDeviceType device_type,
+ const gchar *sysfs_path,
+ const gchar *peer_port)
+{
+ GList *devices;
+ GFile *file;
+
+ devices = qfu_helpers_sysfs_list_devices (device_type, 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, 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->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
@@ -75,9 +632,27 @@ qfu_helpers_sysfs_wait_for_device (QfuHelpersDeviceType device_type,
GAsyncReadyCallback callback,
gpointer user_data)
{
- GTask *task;
+ GTask *task;
+ WaitForDeviceContext *ctx;
+
+ ctx = g_slice_new0 (WaitForDeviceContext);
+ ctx->device_type = device_type;
+ 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_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "not implemented");
- g_object_unref (task);
+ 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 */
}