| /* |
| * 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)); |
| } |