fwupd/src/fu-device-list.c
Richard Hughes 566f8e27c2 Do not check wait_removed as multiple devices can be WAIT_FOR_REPLUG
This makes devices that come back in bootloader mode with no children
timeout, which is bad UX. We don't even need the feature now that we
can WAIT_FOR_REPLUG on multiple devices now.
2021-08-31 12:47:46 +01:00

1055 lines
32 KiB
C

/*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuDeviceList"
#include "config.h"
#include <glib-object.h>
#include <string.h>
#include "fwupd-error.h"
#include "fu-device-list.h"
#include "fu-device-private.h"
#include "fu-mutex.h"
/**
* FuDeviceList:
*
* This list of devices provides a way to find a device using either the
* device-id or a GUID.
*
* The device list will emit ::added and ::removed signals when the device list
* has been changed. If the #FuDevice has changed during a device replug then
* the ::changed signal will be emitted instead of ::added and then ::removed.
*
* See also: [class@FuDevice]
*/
static void
fu_device_list_finalize(GObject *obj);
struct _FuDeviceList {
GObject parent_instance;
GPtrArray *devices; /* of FuDeviceItem */
GRWLock devices_mutex;
};
enum { SIGNAL_ADDED, SIGNAL_REMOVED, SIGNAL_CHANGED, SIGNAL_LAST };
static guint signals[SIGNAL_LAST] = {0};
typedef struct {
FuDevice *device;
FuDevice *device_old;
FuDeviceList *self; /* no ref */
guint remove_id;
} FuDeviceItem;
G_DEFINE_TYPE(FuDeviceList, fu_device_list, G_TYPE_OBJECT)
static void
fu_device_list_emit_device_added(FuDeviceList *self, FuDevice *device)
{
g_debug("::added %s", fu_device_get_id(device));
g_signal_emit(self, signals[SIGNAL_ADDED], 0, device);
}
static void
fu_device_list_emit_device_removed(FuDeviceList *self, FuDevice *device)
{
g_debug("::removed %s", fu_device_get_id(device));
g_signal_emit(self, signals[SIGNAL_REMOVED], 0, device);
}
static void
fu_device_list_emit_device_changed(FuDeviceList *self, FuDevice *device)
{
g_debug("::changed %s", fu_device_get_id(device));
g_signal_emit(self, signals[SIGNAL_CHANGED], 0, device);
}
static gchar *
fu_device_list_to_string(FuDeviceList *self)
{
GString *str = g_string_new(NULL);
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
gboolean wfr;
g_string_append_printf(str,
"%u [%p] %s\n",
i,
item,
item->remove_id != 0 ? "IN_TIMEOUT" : "");
wfr = fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
g_string_append_printf(str,
"new: %s [%p] %s\n",
fu_device_get_id(item->device),
item->device,
wfr ? "WAIT_FOR_REPLUG" : "");
if (item->device_old != NULL) {
wfr =
fu_device_has_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
g_string_append_printf(str,
"old: %s [%p] %s\n",
fu_device_get_id(item->device_old),
item->device_old,
wfr ? "WAIT_FOR_REPLUG" : "");
}
}
g_rw_lock_reader_unlock(&self->devices_mutex);
return g_string_free(str, FALSE);
}
/* we cannot use fu_device_get_children() as this will not find "parent-only"
* logical relationships added using fu_device_add_parent_guid() */
static GPtrArray *
fu_device_list_get_children(FuDeviceList *self, FuDevice *device)
{
GPtrArray *devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (device == fu_device_get_parent(item->device))
g_ptr_array_add(devices, g_object_ref(item->device));
}
g_rw_lock_reader_unlock(&self->devices_mutex);
return devices;
}
static void
fu_device_list_depsolve_order_full(FuDeviceList *self, FuDevice *device, guint depth)
{
g_autoptr(GPtrArray) children = NULL;
/* ourself */
fu_device_set_order(device, depth);
/* optional children */
children = fu_device_list_get_children(self, device);
for (guint i = 0; i < children->len; i++) {
FuDevice *child = g_ptr_array_index(children, i);
if (fu_device_has_flag(child, FWUPD_DEVICE_FLAG_INSTALL_PARENT_FIRST)) {
fu_device_list_depsolve_order_full(self, child, depth + 1);
} else {
fu_device_list_depsolve_order_full(self, child, depth - 1);
}
}
}
/**
* fu_device_list_depsolve_order:
* @self: a device list
* @device: a device
*
* Sets the device order using the logical parent->child relationships -- by default
* the child is updated first, unless the device has set flag
* %FWUPD_DEVICE_FLAG_INSTALL_PARENT_FIRST.
*
* Since: 1.5.0
**/
void
fu_device_list_depsolve_order(FuDeviceList *self, FuDevice *device)
{
g_autoptr(FuDevice) root = fu_device_get_root(device);
fu_device_list_depsolve_order_full(self, root, 0);
}
/**
* fu_device_list_get_all:
* @self: a device list
*
* Returns all the devices that have been added to the device list.
* This includes devices that are no longer active, for instance where a
* different plugin has taken over responsibility of the #FuDevice.
*
* Returns: (transfer container) (element-type FuDevice): the devices
*
* Since: 1.0.2
**/
GPtrArray *
fu_device_list_get_all(FuDeviceList *self)
{
GPtrArray *devices;
g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL);
devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
g_ptr_array_add(devices, g_object_ref(item->device));
}
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->device_old == NULL)
continue;
g_ptr_array_add(devices, g_object_ref(item->device_old));
}
g_rw_lock_reader_unlock(&self->devices_mutex);
return devices;
}
/**
* fu_device_list_get_active:
* @self: a device list
*
* Returns all the active devices that have been added to the device list.
* An active device is defined as a device that is currently connected and has
* is owned by a plugin.
*
* Returns: (transfer container) (element-type FuDevice): the devices
*
* Since: 1.0.2
**/
GPtrArray *
fu_device_list_get_active(FuDeviceList *self)
{
GPtrArray *devices;
g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL);
devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
g_ptr_array_add(devices, g_object_ref(item->device));
}
g_rw_lock_reader_unlock(&self->devices_mutex);
return devices;
}
static FuDeviceItem *
fu_device_list_find_by_device(FuDeviceList *self, FuDevice *device)
{
g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex);
g_return_val_if_fail(locker != NULL, NULL);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->device == device)
return item;
}
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->device_old == device)
return item;
}
return NULL;
}
static FuDeviceItem *
fu_device_list_find_by_guid(FuDeviceList *self, const gchar *guid)
{
g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex);
g_return_val_if_fail(locker != NULL, NULL);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (fu_device_has_guid(item->device, guid))
return item;
}
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->device_old == NULL)
continue;
if (fu_device_has_guid(item->device_old, guid))
return item;
}
return NULL;
}
static FuDeviceItem *
fu_device_list_find_by_connection(FuDeviceList *self,
const gchar *physical_id,
const gchar *logical_id)
{
g_autoptr(GRWLockReaderLocker) locker = NULL;
if (physical_id == NULL)
return NULL;
locker = g_rw_lock_reader_locker_new(&self->devices_mutex);
g_return_val_if_fail(locker != NULL, NULL);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i);
FuDevice *device = item_tmp->device;
if (device != NULL &&
g_strcmp0(fu_device_get_physical_id(device), physical_id) == 0 &&
g_strcmp0(fu_device_get_logical_id(device), logical_id) == 0)
return item_tmp;
}
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i);
FuDevice *device = item_tmp->device_old;
if (device != NULL &&
g_strcmp0(fu_device_get_physical_id(device), physical_id) == 0 &&
g_strcmp0(fu_device_get_logical_id(device), logical_id) == 0)
return item_tmp;
}
return NULL;
}
static FuDeviceItem *
fu_device_list_find_by_id(FuDeviceList *self, const gchar *device_id, gboolean *multiple_matches)
{
FuDeviceItem *item = NULL;
gsize device_id_len;
/* sanity check */
if (device_id == NULL) {
g_critical("device ID was NULL");
return NULL;
}
/* support abbreviated hashes */
device_id_len = strlen(device_id);
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i);
const gchar *ids[] = {fu_device_get_id(item_tmp->device),
fu_device_get_equivalent_id(item_tmp->device),
NULL};
for (guint j = 0; ids[j] != NULL; j++) {
if (strncmp(ids[j], device_id, device_id_len) == 0) {
if (item != NULL && multiple_matches != NULL)
*multiple_matches = TRUE;
item = item_tmp;
}
}
}
g_rw_lock_reader_unlock(&self->devices_mutex);
if (item != NULL)
return item;
/* only search old devices if we didn't find the active device */
g_rw_lock_reader_lock(&self->devices_mutex);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i);
const gchar *ids[3] = {NULL};
if (item_tmp->device_old == NULL)
continue;
ids[0] = fu_device_get_id(item_tmp->device_old);
ids[1] = fu_device_get_equivalent_id(item_tmp->device_old);
for (guint j = 0; ids[j] != NULL; j++) {
if (strncmp(ids[j], device_id, device_id_len) == 0) {
if (item != NULL && multiple_matches != NULL)
*multiple_matches = TRUE;
item = item_tmp;
}
}
}
g_rw_lock_reader_unlock(&self->devices_mutex);
return item;
}
/**
* fu_device_list_get_old:
* @self: a device list
* @device: a device
*
* Returns the old device associated with the currently active device.
*
* Returns: (transfer full): the device, or %NULL if not found
*
* Since: 1.0.3
**/
FuDevice *
fu_device_list_get_old(FuDeviceList *self, FuDevice *device)
{
FuDeviceItem *item = fu_device_list_find_by_device(self, device);
if (item == NULL)
return NULL;
if (item->device_old == NULL)
return NULL;
return g_object_ref(item->device_old);
}
static FuDeviceItem *
fu_device_list_get_by_guids_removed(FuDeviceList *self, GPtrArray *guids)
{
g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex);
g_return_val_if_fail(locker != NULL, NULL);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->remove_id == 0)
continue;
for (guint j = 0; j < guids->len; j++) {
const gchar *guid = g_ptr_array_index(guids, j);
if (fu_device_has_guid(item->device, guid))
return item;
}
}
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item = g_ptr_array_index(self->devices, i);
if (item->device_old == NULL)
continue;
if (item->remove_id == 0)
continue;
for (guint j = 0; j < guids->len; j++) {
const gchar *guid = g_ptr_array_index(guids, j);
if (fu_device_has_guid(item->device_old, guid))
return item;
}
}
return NULL;
}
static gboolean
fu_device_list_device_delayed_remove_cb(gpointer user_data)
{
FuDeviceItem *item = (FuDeviceItem *)user_data;
FuDeviceList *self = FU_DEVICE_LIST(item->self);
/* no longer valid */
item->remove_id = 0;
/* remove any children associated with device */
if (!fu_device_has_internal_flag(item->device,
FU_DEVICE_INTERNAL_FLAG_NO_AUTO_REMOVE_CHILDREN)) {
GPtrArray *children = fu_device_get_children(item->device);
for (guint j = 0; j < children->len; j++) {
FuDevice *child = g_ptr_array_index(children, j);
FuDeviceItem *child_item;
child_item = fu_device_list_find_by_id(self, fu_device_get_id(child), NULL);
if (child_item == NULL) {
g_debug("device %s not found", fu_device_get_id(child));
continue;
}
fu_device_list_emit_device_removed(self, child);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_remove(self->devices, child_item);
g_rw_lock_writer_unlock(&self->devices_mutex);
}
}
/* just remove now */
g_debug("doing delayed removal");
fu_device_list_emit_device_removed(self, item->device);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_remove(self->devices, item);
g_rw_lock_writer_unlock(&self->devices_mutex);
return G_SOURCE_REMOVE;
}
static void
fu_device_list_remove_with_delay(FuDeviceItem *item)
{
/* we can't do anything with an unconnected device */
fu_device_remove_flag(item->device, FWUPD_DEVICE_FLAG_UPDATABLE);
/* give the hardware time to re-enumerate or the user time to
* re-insert the device with a magic button pressed */
g_debug("waiting %ums for %s device removal",
fu_device_get_remove_delay(item->device),
fu_device_get_name(item->device));
item->remove_id = g_timeout_add(fu_device_get_remove_delay(item->device),
fu_device_list_device_delayed_remove_cb,
item);
}
/**
* fu_device_list_remove:
* @self: a device list
* @device: a device
*
* Removes a specific device from the list if it exists.
*
* If the @device has a remove-delay set then a timeout will be started. If
* the exact same #FuDevice is added to the list with fu_device_list_add()
* within the timeout then only a ::changed signal will be emitted.
*
* If there is no remove-delay set, the ::removed signal will be emitted
* straight away.
*
* Since: 1.0.2
**/
void
fu_device_list_remove(FuDeviceList *self, FuDevice *device)
{
FuDeviceItem *item;
g_return_if_fail(FU_IS_DEVICE_LIST(self));
g_return_if_fail(FU_IS_DEVICE(device));
/* check the device already exists */
item = fu_device_list_find_by_id(self, fu_device_get_id(device), NULL);
if (item == NULL) {
g_debug("device %s not found", fu_device_get_id(device));
return;
}
/* ensure never fired if the remove delay is changed */
if (item->remove_id > 0) {
g_source_remove(item->remove_id);
item->remove_id = 0;
}
/* delay the removal and check for replug */
if (fu_device_get_remove_delay(item->device) > 0) {
fu_device_list_remove_with_delay(item);
return;
}
/* remove any children associated with device */
if (!fu_device_has_internal_flag(item->device,
FU_DEVICE_INTERNAL_FLAG_NO_AUTO_REMOVE_CHILDREN)) {
GPtrArray *children = fu_device_get_children(device);
for (guint j = 0; j < children->len; j++) {
FuDevice *child = g_ptr_array_index(children, j);
FuDeviceItem *child_item;
child_item = fu_device_list_find_by_id(self, fu_device_get_id(child), NULL);
if (child_item == NULL) {
g_debug("device %s not found", fu_device_get_id(child));
continue;
}
fu_device_list_emit_device_removed(self, child);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_remove(self->devices, child_item);
g_rw_lock_writer_unlock(&self->devices_mutex);
}
}
/* remove right now */
fu_device_list_emit_device_removed(self, item->device);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_remove(self->devices, item);
g_rw_lock_writer_unlock(&self->devices_mutex);
}
static void
fu_device_list_add_missing_guids(FuDevice *device_new, FuDevice *device_old)
{
GPtrArray *guids_old = fu_device_get_guids(device_old);
for (guint i = 0; i < guids_old->len; i++) {
const gchar *guid_tmp = g_ptr_array_index(guids_old, i);
if (!fu_device_has_guid(device_new, guid_tmp)) {
if (fu_device_has_flag(device_new,
FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS)) {
g_debug("adding GUID %s to device", guid_tmp);
fu_device_add_counterpart_guid(device_new, guid_tmp);
} else {
g_debug("not adding GUID %s to device, use "
"FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS if required",
guid_tmp);
}
}
}
}
static void
fu_device_list_item_finalized_cb(gpointer data, GObject *where_the_object_was)
{
FuDeviceItem *item = (FuDeviceItem *)data;
FuDeviceList *self = FU_DEVICE_LIST(item->self);
g_critical("FuDevice %p was finalized without being removed from "
"FuDeviceList, removing item!",
where_the_object_was);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_remove(self->devices, item);
g_rw_lock_writer_unlock(&self->devices_mutex);
}
/* this should never be required, and yet here we are */
static void
fu_device_list_item_set_device(FuDeviceItem *item, FuDevice *device)
{
if (item->device != NULL) {
g_object_weak_unref(G_OBJECT(item->device), fu_device_list_item_finalized_cb, item);
}
if (device != NULL) {
g_object_weak_ref(G_OBJECT(device), fu_device_list_item_finalized_cb, item);
}
g_set_object(&item->device, device);
}
static void
fu_device_list_clear_wait_for_replug(FuDeviceList *self, FuDeviceItem *item)
{
/* clear timeout if scheduled */
if (item->remove_id != 0) {
g_source_remove(item->remove_id);
item->remove_id = 0;
}
/* remove flag on both old and new devices */
if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) {
g_debug("%s device came back, clearing flag", fu_device_get_id(item->device));
fu_device_remove_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
}
if (item->device_old != NULL) {
if (fu_device_has_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) {
g_debug("%s old device came back, clearing flag",
fu_device_get_id(item->device_old));
fu_device_remove_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
}
}
/* optional debug */
if (g_getenv("FWUPD_DEVICE_LIST_VERBOSE") != NULL) {
g_autofree gchar *str = fu_device_list_to_string(self);
g_debug("\n%s", str);
}
}
static void
fu_device_incorporate_update_state(FuDevice *self, FuDevice *donor)
{
if (fu_device_get_update_error(donor) != NULL && fu_device_get_update_error(self) == NULL) {
const gchar *update_error = fu_device_get_update_error(donor);
g_debug("copying update error %s to new device", update_error);
fu_device_set_update_error(self, update_error);
}
if (fu_device_get_update_state(donor) != FWUPD_UPDATE_STATE_UNKNOWN &&
fu_device_get_update_state(self) == FWUPD_UPDATE_STATE_UNKNOWN) {
FwupdUpdateState update_state = fu_device_get_update_state(donor);
g_debug("copying update state %s to new device",
fwupd_update_state_to_string(update_state));
fu_device_set_update_state(self, update_state);
}
}
static void
fu_device_list_replace(FuDeviceList *self, FuDeviceItem *item, FuDevice *device)
{
guint64 private_flags;
GPtrArray *vendor_ids;
/* copy over any GUIDs that used to exist */
fu_device_list_add_missing_guids(device, item->device);
/* enforce the vendor ID if specified */
vendor_ids = fu_device_get_vendor_ids(item->device);
for (guint i = 0; i < vendor_ids->len; i++) {
const gchar *vendor_id = g_ptr_array_index(vendor_ids, i);
g_debug("copying old vendor ID %s to new device", vendor_id);
fu_device_add_vendor_id(device, vendor_id);
}
/* copy over custom flags */
private_flags = fu_device_get_private_flags(item->device);
if (private_flags != 0) {
g_debug("copying old custom flags 0x%x to new device", (guint)private_flags);
fu_device_set_private_flags(device, private_flags);
}
/* copy over the version strings if not set */
if (fu_device_get_version(item->device) != NULL && fu_device_get_version(device) == NULL) {
const gchar *version = fu_device_get_version(item->device);
g_debug("copying old version %s to new device", version);
fu_device_set_version_format(device, fu_device_get_version_format(item->device));
fu_device_set_version(device, version);
}
/* always use the runtime version */
if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_USE_RUNTIME_VERSION) &&
fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER)) {
const gchar *version = fu_device_get_version(item->device);
g_debug("forcing runtime version %s to new device", version);
fu_device_set_version_format(device, fu_device_get_version_format(item->device));
fu_device_set_version(device, version);
}
/* allow another plugin to handle the write too */
if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED)) {
g_debug("copying another-write-required to new device");
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
}
/* device won't come back in right mode */
if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) {
g_debug("copying will-disappear to new device");
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR);
}
/* copy the parent if not already set */
if (fu_device_get_parent(item->device) != NULL &&
fu_device_get_parent(item->device) != device &&
fu_device_get_parent(device) != item->device && fu_device_get_parent(device) == NULL) {
FuDevice *parent = fu_device_get_parent(item->device);
g_debug("copying parent %s to new device", fu_device_get_id(parent));
fu_device_set_parent(device, parent);
}
/* copy the update state if known */
fu_device_incorporate_update_state(item->device, device);
/* assign the new device */
g_set_object(&item->device_old, item->device);
fu_device_list_item_set_device(item, device);
fu_device_list_emit_device_changed(self, device);
if (g_getenv("FWUPD_DEVICE_LIST_VERBOSE") != NULL) {
g_autofree gchar *str = fu_device_list_to_string(self);
g_debug("\n%s", str);
}
/* we were waiting for this... */
fu_device_list_clear_wait_for_replug(self, item);
}
/**
* fu_device_list_add:
* @self: a device list
* @device: a device
*
* Adds a specific device to the device list if not already present.
*
* If the @device (or a compatible @device) has been previously removed within
* the remove-timeout then only the ::changed signal will be emitted on calling
* this function. Otherwise the ::added signal will be emitted straight away.
*
* Compatible devices are defined as #FuDevice objects that share at least one
* device GUID. If a compatible device is matched then the vendor ID and
* version will be copied to the new object if they are not already set.
*
* Any GUIDs present on the old device and not on the new device will be
* inherited and do not have to be copied over by plugins manually.
*
* Returns: (transfer none): a device, or %NULL if not found
*
* Since: 1.0.2
**/
void
fu_device_list_add(FuDeviceList *self, FuDevice *device)
{
FuDeviceItem *item;
g_return_if_fail(FU_IS_DEVICE_LIST(self));
g_return_if_fail(FU_IS_DEVICE(device));
/* is the device waiting to be replugged? */
item = fu_device_list_find_by_id(self, fu_device_get_id(device), NULL);
if (item != NULL) {
/* literally the same object */
if (g_strcmp0(fu_device_get_id(device), fu_device_get_id(item->device)) == 0) {
g_debug("found existing device %s", fu_device_get_id(device));
if (device != item->device) {
fu_device_incorporate_update_state(device, item->device);
fu_device_list_item_set_device(item, device);
}
fu_device_list_clear_wait_for_replug(self, item);
fu_device_list_emit_device_changed(self, device);
return;
}
/* the old device again */
if (item->device_old != NULL &&
g_strcmp0(fu_device_get_id(device), fu_device_get_id(item->device_old)) == 0) {
g_debug("found old device %s, swapping", fu_device_get_id(device));
fu_device_incorporate_update_state(device, item->device);
g_set_object(&item->device_old, item->device);
fu_device_list_item_set_device(item, device);
fu_device_list_clear_wait_for_replug(self, item);
fu_device_list_emit_device_changed(self, device);
return;
}
/* same ID, different object */
g_debug("found existing device %s, reusing item", fu_device_get_id(item->device));
fu_device_list_replace(self, item, device);
return;
}
/* verify a device with same connection does not already exist */
item = fu_device_list_find_by_connection(self,
fu_device_get_physical_id(device),
fu_device_get_logical_id(device));
if (item != NULL && item->remove_id != 0) {
g_debug("found physical device %s recently removed, reusing "
"item from plugin %s for plugin %s",
fu_device_get_id(item->device),
fu_device_get_plugin(item->device),
fu_device_get_plugin(device));
fu_device_list_replace(self, item, device);
return;
}
/* verify a compatible device does not already exist */
item = fu_device_list_get_by_guids_removed(self, fu_device_get_guids(device));
if (item != NULL) {
if (fu_device_has_internal_flag(device,
FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID)) {
g_debug("found compatible device %s recently removed, reusing "
"item from plugin %s for plugin %s",
fu_device_get_id(item->device),
fu_device_get_plugin(item->device),
fu_device_get_plugin(device));
fu_device_list_replace(self, item, device);
return;
} else {
g_debug("not adding matching %s for device add, use "
"FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID if required",
fu_device_get_id(item->device));
}
}
/* add helper */
item = g_new0(FuDeviceItem, 1);
item->self = self; /* no ref */
fu_device_list_item_set_device(item, device);
g_rw_lock_writer_lock(&self->devices_mutex);
g_ptr_array_add(self->devices, item);
g_rw_lock_writer_unlock(&self->devices_mutex);
fu_device_list_emit_device_added(self, device);
}
/**
* fu_device_list_get_by_guid:
* @self: a device list
* @guid: a device GUID
* @error: (nullable): optional return location for an error
*
* Finds a specific device that has the matching GUID.
*
* Returns: (transfer full): a device, or %NULL if not found
*
* Since: 1.0.2
**/
FuDevice *
fu_device_list_get_by_guid(FuDeviceList *self, const gchar *guid, GError **error)
{
FuDeviceItem *item;
g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL);
g_return_val_if_fail(guid != NULL, NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
item = fu_device_list_find_by_guid(self, guid);
if (item != NULL)
return g_object_ref(item->device);
g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GUID %s was not found", guid);
return NULL;
}
static GPtrArray *
fu_device_list_get_wait_for_replug(FuDeviceList *self)
{
GPtrArray *devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
for (guint i = 0; i < self->devices->len; i++) {
FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i);
if (fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG))
g_ptr_array_add(devices, g_object_ref(item_tmp->device));
}
return devices;
}
/**
* fu_device_list_wait_for_replug:
* @self: a device list
* @error: (nullable): optional return location for an error
*
* Waits for all the devices with %FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG to replug.
*
* If the device does not exist this function returns without an error.
*
* Returns: %TRUE for success
*
* Since: 1.1.2
**/
gboolean
fu_device_list_wait_for_replug(FuDeviceList *self, GError **error)
{
guint remove_delay = 0;
g_autoptr(GTimer) timer = g_timer_new();
g_autoptr(GPtrArray) devices_wfr1 = NULL;
g_autoptr(GPtrArray) devices_wfr2 = NULL;
g_return_val_if_fail(FU_IS_DEVICE_LIST(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* not required, or possibly literally just happened */
devices_wfr1 = fu_device_list_get_wait_for_replug(self);
if (devices_wfr1->len == 0) {
g_debug("no replug or re-enumerate required");
return TRUE;
}
/* use the maximum of all the devices */
for (guint i = 0; i < devices_wfr1->len; i++) {
FuDevice *device_tmp = g_ptr_array_index(devices_wfr1, i);
if (fu_device_get_remove_delay(device_tmp) > remove_delay)
remove_delay = fu_device_get_remove_delay(device_tmp);
}
/* plugin did not specify */
if (remove_delay == 0) {
remove_delay = FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE;
g_warning("plugin did not specify a remove delay, "
"so guessing we should wait %ums for replug",
remove_delay);
} else {
g_debug("waiting %ums for replug", remove_delay);
}
/* time to unplug and then re-plug */
do {
g_autoptr(GPtrArray) devices_wfr_tmp = NULL;
g_usleep(1000);
g_main_context_iteration(NULL, FALSE);
devices_wfr_tmp = fu_device_list_get_wait_for_replug(self);
if (devices_wfr_tmp->len == 0)
break;
} while (g_timer_elapsed(timer, NULL) * 1000.f < remove_delay);
/* check that no other devices are still waiting for replug */
devices_wfr2 = fu_device_list_get_wait_for_replug(self);
if (devices_wfr2->len > 0) {
g_autoptr(GPtrArray) device_ids = g_ptr_array_new_with_free_func(g_free);
g_autofree gchar *device_ids_str = NULL;
/* dump to console */
if (g_getenv("FWUPD_DEVICE_LIST_VERBOSE") != NULL) {
g_autofree gchar *str = fu_device_list_to_string(self);
g_debug("\n%s", str);
}
/* unset and build error string */
for (guint i = 0; i < devices_wfr2->len; i++) {
FuDevice *device_tmp = g_ptr_array_index(devices_wfr2, i);
fu_device_remove_flag(device_tmp, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
g_ptr_array_add(device_ids, g_strdup(fu_device_get_id(device_tmp)));
}
device_ids_str = fu_common_strjoin_array(",", device_ids);
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_FOUND,
"device %s did not come back",
device_ids_str);
return FALSE;
}
/* the loop was quit without the timer */
g_debug("waited for replug");
return TRUE;
}
/**
* fu_device_list_get_by_id:
* @self: a device list
* @device_id: a device ID, typically a SHA1 hash
* @error: (nullable): optional return location for an error
*
* Finds a specific device using the ID string. This function also supports
* using abbreviated hashes.
*
* Returns: (transfer full): a device, or %NULL if not found
*
* Since: 1.0.2
**/
FuDevice *
fu_device_list_get_by_id(FuDeviceList *self, const gchar *device_id, GError **error)
{
FuDeviceItem *item;
gboolean multiple_matches = FALSE;
g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL);
g_return_val_if_fail(device_id != NULL, NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
/* multiple things matched */
item = fu_device_list_find_by_id(self, device_id, &multiple_matches);
if (multiple_matches) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"device ID %s was not unique",
device_id);
return NULL;
}
/* nothing at all matched */
if (item == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_FOUND,
"device ID %s was not found",
device_id);
return NULL;
}
/* something found */
return g_object_ref(item->device);
}
static void
fu_device_list_item_free(FuDeviceItem *item)
{
if (item->remove_id != 0)
g_source_remove(item->remove_id);
if (item->device_old != NULL)
g_object_unref(item->device_old);
fu_device_list_item_set_device(item, NULL);
g_free(item);
}
static void
fu_device_list_class_init(FuDeviceListClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fu_device_list_finalize;
signals[SIGNAL_ADDED] = g_signal_new("added",
G_TYPE_FROM_CLASS(object_class),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
FU_TYPE_DEVICE);
signals[SIGNAL_REMOVED] = g_signal_new("removed",
G_TYPE_FROM_CLASS(object_class),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
FU_TYPE_DEVICE);
signals[SIGNAL_CHANGED] = g_signal_new("changed",
G_TYPE_FROM_CLASS(object_class),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE,
1,
FU_TYPE_DEVICE);
}
static void
fu_device_list_init(FuDeviceList *self)
{
self->devices = g_ptr_array_new_with_free_func((GDestroyNotify)fu_device_list_item_free);
g_rw_lock_init(&self->devices_mutex);
}
static void
fu_device_list_finalize(GObject *obj)
{
FuDeviceList *self = FU_DEVICE_LIST(obj);
g_rw_lock_clear(&self->devices_mutex);
g_ptr_array_unref(self->devices);
G_OBJECT_CLASS(fu_device_list_parent_class)->finalize(obj);
}
/**
* fu_device_list_new:
*
* Creates a new device list.
*
* Returns: (transfer full): a device list
*
* Since: 1.0.2
**/
FuDeviceList *
fu_device_list_new(void)
{
FuDeviceList *self;
self = g_object_new(FU_TYPE_DEVICE_LIST, NULL);
return FU_DEVICE_LIST(self);
}