blob: 1d59b10edfa80a266d9743cf4999f1a5f92825f0 [file] [log] [blame]
/*
* Copyright 2011 Richard Hughes <richard@hughsie.com>
* Copyright 2011 Hans de Goede <hdegoede@redhat.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#define G_LOG_DOMAIN "FuBackend"
#include "config.h"
#include <fwupdplugin.h>
#include <libusb.h>
#include "fu-context-private.h"
#include "fu-usb-backend.h"
#ifndef HAVE_UDEV
#include "fu-usb-device-private.h"
#endif
struct _FuUsbBackend {
FuBackend parent_instance;
libusb_context *ctx;
#ifndef HAVE_UDEV
libusb_hotplug_callback_handle hotplug_id;
GThread *thread_event;
volatile gint thread_event_run;
GPtrArray *idle_events;
GMutex idle_events_mutex;
guint idle_events_id;
guint hotplug_poll_id;
guint hotplug_poll_interval;
#endif
};
G_DEFINE_TYPE(FuUsbBackend, fu_usb_backend, FU_TYPE_BACKEND)
#define FU_USB_BACKEND_POLL_INTERVAL_DEFAULT 1000 /* ms */
#define FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG 5 /* ms */
#ifndef HAVE_UDEV
static gchar *
fu_usb_backend_get_usb_device_backend_id(libusb_device *usb_device)
{
return g_strdup_printf("%02x:%02x",
libusb_get_bus_number(usb_device),
libusb_get_device_address(usb_device));
}
static FuUsbDevice *
fu_usb_backend_create_device(FuUsbBackend *self, libusb_device *usb_device)
{
g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device);
return g_object_new(FU_TYPE_USB_DEVICE,
"backend",
FU_BACKEND(self),
"backend-id",
backend_id,
"libusb-device",
usb_device,
NULL);
}
#ifndef HAVE_UDEV
static FuDevice *
fu_usb_backend_create_device_impl(FuBackend *backend, const gchar *backend_id, GError **error)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
FuContext *ctx = fu_backend_get_context(backend);
guint64 usb_addr = 0;
guint64 usb_bus = 0;
libusb_device **dev_list = NULL;
g_auto(GStrv) bus_addr = NULL;
g_autoptr(FuUsbDevice) usb_device = NULL;
/* back from bus:addr */
bus_addr = g_strsplit(backend_id, ":", 2);
if (!fu_strtoull(bus_addr[0], &usb_bus, 0x0, G_MAXUINT8, FU_INTEGER_BASE_16, error)) {
g_prefix_error(error, "failed to parse bus from %s: ", backend_id);
return NULL;
}
if (!fu_strtoull(bus_addr[1], &usb_addr, 0x0, G_MAXUINT8, FU_INTEGER_BASE_16, error)) {
g_prefix_error(error, "failed to parse addr from %s: ", backend_id);
return NULL;
}
/* find in the current device list */
libusb_get_device_list(self->ctx, &dev_list);
for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++) {
if (libusb_get_bus_number(dev_list[i]) == usb_bus &&
libusb_get_device_address(dev_list[i]) == usb_addr) {
usb_device = fu_usb_device_new(ctx, dev_list[i]);
break;
}
}
libusb_free_device_list(dev_list, 1);
/* did not find */
if (usb_device == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_FOUND,
"%s was not found",
backend_id);
return NULL;
}
/* success */
return FU_DEVICE(g_steal_pointer(&usb_device));
}
#endif
static void
fu_usb_backend_add_device(FuUsbBackend *self, libusb_device *usb_device)
{
FuDevice *device_old;
g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device);
g_autoptr(FuUsbDevice) device = NULL;
device_old = fu_backend_lookup_by_id(FU_BACKEND(self), backend_id);
if (device_old != NULL)
return;
device = fu_usb_backend_create_device(self, usb_device);
fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device));
}
static void
fu_usb_backend_remove_device(FuUsbBackend *self, libusb_device *usb_device)
{
g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device);
FuDevice *device = fu_backend_lookup_by_id(FU_BACKEND(self), backend_id);
if (device != NULL)
fu_backend_device_removed(FU_BACKEND(self), device);
}
static void
fu_usb_backend_rescan(FuUsbBackend *self)
{
libusb_device **dev_list = NULL;
g_autoptr(GList) existing_devices = NULL;
g_autoptr(GPtrArray) devices = fu_backend_get_devices(FU_BACKEND(self));
/* skip actual enumeration */
if (g_getenv("FWUPD_SELF_TEST") != NULL)
return;
/* copy to a context so we can remove from the array */
for (guint i = 0; i < devices->len; i++) {
FuUsbDevice *device = g_ptr_array_index(devices, i);
existing_devices = g_list_prepend(existing_devices, device);
}
/* look for any removed devices */
libusb_get_device_list(self->ctx, &dev_list);
for (GList *l = existing_devices; l != NULL; l = l->next) {
FuUsbDevice *device = FU_USB_DEVICE(l->data);
gboolean found = FALSE;
for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++) {
if (libusb_get_bus_number(dev_list[i]) == fu_usb_device_get_bus(device) &&
libusb_get_device_address(dev_list[i]) ==
fu_usb_device_get_address(device)) {
found = TRUE;
break;
}
}
if (!found)
fu_backend_device_removed(FU_BACKEND(self), FU_DEVICE(device));
}
/* add any devices not yet added (duplicates will be filtered */
for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++)
fu_usb_backend_add_device(self, dev_list[i]);
libusb_free_device_list(dev_list, 1);
}
static gboolean
fu_usb_backend_rescan_cb(gpointer user_data)
{
FuUsbBackend *self = FU_USB_BACKEND(user_data);
fu_usb_backend_rescan(self);
return TRUE;
}
static void
fu_usb_backend_ensure_rescan_timeout(FuUsbBackend *self)
{
if (self->hotplug_poll_id > 0) {
g_source_remove(self->hotplug_poll_id);
self->hotplug_poll_id = 0;
}
if (self->hotplug_poll_interval > 0) {
self->hotplug_poll_id =
g_timeout_add(self->hotplug_poll_interval, fu_usb_backend_rescan_cb, self);
}
}
static void
fu_usb_backend_set_hotplug_poll_interval(FuUsbBackend *self, guint hotplug_poll_interval)
{
/* same */
if (self->hotplug_poll_interval == hotplug_poll_interval)
return;
self->hotplug_poll_interval = hotplug_poll_interval;
/* if already running then change the existing timeout */
if (self->hotplug_poll_id > 0)
fu_usb_backend_ensure_rescan_timeout(self);
}
#ifdef _WIN32
static void
fu_usb_backend_device_notify_flags_cb(FuDevice *device, GParamSpec *pspec, FuBackend *backend)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
/* if waiting for a disconnect, set win32 to poll insanely fast -- and set it
* back to the default when the device removal was detected */
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) {
g_debug("setting USB poll interval to %ums to detect replug",
(guint)FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG);
fu_usb_backend_set_hotplug_poll_interval(self,
FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG);
} else {
fu_usb_backend_set_hotplug_poll_interval(self,
FU_USB_BACKEND_POLL_INTERVAL_DEFAULT);
}
}
#endif
typedef struct {
FuUsbBackend *self;
libusb_device *dev;
libusb_hotplug_event event;
} FuUsbBackendIdleHelper;
static gpointer
fu_usb_backend_idle_helper_copy(gconstpointer src, gpointer user_data)
{
FuUsbBackendIdleHelper *helper_src = (FuUsbBackendIdleHelper *)src;
FuUsbBackendIdleHelper *helper_dst = g_new0(FuUsbBackendIdleHelper, 1);
helper_dst->self = g_object_ref(helper_src->self);
helper_dst->dev = libusb_ref_device(helper_src->dev);
helper_dst->event = helper_src->event;
return helper_dst;
}
/* always in the main thread */
static gboolean
fu_usb_backend_idle_hotplug_cb(gpointer user_data)
{
FuUsbBackend *self = FU_USB_BACKEND(user_data);
g_autoptr(GPtrArray) idle_events = NULL;
/* drain the idle events with the lock held */
g_mutex_lock(&self->idle_events_mutex);
idle_events = g_ptr_array_copy(self->idle_events, fu_usb_backend_idle_helper_copy, NULL);
g_ptr_array_set_size(self->idle_events, 0);
self->idle_events_id = 0;
g_mutex_unlock(&self->idle_events_mutex);
/* run the callbacks when not locked */
for (guint i = 0; i < idle_events->len; i++) {
FuUsbBackendIdleHelper *helper = g_ptr_array_index(idle_events, i);
switch (helper->event) {
case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED:
fu_usb_backend_add_device(helper->self, helper->dev);
break;
case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT:
fu_usb_backend_remove_device(helper->self, helper->dev);
break;
default:
break;
}
}
/* all done */
return FALSE;
}
/* this is run in the libusb thread */
static int LIBUSB_CALL
fu_usb_backend_hotplug_cb(struct libusb_context *ctx,
struct libusb_device *dev,
libusb_hotplug_event event,
void *user_data)
{
FuUsbBackend *self = FU_USB_BACKEND(user_data);
FuUsbBackendIdleHelper *helper;
g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->idle_events_mutex);
g_assert(locker != NULL); /* nocheck:blocked */
helper = g_new0(FuUsbBackendIdleHelper, 1);
helper->self = g_object_ref(self);
helper->dev = libusb_ref_device(dev);
helper->event = event;
g_ptr_array_add(self->idle_events, helper);
if (self->idle_events_id == 0)
self->idle_events_id = g_idle_add(fu_usb_backend_idle_hotplug_cb, self);
return 0;
}
static gpointer
fu_usb_backend_event_thread_cb(gpointer data)
{
FuUsbBackend *self = FU_USB_BACKEND(data);
struct timeval tv = {
.tv_usec = 0,
.tv_sec = 2,
};
while (g_atomic_int_get(&self->thread_event_run) > 0)
libusb_handle_events_timeout_completed(self->ctx, &tv, NULL);
return NULL;
}
#endif
static gboolean
fu_usb_backend_setup(FuBackend *backend,
FuBackendSetupFlags flags,
FuProgress *progress,
GError **error)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
FuContext *ctx = fu_backend_get_context(FU_BACKEND(self));
gint log_level = g_getenv("FWUPD_VERBOSE") != NULL ? 3 : 0;
gint rc;
#if defined(HAVE_LIBUSB_INIT_CONTEXT) && defined(HAVE_UDEV)
const struct libusb_init_option options[] = {{.option = LIBUSB_OPTION_NO_DEVICE_DISCOVERY,
.value = {
.ival = 1,
}}};
rc = libusb_init_context(&self->ctx, options, G_N_ELEMENTS(options));
#else
rc = libusb_init(&self->ctx);
#endif
if (rc < 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"failed to init libusb: %s [%i]",
libusb_strerror(rc),
rc);
return FALSE;
}
#ifdef HAVE_LIBUSB_SET_OPTION
libusb_set_option(self->ctx, LIBUSB_OPTION_LOG_LEVEL, log_level);
#else
libusb_set_debug(self->ctx, log_level);
#endif
fu_context_set_data(ctx, "libusb_context", self->ctx);
/* no hotplug required, probably in tests */
if ((flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) == 0)
return TRUE;
#ifndef HAVE_UDEV
self->thread_event_run = 1;
self->thread_event = g_thread_new("FuUsbBackendEvt", fu_usb_backend_event_thread_cb, self);
/* watch for add/remove */
if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
rc = libusb_hotplug_register_callback(self->ctx,
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED |
LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
0,
LIBUSB_HOTPLUG_MATCH_ANY,
LIBUSB_HOTPLUG_MATCH_ANY,
LIBUSB_HOTPLUG_MATCH_ANY,
fu_usb_backend_hotplug_cb,
self,
&self->hotplug_id);
if (rc != LIBUSB_SUCCESS) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"error creating a hotplug callback: %s [%i]",
libusb_strerror(rc),
rc);
return FALSE;
}
} else {
fu_usb_backend_set_hotplug_poll_interval(self,
FU_USB_BACKEND_POLL_INTERVAL_DEFAULT);
}
#endif
/* success */
return TRUE;
}
#ifndef HAVE_UDEV
static gboolean
fu_usb_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
fu_usb_backend_rescan(self);
return TRUE;
}
static void
fu_usb_backend_registered(FuBackend *backend, FuDevice *device)
{
#ifdef _WIN32
/* not required */
if (!FU_IS_USB_DEVICE(device))
return;
/* on win32 we need to poll the context faster */
g_signal_connect(FU_DEVICE(device),
"notify::flags",
G_CALLBACK(fu_usb_backend_device_notify_flags_cb),
backend);
#endif
}
/* not defined in FreeBSD */
#ifndef HAVE_LIBUSB_GET_PARENT
static libusb_device *
libusb_get_parent(libusb_device *dev) /* nocheck:name */
{
return NULL;
}
#endif
static FuDevice *
fu_usb_backend_get_device_parent(FuBackend *backend,
FuDevice *device,
const gchar *subsystem,
GError **error)
{
FuUsbBackend *self = FU_USB_BACKEND(backend);
libusb_device *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
libusb_device *usb_parent;
/* libusb or kernel */
if (usb_device == NULL) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device");
return NULL;
}
usb_parent = libusb_get_parent(usb_device);
if (usb_parent == NULL) {
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent");
return NULL;
}
return FU_DEVICE(fu_usb_backend_create_device(self, usb_parent));
}
#endif
static void
fu_usb_backend_finalize(GObject *object)
{
#ifndef HAVE_UDEV
FuUsbBackend *self = FU_USB_BACKEND(object);
/* this is safe to call even when self->hotplug_id is unset */
if (g_atomic_int_dec_and_test(&self->thread_event_run)) {
libusb_hotplug_deregister_callback(self->ctx, self->hotplug_id);
g_thread_join(self->thread_event);
}
if (self->idle_events_id > 0)
g_source_remove(self->idle_events_id);
if (self->hotplug_poll_id > 0)
g_source_remove(self->hotplug_poll_id);
if (self->ctx != NULL)
libusb_exit(self->ctx);
g_clear_pointer(&self->idle_events, g_ptr_array_unref);
g_mutex_clear(&self->idle_events_mutex);
#endif
G_OBJECT_CLASS(fu_usb_backend_parent_class)->finalize(object);
}
#ifndef HAVE_UDEV
static void
fu_usb_backend_idle_helper_free(FuUsbBackendIdleHelper *helper)
{
g_object_unref(helper->self);
libusb_unref_device(helper->dev);
g_free(helper);
}
#endif
static void
fu_usb_backend_init(FuUsbBackend *self)
{
#ifndef HAVE_UDEV
/* to escape the thread into the mainloop */
g_mutex_init(&self->idle_events_mutex);
self->idle_events =
g_ptr_array_new_with_free_func((GDestroyNotify)fu_usb_backend_idle_helper_free);
#endif
}
static void
fu_usb_backend_class_init(FuUsbBackendClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
FuBackendClass *backend_class = FU_BACKEND_CLASS(klass);
object_class->finalize = fu_usb_backend_finalize;
backend_class->setup = fu_usb_backend_setup;
#ifndef HAVE_UDEV
backend_class->coldplug = fu_usb_backend_coldplug;
backend_class->registered = fu_usb_backend_registered;
backend_class->get_device_parent = fu_usb_backend_get_device_parent;
backend_class->create_device = fu_usb_backend_create_device_impl;
#endif
}
FuBackend *
fu_usb_backend_new(FuContext *ctx)
{
return FU_BACKEND(g_object_new(FU_TYPE_USB_BACKEND, "name", "usb", "context", ctx, NULL));
}