mirror of
https://git.proxmox.com/git/fwupd
synced 2025-06-03 13:41:12 +00:00
552 lines
16 KiB
C
552 lines
16 KiB
C
|
|
/*
|
|
* Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <fwupdplugin.h>
|
|
|
|
#include <libmm-glib.h>
|
|
#include <string.h>
|
|
|
|
#include "fu-mm-device.h"
|
|
#include "fu-mm-utils.h"
|
|
|
|
/* amount of time to wait for ports of the same device being exposed by kernel */
|
|
#define FU_MM_UDEV_DEVICE_PORTS_TIMEOUT 3 /* s */
|
|
|
|
/* out-of-tree modem-power driver is unsupported */
|
|
#define MODEM_POWER_SYSFS_PATH "/sys/class/modem-power"
|
|
|
|
struct FuPluginData {
|
|
MMManager *manager;
|
|
gboolean manager_ready;
|
|
GUdevClient *udev_client;
|
|
GFileMonitor *modem_power_monitor;
|
|
guint udev_timeout_id;
|
|
|
|
/* when a device is inhibited from MM, we store all relevant details
|
|
* ourselves to recreate a functional device object even without MM
|
|
*/
|
|
FuMmDevice *shadow_device;
|
|
};
|
|
|
|
static void
|
|
fu_plugin_mm_load(FuContext *ctx)
|
|
{
|
|
fu_context_add_quirk_key(ctx, "ModemManagerBranchAtCommand");
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_udev_device_removed(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
FuMmDevice *dev;
|
|
|
|
if (priv->shadow_device == NULL)
|
|
return;
|
|
|
|
dev = fu_plugin_cache_lookup(plugin,
|
|
fu_device_get_physical_id(FU_DEVICE(priv->shadow_device)));
|
|
if (dev == NULL)
|
|
return;
|
|
|
|
/* once the first port is gone, consider device is gone */
|
|
fu_plugin_cache_remove(plugin, fu_device_get_physical_id(FU_DEVICE(priv->shadow_device)));
|
|
fu_plugin_device_remove(plugin, FU_DEVICE(dev));
|
|
|
|
/* no need to wait for more ports, cancel that right away */
|
|
if (priv->udev_timeout_id != 0) {
|
|
g_source_remove(priv->udev_timeout_id);
|
|
priv->udev_timeout_id = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_uninhibit_device(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_autoptr(FuMmDevice) shadow_device = NULL;
|
|
|
|
g_clear_object(&priv->udev_client);
|
|
|
|
/* get the device removed from the plugin cache before uninhibiting */
|
|
fu_plugin_mm_udev_device_removed(plugin);
|
|
|
|
shadow_device = g_steal_pointer(&priv->shadow_device);
|
|
if (priv->manager != NULL && shadow_device != NULL) {
|
|
const gchar *inhibition_uid = fu_mm_device_get_inhibition_uid(shadow_device);
|
|
g_debug("uninhibit modemmanager device with uid %s", inhibition_uid);
|
|
mm_manager_uninhibit_device_sync(priv->manager, inhibition_uid, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_udev_device_ports_timeout(gpointer user_data)
|
|
{
|
|
FuPlugin *plugin = user_data;
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
FuMmDevice *dev;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
g_return_val_if_fail(priv->shadow_device != NULL, G_SOURCE_REMOVE);
|
|
priv->udev_timeout_id = 0;
|
|
|
|
dev = fu_plugin_cache_lookup(plugin,
|
|
fu_device_get_physical_id(FU_DEVICE(priv->shadow_device)));
|
|
if (dev != NULL) {
|
|
if (!fu_device_probe(FU_DEVICE(dev), &error)) {
|
|
g_warning("failed to probe MM device: %s", error->message);
|
|
} else {
|
|
fu_plugin_device_add(plugin, FU_DEVICE(dev));
|
|
}
|
|
}
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_udev_device_ports_timeout_reset(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
|
|
g_return_if_fail(priv->shadow_device != NULL);
|
|
if (priv->udev_timeout_id != 0)
|
|
g_source_remove(priv->udev_timeout_id);
|
|
priv->udev_timeout_id = g_timeout_add_seconds(FU_MM_UDEV_DEVICE_PORTS_TIMEOUT,
|
|
fu_plugin_mm_udev_device_ports_timeout,
|
|
plugin);
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_udev_device_port_added(FuPlugin *plugin,
|
|
const gchar *subsystem,
|
|
const gchar *path,
|
|
gint ifnum)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
FuMmDevice *existing;
|
|
g_autoptr(FuMmDevice) dev = NULL;
|
|
|
|
g_return_if_fail(priv->shadow_device != NULL);
|
|
|
|
existing =
|
|
fu_plugin_cache_lookup(plugin,
|
|
fu_device_get_physical_id(FU_DEVICE(priv->shadow_device)));
|
|
if (existing != NULL) {
|
|
/* add port to existing device */
|
|
fu_mm_device_udev_add_port(existing, subsystem, path, ifnum);
|
|
fu_plugin_mm_udev_device_ports_timeout_reset(plugin);
|
|
return;
|
|
}
|
|
|
|
/* create device and add to cache */
|
|
dev = fu_mm_device_udev_new(fu_plugin_get_context(plugin),
|
|
priv->manager,
|
|
priv->shadow_device);
|
|
fu_mm_device_udev_add_port(dev, subsystem, path, ifnum);
|
|
fu_plugin_cache_add(plugin, fu_device_get_physical_id(FU_DEVICE(priv->shadow_device)), dev);
|
|
|
|
/* wait a bit before probing, in case more ports get added */
|
|
fu_plugin_mm_udev_device_ports_timeout_reset(plugin);
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_udev_uevent_cb(GUdevClient *udev,
|
|
const gchar *action,
|
|
GUdevDevice *device,
|
|
gpointer user_data)
|
|
{
|
|
FuPlugin *plugin = FU_PLUGIN(user_data);
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
const gchar *subsystem = g_udev_device_get_subsystem(device);
|
|
const gchar *name = g_udev_device_get_name(device);
|
|
g_autofree gchar *path = NULL;
|
|
g_autofree gchar *device_sysfs_path = NULL;
|
|
g_autofree gchar *device_bus = NULL;
|
|
gint ifnum = -1;
|
|
|
|
if (action == NULL || subsystem == NULL || priv->shadow_device == NULL || name == NULL)
|
|
return TRUE;
|
|
|
|
/* ignore if loading port info fails */
|
|
if (!fu_mm_utils_get_udev_port_info(device, &device_bus, &device_sysfs_path, &ifnum, NULL))
|
|
return TRUE;
|
|
|
|
/* ignore non-USB and non-PCI events */
|
|
if (g_strcmp0(device_bus, "USB") != 0 && g_strcmp0(device_bus, "PCI") != 0)
|
|
return TRUE;
|
|
|
|
/* ignore all events for ports not owned by our device */
|
|
if (g_strcmp0(device_sysfs_path,
|
|
fu_device_get_physical_id(FU_DEVICE(priv->shadow_device))) != 0)
|
|
return TRUE;
|
|
|
|
path = g_strdup_printf("/dev/%s", name);
|
|
|
|
if ((g_str_equal(action, "add")) || (g_str_equal(action, "change"))) {
|
|
g_debug("added port to shadow_device modem: %s (ifnum %d)", path, ifnum);
|
|
fu_plugin_mm_udev_device_port_added(plugin, subsystem, path, ifnum);
|
|
} else if (g_str_equal(action, "remove")) {
|
|
g_debug("removed port from shadow_device modem: %s", path);
|
|
fu_plugin_mm_udev_device_removed(plugin);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_inhibit_device(FuPlugin *plugin, FuDevice *device, GError **error)
|
|
{
|
|
const gchar *inhibition_uid;
|
|
static const gchar *subsystems[] = {"tty", "usbmisc", "wwan", NULL};
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_autoptr(FuMmDevice) shadow_device = NULL;
|
|
|
|
fu_plugin_mm_uninhibit_device(plugin);
|
|
|
|
shadow_device = fu_mm_shadow_device_new(FU_MM_DEVICE(device));
|
|
inhibition_uid = fu_mm_device_get_inhibition_uid(shadow_device);
|
|
g_debug("inhibit modemmanager device with uid %s", inhibition_uid);
|
|
if (!mm_manager_inhibit_device_sync(priv->manager, inhibition_uid, NULL, error))
|
|
return FALSE;
|
|
|
|
/* setup shadow_device device info */
|
|
priv->shadow_device = g_steal_pointer(&shadow_device);
|
|
|
|
/* only do modem port monitoring using udev if the module is expected
|
|
* to reset itself into a fully different layout, e.g. a fastboot device */
|
|
if (fu_mm_device_get_update_methods(FU_MM_DEVICE(device)) &
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) {
|
|
priv->udev_client = g_udev_client_new(subsystems);
|
|
g_signal_connect(G_UDEV_CLIENT(priv->udev_client),
|
|
"uevent",
|
|
G_CALLBACK(fu_plugin_mm_udev_uevent_cb),
|
|
plugin);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_ensure_modem_power_inhibit(FuPlugin *plugin, FuDevice *device)
|
|
{
|
|
if (g_file_test(MODEM_POWER_SYSFS_PATH, G_FILE_TEST_EXISTS)) {
|
|
fu_device_inhibit(device,
|
|
"modem-power",
|
|
"The modem-power kernel driver cannot be used");
|
|
} else {
|
|
fu_device_uninhibit(device, "modem-power");
|
|
}
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_device_add(FuPlugin *plugin, MMObject *modem)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
const gchar *object_path = mm_object_get_path(modem);
|
|
g_autoptr(FuMmDevice) dev = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
g_debug("added modem: %s", object_path);
|
|
|
|
if (fu_plugin_cache_lookup(plugin, object_path) != NULL) {
|
|
g_warning("MM device already added, ignoring");
|
|
return;
|
|
}
|
|
dev = fu_mm_device_new(fu_plugin_get_context(plugin), priv->manager, modem);
|
|
if (!fu_device_setup(FU_DEVICE(dev), &error)) {
|
|
g_warning("failed to probe MM device: %s", error->message);
|
|
return;
|
|
}
|
|
fu_plugin_mm_ensure_modem_power_inhibit(plugin, FU_DEVICE(dev));
|
|
fu_plugin_device_add(plugin, FU_DEVICE(dev));
|
|
fu_plugin_cache_add(plugin, object_path, dev);
|
|
fu_plugin_cache_add(plugin, fu_device_get_physical_id(FU_DEVICE(dev)), dev);
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_device_added_cb(MMManager *manager, MMObject *modem, FuPlugin *plugin)
|
|
{
|
|
fu_plugin_mm_device_add(plugin, modem);
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_device_removed_cb(MMManager *manager, MMObject *modem, FuPlugin *plugin)
|
|
{
|
|
const gchar *object_path = mm_object_get_path(modem);
|
|
FuMmDevice *dev = fu_plugin_cache_lookup(plugin, object_path);
|
|
MMModemFirmwareUpdateMethod update_methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE;
|
|
|
|
if (dev == NULL)
|
|
return;
|
|
g_debug("removed modem: %s", mm_object_get_path(modem));
|
|
|
|
#if MM_CHECK_VERSION(1, 19, 1)
|
|
/* No information will be displayed during the upgrade process if the
|
|
* device is removed, the main reason is that device is "removed" from
|
|
* ModemManager, but it still exists in the system */
|
|
update_methods =
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU | MM_MODEM_FIRMWARE_UPDATE_METHOD_SAHARA;
|
|
#elif MM_CHECK_VERSION(1, 17, 1)
|
|
update_methods = MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU;
|
|
#endif
|
|
|
|
if (!(fu_mm_device_get_update_methods(FU_MM_DEVICE(dev)) & update_methods)) {
|
|
fu_plugin_cache_remove(plugin, object_path);
|
|
fu_plugin_device_remove(plugin, FU_DEVICE(dev));
|
|
}
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_teardown_manager(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
|
|
if (priv->manager_ready) {
|
|
g_debug("ModemManager no longer available");
|
|
g_signal_handlers_disconnect_by_func(priv->manager,
|
|
G_CALLBACK(fu_plugin_mm_device_added_cb),
|
|
plugin);
|
|
g_signal_handlers_disconnect_by_func(priv->manager,
|
|
G_CALLBACK(fu_plugin_mm_device_removed_cb),
|
|
plugin);
|
|
priv->manager_ready = FALSE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_setup_manager(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
const gchar *version = mm_manager_get_version(priv->manager);
|
|
GList *list;
|
|
|
|
if (fu_version_compare(version, MM_REQUIRED_VERSION, FWUPD_VERSION_FORMAT_TRIPLET) < 0) {
|
|
g_warning("ModemManager %s is available, but need at least %s",
|
|
version,
|
|
MM_REQUIRED_VERSION);
|
|
return;
|
|
}
|
|
|
|
g_debug("ModemManager %s is available", version);
|
|
|
|
g_signal_connect(G_DBUS_OBJECT_MANAGER(priv->manager),
|
|
"object-added",
|
|
G_CALLBACK(fu_plugin_mm_device_added_cb),
|
|
plugin);
|
|
g_signal_connect(G_DBUS_OBJECT_MANAGER(priv->manager),
|
|
"object-removed",
|
|
G_CALLBACK(fu_plugin_mm_device_removed_cb),
|
|
plugin);
|
|
|
|
list = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(priv->manager));
|
|
for (GList *l = list; l != NULL; l = g_list_next(l)) {
|
|
MMObject *modem = MM_OBJECT(l->data);
|
|
fu_plugin_mm_device_add(plugin, modem);
|
|
g_object_unref(modem);
|
|
}
|
|
g_list_free(list);
|
|
|
|
priv->manager_ready = TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_name_owner_updated(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_autofree gchar *name_owner = NULL;
|
|
name_owner = g_dbus_object_manager_client_get_name_owner(
|
|
G_DBUS_OBJECT_MANAGER_CLIENT(priv->manager));
|
|
if (name_owner != NULL)
|
|
fu_plugin_mm_setup_manager(plugin);
|
|
else
|
|
fu_plugin_mm_teardown_manager(plugin);
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_signal_connect_swapped(MM_MANAGER(priv->manager),
|
|
"notify::name-owner",
|
|
G_CALLBACK(fu_plugin_mm_name_owner_updated),
|
|
plugin);
|
|
fu_plugin_mm_name_owner_updated(plugin);
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_modem_power_changed_cb(GFileMonitor *monitor,
|
|
GFile *file,
|
|
GFile *other_file,
|
|
GFileMonitorEvent event_type,
|
|
gpointer user_data)
|
|
{
|
|
FuPlugin *plugin = FU_PLUGIN(user_data);
|
|
GPtrArray *devices = fu_plugin_get_devices(plugin);
|
|
for (guint i = 0; i < devices->len; i++) {
|
|
FuDevice *device = g_ptr_array_index(devices, i);
|
|
fu_plugin_mm_ensure_modem_power_inhibit(plugin, device);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_startup(FuPlugin *plugin, FuProgress *progress, GError **error)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_autoptr(GDBusConnection) connection = NULL;
|
|
g_autoptr(GFile) file = g_file_new_for_path(MODEM_POWER_SYSFS_PATH);
|
|
|
|
connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error);
|
|
if (connection == NULL)
|
|
return FALSE;
|
|
priv->manager = mm_manager_new_sync(connection,
|
|
G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
|
|
NULL,
|
|
error);
|
|
if (priv->manager == NULL)
|
|
return FALSE;
|
|
|
|
/* detect presence of unsupported modem-power driver */
|
|
priv->modem_power_monitor = g_file_monitor(file, G_FILE_MONITOR_NONE, NULL, error);
|
|
if (priv->modem_power_monitor == NULL)
|
|
return FALSE;
|
|
g_signal_connect(priv->modem_power_monitor,
|
|
"changed",
|
|
G_CALLBACK(fu_plugin_mm_modem_power_changed_cb),
|
|
plugin);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_init(FuPlugin *plugin)
|
|
{
|
|
fu_plugin_alloc_data(plugin, sizeof(FuPluginData));
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_destroy(FuPlugin *plugin)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
|
|
fu_plugin_mm_uninhibit_device(plugin);
|
|
|
|
if (priv->udev_timeout_id)
|
|
g_source_remove(priv->udev_timeout_id);
|
|
if (priv->udev_client)
|
|
g_object_unref(priv->udev_client);
|
|
if (priv->manager != NULL)
|
|
g_object_unref(priv->manager);
|
|
if (priv->modem_power_monitor != NULL)
|
|
g_object_unref(priv->modem_power_monitor);
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_detach(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuPluginData *priv = fu_plugin_get_data(plugin);
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
/* open device */
|
|
locker = fu_device_locker_new(device, error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* inhibit device and track it inside the plugin, not bound to the
|
|
* lifetime of the FuMmDevice, because that object will only exist for
|
|
* as long as the ModemManager device exists, and inhibiting will
|
|
* implicitly remove the device from ModemManager. */
|
|
if (priv->shadow_device == NULL) {
|
|
if (!fu_plugin_mm_inhibit_device(plugin, device, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* reset */
|
|
if (!fu_device_detach_full(device, progress, error)) {
|
|
fu_plugin_mm_uninhibit_device(plugin);
|
|
return FALSE;
|
|
}
|
|
|
|
/* note: wait for replug set by device if it really needs it */
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_plugin_mm_device_attach_finished(gpointer user_data)
|
|
{
|
|
FuPlugin *plugin = FU_PLUGIN(user_data);
|
|
fu_plugin_mm_uninhibit_device(plugin);
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_attach(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
/* open device */
|
|
locker = fu_device_locker_new(device, error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* schedule device attach asynchronously, which is extremely important
|
|
* so that engine can setup the device "waiting" logic before the actual
|
|
* attach procedure happens (which will reset the module if it worked
|
|
* properly) */
|
|
if (!fu_device_attach_full(device, progress, error))
|
|
return FALSE;
|
|
|
|
/* this signal will always be emitted asynchronously */
|
|
g_signal_connect_swapped(FU_DEVICE(device),
|
|
"attach-finished",
|
|
G_CALLBACK(fu_plugin_mm_device_attach_finished),
|
|
plugin);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_plugin_mm_backend_device_added(FuPlugin *plugin, FuDevice *device, GError **error)
|
|
{
|
|
FuDevice *device_tmp;
|
|
g_autoptr(GUdevDevice) udev_device = NULL;
|
|
|
|
/* interesting device? */
|
|
if (!FU_IS_USB_DEVICE(device))
|
|
return TRUE;
|
|
|
|
/* look up the FuMmDevice for the USB device that just appeared */
|
|
udev_device = fu_usb_device_find_udev_device(FU_USB_DEVICE(device), error);
|
|
if (udev_device == NULL)
|
|
return FALSE;
|
|
device_tmp = fu_plugin_cache_lookup(plugin, g_udev_device_get_sysfs_path(udev_device));
|
|
if (device_tmp == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"%s not added by ModemManager",
|
|
g_udev_device_get_sysfs_path(udev_device));
|
|
return FALSE;
|
|
}
|
|
fu_mm_device_set_usb_device(FU_MM_DEVICE(device_tmp), FU_USB_DEVICE(device));
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs)
|
|
{
|
|
vfuncs->build_hash = FU_BUILD_HASH;
|
|
vfuncs->load = fu_plugin_mm_load;
|
|
vfuncs->init = fu_plugin_mm_init;
|
|
vfuncs->destroy = fu_plugin_mm_destroy;
|
|
vfuncs->startup = fu_plugin_mm_startup;
|
|
vfuncs->coldplug = fu_plugin_mm_coldplug;
|
|
vfuncs->attach = fu_plugin_mm_attach;
|
|
vfuncs->detach = fu_plugin_mm_detach;
|
|
vfuncs->backend_device_added = fu_plugin_mm_backend_device_added;
|
|
}
|