blob: 0629ec8a0d901e8671e34890c37675482a98db27 [file] [log] [blame] [edit]
/* -*- 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) 2022 VMware, Inc.
*/
#include "config.h"
#include <stdlib.h>
#include <gio/gio.h>
#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 */
}