fwupd/libfwupdplugin/fu-udev-device.c

2236 lines
59 KiB
C

/*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuUdevDevice"
#include "config.h"
#include <fcntl.h>
#include <string.h>
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_IOCTL_H
#include <sys/ioctl.h>
#endif
#include <glib/gstdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "fu-device-private.h"
#include "fu-i2c-device.h"
#include "fu-string.h"
#include "fu-udev-device-private.h"
/**
* FuUdevDevice:
*
* A UDev device, typically only available on Linux.
*
* See also: [class@FuDevice]
*/
typedef struct {
GUdevDevice *udev_device;
guint16 vendor;
guint16 model;
guint16 subsystem_vendor;
guint16 subsystem_model;
guint8 revision;
gchar *subsystem;
gchar *bind_id;
gchar *driver;
gchar *device_file;
gint fd;
FuUdevDeviceFlags flags;
} FuUdevDevicePrivate;
G_DEFINE_TYPE_WITH_PRIVATE(FuUdevDevice, fu_udev_device, FU_TYPE_DEVICE)
enum {
PROP_0,
PROP_UDEV_DEVICE,
PROP_SUBSYSTEM,
PROP_DRIVER,
PROP_DEVICE_FILE,
PROP_BIND_ID,
PROP_LAST
};
enum { SIGNAL_CHANGED, SIGNAL_LAST };
static guint signals[SIGNAL_LAST] = {0};
#define GET_PRIVATE(o) (fu_udev_device_get_instance_private(o))
/**
* fu_udev_device_emit_changed:
* @self: a #FuUdevDevice
*
* Emits the ::changed signal for the object.
*
* Since: 1.1.2
**/
void
fu_udev_device_emit_changed(FuUdevDevice *self)
{
g_autoptr(GError) error = NULL;
g_return_if_fail(FU_IS_UDEV_DEVICE(self));
g_debug("FuUdevDevice emit changed");
if (!fu_device_rescan(FU_DEVICE(self), &error))
g_debug("%s", error->message);
g_signal_emit(self, signals[SIGNAL_CHANGED], 0);
}
static guint32
fu_udev_device_get_sysfs_attr_as_uint16(GUdevDevice *udev_device, const gchar *name)
{
#ifdef HAVE_GUDEV
const gchar *tmp;
guint64 tmp64 = 0;
g_autoptr(GError) error_local = NULL;
tmp = g_udev_device_get_sysfs_attr(udev_device, name);
if (tmp == NULL)
return G_MAXUINT16;
if (!fu_strtoull(tmp, &tmp64, 0, G_MAXUINT32, &error_local)) {
g_warning("reading %s for %s was invalid: %s", name, tmp, error_local->message);
return G_MAXUINT16;
}
return tmp64;
#else
return G_MAXUINT16;
#endif
}
static guint8
fu_udev_device_get_sysfs_attr_as_uint8(GUdevDevice *udev_device, const gchar *name)
{
#ifdef HAVE_GUDEV
const gchar *tmp;
guint64 tmp64 = 0;
g_autoptr(GError) error_local = NULL;
tmp = g_udev_device_get_sysfs_attr(udev_device, name);
if (tmp == NULL)
return G_MAXUINT8;
if (!fu_strtoull(tmp, &tmp64, 0, G_MAXUINT8, &error_local)) {
g_warning("reading %s for %s was invalid: %s",
name,
g_udev_device_get_sysfs_path(udev_device),
error_local->message);
return G_MAXUINT8;
}
return tmp64;
#else
return G_MAXUINT8;
#endif
}
#ifdef HAVE_GUDEV
static void
fu_udev_device_to_string_raw(GUdevDevice *udev_device, guint idt, GString *str)
{
const gchar *const *keys;
if (udev_device == NULL)
return;
keys = g_udev_device_get_property_keys(udev_device);
for (guint i = 0; keys[i] != NULL; i++) {
fu_string_append(str,
idt,
keys[i],
g_udev_device_get_property(udev_device, keys[i]));
}
keys = g_udev_device_get_sysfs_attr_keys(udev_device);
for (guint i = 0; keys[i] != NULL; i++) {
fu_string_append(str,
idt,
keys[i],
g_udev_device_get_sysfs_attr(udev_device, keys[i]));
}
}
#endif
static void
fu_udev_device_to_string(FuDevice *device, guint idt, GString *str)
{
#ifdef HAVE_GUDEV
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
if (priv->udev_device != NULL) {
fu_string_append(str,
idt,
"SysfsPath",
g_udev_device_get_sysfs_path(priv->udev_device));
fu_string_append(str, idt, "Subsystem", priv->subsystem);
if (priv->driver != NULL)
fu_string_append(str, idt, "Driver", priv->driver);
if (priv->bind_id != NULL)
fu_string_append(str, idt, "BindId", priv->bind_id);
if (priv->device_file != NULL)
fu_string_append(str, idt, "DeviceFile", priv->device_file);
}
if (g_getenv("FU_UDEV_DEVICE_DEBUG") != NULL) {
g_autoptr(GUdevDevice) udev_parent = NULL;
fu_udev_device_to_string_raw(priv->udev_device, idt, str);
udev_parent = g_udev_device_get_parent(priv->udev_device);
if (udev_parent != NULL) {
fu_string_append(str, idt, "Parent", NULL);
fu_udev_device_to_string_raw(udev_parent, idt + 1, str);
}
}
#endif
}
static gboolean
fu_udev_device_ensure_bind_id(FuUdevDevice *self, GError **error)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* sanity check */
if (priv->bind_id != NULL)
return TRUE;
#ifdef HAVE_GUDEV
/* automatically set the bind ID from the subsystem */
if (g_strcmp0(priv->subsystem, "pci") == 0) {
priv->bind_id =
g_strdup(g_udev_device_get_property(priv->udev_device, "PCI_SLOT_NAME"));
return TRUE;
}
if (g_strcmp0(priv->subsystem, "hid") == 0) {
priv->bind_id = g_strdup(g_udev_device_get_property(priv->udev_device, "HID_PHYS"));
return TRUE;
}
if (g_strcmp0(priv->subsystem, "usb") == 0) {
priv->bind_id =
g_path_get_basename(g_udev_device_get_sysfs_path(priv->udev_device));
return TRUE;
}
#endif
/* nothing found automatically */
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"cannot derive bind-id from subsystem %s",
priv->subsystem);
return FALSE;
}
static void
fu_udev_device_set_subsystem(FuUdevDevice *self, const gchar *subsystem)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* not changed */
if (g_strcmp0(priv->subsystem, subsystem) == 0)
return;
g_free(priv->subsystem);
priv->subsystem = g_strdup(subsystem);
g_object_notify(G_OBJECT(self), "subsystem");
}
/**
* fu_udev_device_set_bind_id:
* @self: a #FuUdevDevice
* @bind_id: a bind-id string, e.g. `pci:0:0:1`
*
* Sets the device ID used for binding the device, e.g. `pci:1:2:3`
*
* Since: 1.7.2
**/
void
fu_udev_device_set_bind_id(FuUdevDevice *self, const gchar *bind_id)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* not changed */
if (g_strcmp0(priv->bind_id, bind_id) == 0)
return;
g_free(priv->bind_id);
priv->bind_id = g_strdup(bind_id);
g_object_notify(G_OBJECT(self), "bind-id");
}
static void
fu_udev_device_set_driver(FuUdevDevice *self, const gchar *driver)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* not changed */
if (g_strcmp0(priv->driver, driver) == 0)
return;
g_free(priv->driver);
priv->driver = g_strdup(driver);
g_object_notify(G_OBJECT(self), "driver");
}
static void
fu_udev_device_set_device_file(FuUdevDevice *self, const gchar *device_file)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* not changed */
if (g_strcmp0(priv->device_file, device_file) == 0)
return;
g_free(priv->device_file);
priv->device_file = g_strdup(device_file);
g_object_notify(G_OBJECT(self), "device-file");
}
#ifdef HAVE_GUDEV
static const gchar *
fu_udev_device_get_vendor_fallback(GUdevDevice *udev_device)
{
const gchar *tmp;
tmp = g_udev_device_get_property(udev_device, "ID_VENDOR_FROM_DATABASE");
if (tmp != NULL)
return tmp;
tmp = g_udev_device_get_property(udev_device, "ID_VENDOR");
if (tmp != NULL)
return tmp;
return NULL;
}
#endif
#ifdef HAVE_GUDEV
static gboolean
fu_udev_device_probe_serio(FuUdevDevice *self, GError **error)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
/* firmware ID */
tmp = g_udev_device_get_property(priv->udev_device, "SERIO_FIRMWARE_ID");
if (tmp != NULL) {
/* this prefix is not useful */
if (g_str_has_prefix(tmp, "PNP: "))
tmp += 5;
fu_device_add_instance_strsafe(FU_DEVICE(self), "FWID", tmp);
if (!fu_device_build_instance_id(FU_DEVICE(self), error, "SERIO", "FWID", NULL))
return FALSE;
}
return TRUE;
}
#endif
static gboolean
fu_udev_device_probe(FuDevice *device, GError **error)
{
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
#ifdef HAVE_GUDEV
const gchar *tmp;
g_autofree gchar *subsystem = NULL;
g_autoptr(GUdevDevice) udev_parent = NULL;
g_autoptr(GUdevDevice) parent_i2c = NULL;
#endif
/* nothing to do */
if (priv->udev_device == NULL)
return TRUE;
/* set ven:dev:rev */
priv->vendor = fu_udev_device_get_sysfs_attr_as_uint16(priv->udev_device, "vendor");
priv->model = fu_udev_device_get_sysfs_attr_as_uint16(priv->udev_device, "device");
priv->revision = fu_udev_device_get_sysfs_attr_as_uint8(priv->udev_device, "revision");
priv->subsystem_vendor =
fu_udev_device_get_sysfs_attr_as_uint16(priv->udev_device, "subsystem_vendor");
priv->subsystem_model =
fu_udev_device_get_sysfs_attr_as_uint16(priv->udev_device, "subsystem_device");
#ifdef HAVE_GUDEV
/* fallback to the parent */
udev_parent = g_udev_device_get_parent(priv->udev_device);
if (udev_parent != NULL && priv->flags & FU_UDEV_DEVICE_FLAG_VENDOR_FROM_PARENT &&
priv->vendor == 0x0 && priv->model == 0x0 && priv->revision == 0x0) {
priv->vendor = fu_udev_device_get_sysfs_attr_as_uint16(udev_parent, "vendor");
priv->model = fu_udev_device_get_sysfs_attr_as_uint16(udev_parent, "device");
priv->revision = fu_udev_device_get_sysfs_attr_as_uint8(udev_parent, "revision");
priv->subsystem_vendor =
fu_udev_device_get_sysfs_attr_as_uint16(udev_parent, "subsystem_vendor");
priv->subsystem_model =
fu_udev_device_get_sysfs_attr_as_uint16(udev_parent, "subsystem_device");
}
/* hidraw helpfully encodes the information in a different place */
if (udev_parent != NULL && priv->vendor == 0x0 && priv->model == 0x0 &&
priv->revision == 0x0 && g_strcmp0(priv->subsystem, "hidraw") == 0) {
tmp = g_udev_device_get_property(udev_parent, "HID_ID");
if (tmp != NULL) {
g_auto(GStrv) split = g_strsplit(tmp, ":", -1);
if (g_strv_length(split) == 3) {
guint64 val = g_ascii_strtoull(split[1], NULL, 16);
if (val > G_MAXUINT16) {
g_warning("reading %s for %s overflowed",
split[1],
g_udev_device_get_sysfs_path(priv->udev_device));
} else {
priv->vendor = val;
}
val = g_ascii_strtoull(split[2], NULL, 16);
if (val > G_MAXUINT32) {
g_warning("reading %s for %s overflowed",
split[2],
g_udev_device_get_sysfs_path(priv->udev_device));
} else {
priv->model = val;
}
}
}
tmp = g_udev_device_get_property(udev_parent, "HID_NAME");
if (tmp != NULL) {
if (fu_device_get_name(device) == NULL)
fu_device_set_name(device, tmp);
}
}
/* set the version if the revision has been set */
if (fu_device_get_version(device) == NULL &&
fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN) {
if (priv->revision != 0x00 && priv->revision != 0xFF) {
g_autofree gchar *version = g_strdup_printf("%02x", priv->revision);
fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN);
fu_device_set_version(device, version);
}
}
/* set model */
if (fu_device_get_name(device) == NULL) {
tmp = g_udev_device_get_property(priv->udev_device, "ID_MODEL_FROM_DATABASE");
if (tmp == NULL)
tmp = g_udev_device_get_property(priv->udev_device, "ID_MODEL");
if (tmp == NULL)
tmp = g_udev_device_get_property(priv->udev_device,
"ID_PCI_CLASS_FROM_DATABASE");
if (tmp != NULL)
fu_device_set_name(device, tmp);
}
/* set vendor */
if (fu_device_get_vendor(device) == NULL) {
tmp = fu_udev_device_get_vendor_fallback(priv->udev_device);
if (tmp != NULL)
fu_device_set_vendor(device, tmp);
}
/* try harder to find a vendor name the user will recognize */
if (priv->flags & FU_UDEV_DEVICE_FLAG_VENDOR_FROM_PARENT && udev_parent != NULL &&
fu_device_get_vendor(device) == NULL) {
g_autoptr(GUdevDevice) device_tmp = g_object_ref(udev_parent);
for (guint i = 0; i < 0xff; i++) {
g_autoptr(GUdevDevice) parent = NULL;
tmp = fu_udev_device_get_vendor_fallback(device_tmp);
if (tmp != NULL) {
fu_device_set_vendor(device, tmp);
break;
}
parent = g_udev_device_get_parent(device_tmp);
if (parent == NULL)
break;
g_set_object(&device_tmp, parent);
}
}
/* set serial */
if (!fu_device_has_internal_flag(device, FU_DEVICE_INTERNAL_FLAG_NO_SERIAL_NUMBER) &&
fu_device_get_serial(device) == NULL) {
tmp = g_udev_device_get_property(priv->udev_device, "ID_SERIAL_SHORT");
if (tmp == NULL)
tmp = g_udev_device_get_property(priv->udev_device, "ID_SERIAL");
if (tmp != NULL)
fu_device_set_serial(device, tmp);
}
/* set revision */
if (fu_device_get_version(device) == NULL &&
fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN) {
tmp = g_udev_device_get_property(priv->udev_device, "ID_REVISION");
if (tmp != NULL)
fu_device_set_version(device, tmp);
}
/* set vendor ID */
subsystem = g_ascii_strup(g_udev_device_get_subsystem(priv->udev_device), -1);
if (subsystem != NULL && priv->vendor != 0x0000 && priv->vendor != 0xFFFF) {
g_autofree gchar *vendor_id = NULL;
vendor_id = g_strdup_printf("%s:0x%04X", subsystem, (guint)priv->vendor);
fu_device_add_vendor_id(device, vendor_id);
}
/* add GUIDs in order of priority */
if (priv->vendor != 0x0000 && priv->vendor != 0xFFFF)
fu_device_add_instance_u16(device, "VEN", priv->vendor);
if (priv->model != 0x0000 && priv->model != 0xFFFF)
fu_device_add_instance_u16(device, "DEV", priv->model);
if (priv->subsystem_vendor != 0x0000 && priv->subsystem_vendor != 0xFFFF &&
priv->subsystem_model != 0x0000 && priv->subsystem_model != 0xFFFF) {
g_autofree gchar *subsys =
g_strdup_printf("%04X%04X", priv->subsystem_vendor, priv->subsystem_model);
fu_device_add_instance_str(device, "SUBSYS", subsys);
}
if (priv->revision != 0xFF)
fu_device_add_instance_u8(device, "REV", priv->revision);
fu_device_build_instance_id_quirk(device, NULL, subsystem, "VEN", NULL);
fu_device_build_instance_id(device, NULL, subsystem, "VEN", "DEV", NULL);
fu_device_build_instance_id(device, NULL, subsystem, "VEN", "DEV", "REV", NULL);
fu_device_build_instance_id(device, NULL, subsystem, "VEN", "DEV", "SUBSYS", NULL);
fu_device_build_instance_id(device, NULL, subsystem, "VEN", "DEV", "SUBSYS", "REV", NULL);
/* add device class */
tmp = g_udev_device_get_sysfs_attr(priv->udev_device, "class");
if (tmp != NULL && g_str_has_prefix(tmp, "0x"))
tmp += 2;
fu_device_add_instance_strup(device, "CLASS", tmp);
fu_device_build_instance_id_quirk(device, NULL, subsystem, "VEN", "CLASS", NULL);
/* add devtype */
fu_device_add_instance_strup(device, "TYPE", g_udev_device_get_devtype(priv->udev_device));
fu_device_build_instance_id_quirk(device, NULL, subsystem, "TYPE", NULL);
/* add the driver */
fu_device_add_instance_str(device, "DRIVER", priv->driver);
fu_device_build_instance_id_quirk(device, NULL, subsystem, "DRIVER", NULL);
/* add subsystem to match in plugins */
if (subsystem != NULL) {
fu_device_add_instance_id_full(device,
subsystem,
FU_DEVICE_INSTANCE_FLAG_ONLY_QUIRKS);
}
/* add firmware_id */
if (g_strcmp0(g_udev_device_get_subsystem(priv->udev_device), "serio") == 0) {
if (!fu_udev_device_probe_serio(self, error))
return FALSE;
}
/* determine if we're wired internally */
parent_i2c = g_udev_device_get_parent_with_subsystem(priv->udev_device, "i2c", NULL);
if (parent_i2c != NULL)
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL);
#endif
/* success */
return TRUE;
}
#ifdef HAVE_GUDEV
static gchar *
fu_udev_device_get_miscdev0(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *fn;
g_autofree gchar *miscdir = NULL;
g_autoptr(GDir) dir = NULL;
miscdir = g_build_filename(g_udev_device_get_sysfs_path(priv->udev_device), "misc", NULL);
dir = g_dir_open(miscdir, 0, NULL);
if (dir == NULL)
return NULL;
fn = g_dir_read_name(dir);
if (fn == NULL)
return NULL;
return g_strdup_printf("/dev/%s", fn);
}
#endif
/**
* fu_udev_device_set_dev:
* @self: a #FuUdevDevice
* @udev_device: a #GUdevDevice
*
* Sets the #GUdevDevice. This may need to be used to replace the actual device
* used for reads and writes before the device is probed.
*
* Since: 1.6.2
**/
void
fu_udev_device_set_dev(FuUdevDevice *self, GUdevDevice *udev_device)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
#ifdef HAVE_GUDEV
const gchar *summary;
#endif
g_return_if_fail(FU_IS_UDEV_DEVICE(self));
#ifdef HAVE_GUDEV
/* the net subsystem is not a real hardware class */
if (udev_device != NULL &&
g_strcmp0(g_udev_device_get_subsystem(udev_device), "net") == 0) {
g_autoptr(GUdevDevice) udev_device_phys = NULL;
udev_device_phys = g_udev_device_get_parent(udev_device);
g_set_object(&priv->udev_device, udev_device_phys);
fu_device_set_metadata(FU_DEVICE(self),
"ParentSubsystem",
g_udev_device_get_subsystem(udev_device));
} else {
g_set_object(&priv->udev_device, udev_device);
}
#else
g_set_object(&priv->udev_device, udev_device);
#endif
/* set new device */
if (priv->udev_device == NULL)
return;
#ifdef HAVE_GUDEV
fu_udev_device_set_subsystem(self, g_udev_device_get_subsystem(priv->udev_device));
fu_udev_device_set_driver(self, g_udev_device_get_driver(priv->udev_device));
fu_udev_device_set_device_file(self, g_udev_device_get_device_file(priv->udev_device));
/* so we can display something sensible for unclaimed devices */
fu_device_set_backend_id(FU_DEVICE(self), g_udev_device_get_sysfs_path(priv->udev_device));
/* fall back to the first thing handled by misc drivers */
if (priv->device_file == NULL) {
/* perhaps we should unconditionally fall back? or perhaps
* require FU_UDEV_DEVICE_FLAG_FALLBACK_MISC... */
if (g_strcmp0(priv->subsystem, "serio") == 0)
priv->device_file = fu_udev_device_get_miscdev0(self);
if (priv->device_file != NULL)
g_debug("falling back to misc %s", priv->device_file);
}
/* try to get one line summary */
summary = g_udev_device_get_sysfs_attr(priv->udev_device, "description");
if (summary == NULL) {
g_autoptr(GUdevDevice) parent = NULL;
parent = g_udev_device_get_parent(priv->udev_device);
if (parent != NULL)
summary = g_udev_device_get_sysfs_attr(parent, "description");
}
if (summary != NULL)
fu_device_set_summary(FU_DEVICE(self), summary);
#endif
}
/**
* fu_udev_device_get_slot_depth:
* @self: a #FuUdevDevice
* @subsystem: a subsystem
*
* Determine how far up a chain a given device is
*
* Returns: unsigned integer
*
* Since: 1.2.4
**/
guint
fu_udev_device_get_slot_depth(FuUdevDevice *self, const gchar *subsystem)
{
#ifdef HAVE_GUDEV
GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(self));
g_autoptr(GUdevDevice) device_tmp = NULL;
device_tmp = g_udev_device_get_parent_with_subsystem(udev_device, subsystem, NULL);
if (device_tmp == NULL)
return 0;
for (guint i = 0; i < 0xff; i++) {
g_autoptr(GUdevDevice) parent = g_udev_device_get_parent(device_tmp);
if (parent == NULL)
return i;
g_set_object(&device_tmp, parent);
}
#endif
return 0;
}
static gboolean
fu_udev_device_unbind_driver(FuDevice *device, GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_autofree gchar *fn = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GOutputStream) stream = NULL;
/* is already unbound */
fn = g_build_filename(g_udev_device_get_sysfs_path(priv->udev_device),
"driver",
"unbind",
NULL);
if (!g_file_test(fn, G_FILE_TEST_EXISTS))
return TRUE;
/* write bus ID to file */
if (!fu_udev_device_ensure_bind_id(self, error))
return FALSE;
file = g_file_new_for_path(fn);
stream =
G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error));
if (stream == NULL)
return FALSE;
return g_output_stream_write_all(stream,
priv->bind_id,
strlen(priv->bind_id),
NULL,
NULL,
error);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"driver unbinding not supported");
return FALSE;
#endif
}
static gboolean
fu_udev_device_bind_driver(FuDevice *device,
const gchar *subsystem,
const gchar *driver,
GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_autofree gchar *driver_safe = g_strdup(driver);
g_autofree gchar *fn = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GOutputStream) stream = NULL;
/* copy the logic from modprobe */
g_strdelimit(driver_safe, "-", '_');
/* driver exists */
fn = g_strdup_printf("/sys/module/%s/drivers/%s:%s/bind",
driver_safe,
subsystem,
driver_safe);
if (!g_file_test(fn, G_FILE_TEST_EXISTS)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"cannot bind with %s:%s",
subsystem,
driver);
return FALSE;
}
/* write bus ID to file */
if (!fu_udev_device_ensure_bind_id(self, error))
return FALSE;
if (priv->bind_id == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"bind-id not set for subsystem %s",
priv->subsystem);
return FALSE;
}
file = g_file_new_for_path(fn);
stream =
G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error));
if (stream == NULL)
return FALSE;
return g_output_stream_write_all(stream,
priv->bind_id,
strlen(priv->bind_id),
NULL,
NULL,
error);
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"driver binding not supported on Windows");
return FALSE;
#endif
}
static void
fu_udev_device_incorporate(FuDevice *self, FuDevice *donor)
{
FuUdevDevice *uself = FU_UDEV_DEVICE(self);
FuUdevDevice *udonor = FU_UDEV_DEVICE(donor);
FuUdevDevicePrivate *priv = GET_PRIVATE(uself);
FuUdevDevicePrivate *priv_donor = GET_PRIVATE(udonor);
g_return_if_fail(FU_IS_UDEV_DEVICE(self));
g_return_if_fail(FU_IS_UDEV_DEVICE(donor));
fu_udev_device_set_dev(uself, fu_udev_device_get_dev(udonor));
if (priv->device_file == NULL) {
fu_udev_device_set_subsystem(uself, fu_udev_device_get_subsystem(udonor));
fu_udev_device_set_bind_id(uself, fu_udev_device_get_bind_id(udonor));
fu_udev_device_set_device_file(uself, fu_udev_device_get_device_file(udonor));
fu_udev_device_set_driver(uself, fu_udev_device_get_driver(udonor));
}
if (priv->vendor == 0x0 && priv_donor->vendor != 0x0)
priv->vendor = priv_donor->vendor;
if (priv->model == 0x0 && priv_donor->model != 0x0)
priv->model = priv_donor->model;
if (priv->subsystem_vendor == 0x0 && priv_donor->subsystem_vendor != 0x0)
priv->subsystem_vendor = priv_donor->subsystem_vendor;
if (priv->subsystem_model == 0x0 && priv_donor->subsystem_model != 0x0)
priv->subsystem_model = priv_donor->subsystem_model;
if (priv->revision == 0x0 && priv_donor->revision != 0x0)
priv->revision = priv_donor->revision;
}
/**
* fu_udev_device_get_dev:
* @self: a #FuUdevDevice
*
* Gets the #GUdevDevice.
*
* Returns: (transfer none): a #GUdevDevice, or %NULL
*
* Since: 1.1.2
**/
GUdevDevice *
fu_udev_device_get_dev(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
return priv->udev_device;
}
/**
* fu_udev_device_get_subsystem:
* @self: a #FuUdevDevice
*
* Gets the device subsystem, e.g. `pci`
*
* Returns: a subsystem, or NULL if unset or invalid
*
* Since: 1.1.2
**/
const gchar *
fu_udev_device_get_subsystem(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
return priv->subsystem;
}
/**
* fu_udev_device_get_bind_id:
* @self: a #FuUdevDevice
*
* Gets the device ID used for binding the device, e.g. `pci:1:2:3`
*
* Returns: a bind_id, or NULL if unset or invalid
*
* Since: 1.7.2
**/
const gchar *
fu_udev_device_get_bind_id(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
fu_udev_device_ensure_bind_id(self, NULL);
return priv->bind_id;
}
/**
* fu_udev_device_get_driver:
* @self: a #FuUdevDevice
*
* Gets the device driver, e.g. `psmouse`.
*
* Returns: a subsystem, or NULL if unset or invalid
*
* Since: 1.5.3
**/
const gchar *
fu_udev_device_get_driver(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
return priv->driver;
}
/**
* fu_udev_device_get_device_file:
* @self: a #FuUdevDevice
*
* Gets the device node.
*
* Returns: a device file, or NULL if unset
*
* Since: 1.3.1
**/
const gchar *
fu_udev_device_get_device_file(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
return priv->device_file;
}
/**
* fu_udev_device_get_sysfs_path:
* @self: a #FuUdevDevice
*
* Gets the device sysfs path, e.g. `/sys/devices/pci0000:00/0000:00:14.0`.
*
* Returns: a local path, or NULL if unset or invalid
*
* Since: 1.1.2
**/
const gchar *
fu_udev_device_get_sysfs_path(FuUdevDevice *self)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
if (priv->udev_device != NULL)
return g_udev_device_get_sysfs_path(priv->udev_device);
#endif
return NULL;
}
/**
* fu_udev_device_get_number:
* @self: a #FuUdevDevice
*
* Gets the device number, if any.
*
* Returns: integer, 0 if the data is unavailable, or %G_MAXUINT64 if the
* feature is not available
*
* Since: 1.5.0
**/
guint64
fu_udev_device_get_number(FuUdevDevice *self)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0);
if (priv->udev_device != NULL) {
guint64 tmp = 0;
g_autoptr(GError) error_local = NULL;
if (!fu_strtoull(g_udev_device_get_number(priv->udev_device),
&tmp,
0x0,
G_MAXUINT64,
&error_local)) {
g_warning("failed to convert udev number: %s", error_local->message);
return G_MAXUINT64;
}
return tmp;
}
#endif
return G_MAXUINT64;
}
/**
* fu_udev_device_get_vendor:
* @self: a #FuUdevDevice
*
* Gets the device vendor code.
*
* Returns: a vendor code, or 0 if unset or invalid
*
* Since: 1.1.2
**/
guint32
fu_udev_device_get_vendor(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0x0000);
return priv->vendor;
}
/**
* fu_udev_device_get_model:
* @self: a #FuUdevDevice
*
* Gets the device device code.
*
* Returns: a vendor code, or 0 if unset or invalid
*
* Since: 1.1.2
**/
guint32
fu_udev_device_get_model(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0x0000);
return priv->model;
}
/**
* fu_udev_device_get_subsystem_vendor:
* @self: a #FuUdevDevice
*
* Gets the device subsystem vendor code.
*
* Returns: a vendor code, or 0 if unset or invalid
*
* Since: 1.5.0
**/
guint32
fu_udev_device_get_subsystem_vendor(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0x0000);
return priv->subsystem_vendor;
}
/**
* fu_udev_device_get_subsystem_model:
* @self: a #FuUdevDevice
*
* Gets the device subsystem model code.
*
* Returns: a vendor code, or 0 if unset or invalid
*
* Since: 1.5.0
**/
guint32
fu_udev_device_get_subsystem_model(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0x0000);
return priv->subsystem_model;
}
/**
* fu_udev_device_get_revision:
* @self: a #FuUdevDevice
*
* Gets the device revision.
*
* Returns: a vendor code, or 0 if unset or invalid
*
* Since: 1.1.2
**/
guint8
fu_udev_device_get_revision(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0x00);
return priv->revision;
}
#ifdef HAVE_GUDEV
static gchar *
fu_udev_device_get_parent_subsystems(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
GString *str = g_string_new(NULL);
g_autoptr(GUdevDevice) udev_device = g_object_ref(priv->udev_device);
/* find subsystems of self and all parent devices */
if (priv->subsystem != NULL)
g_string_append_printf(str, "%s,", priv->subsystem);
while (TRUE) {
g_autoptr(GUdevDevice) parent = g_udev_device_get_parent(udev_device);
if (parent == NULL)
break;
if (g_udev_device_get_subsystem(parent) != NULL) {
g_string_append_printf(str, "%s,", g_udev_device_get_subsystem(parent));
}
g_set_object(&udev_device, g_steal_pointer(&parent));
}
if (str->len > 0)
g_string_truncate(str, str->len - 1);
return g_string_free(str, FALSE);
}
static gboolean
fu_udev_device_match_subsystem_devtype(GUdevDevice *udev_device,
const gchar *subsystem,
const gchar *devtype)
{
if (subsystem != NULL) {
if (g_strcmp0(g_udev_device_get_subsystem(udev_device), subsystem) != 0)
return FALSE;
}
if (devtype != NULL) {
if (g_strcmp0(g_udev_device_get_devtype(udev_device), devtype) != 0)
return FALSE;
}
return TRUE;
}
static GUdevDevice *
fu_udev_device_get_parent_with_subsystem_devtype(GUdevDevice *udev_device,
const gchar *subsystem,
const gchar *devtype)
{
g_autoptr(GUdevDevice) udev_device_tmp = g_object_ref(udev_device);
while (udev_device_tmp != NULL) {
g_autoptr(GUdevDevice) parent = NULL;
if (fu_udev_device_match_subsystem_devtype(udev_device_tmp, subsystem, devtype))
return g_object_ref(udev_device_tmp);
parent = g_udev_device_get_parent(udev_device_tmp);
g_set_object(&udev_device_tmp, parent);
}
return NULL;
}
#endif
/**
* fu_udev_device_set_physical_id:
* @self: a #FuUdevDevice
* @subsystems: a subsystem string, e.g. `pci,usb,scsi:scsi_target`
* @error: (nullable): optional return location for an error
*
* Sets the physical ID from the device subsystem. Plugins should choose the
* subsystem that is "deepest" in the udev tree, for instance choosing `usb`
* over `pci` for a mouse device.
*
* The devtype can also be specified for a specific device, which is useful when the
* subsystem alone is not enough to identify the physical device. e.g. ignoring the
* specific LUNs for a SCSI device.
*
* Returns: %TRUE if the physical device was set.
*
* Since: 1.1.2
**/
gboolean
fu_udev_device_set_physical_id(FuUdevDevice *self, const gchar *subsystems, GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
g_autofree gchar *physical_id = NULL;
g_autofree gchar *subsystem = NULL;
g_auto(GStrv) split = NULL;
g_autoptr(GUdevDevice) udev_device = NULL;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(subsystems != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* nothing to do */
if (priv->udev_device == NULL)
return TRUE;
/* look for each subsystem[:devtype] in turn */
split = g_strsplit(subsystems, ",", -1);
for (guint i = 0; split[i] != NULL; i++) {
g_auto(GStrv) subsys_devtype = g_strsplit(split[i], ":", 2);
/* matching on devtype is optional */
udev_device = fu_udev_device_get_parent_with_subsystem_devtype(priv->udev_device,
subsys_devtype[0],
subsys_devtype[1]);
if (udev_device != NULL) {
subsystem = g_strdup(subsys_devtype[0]);
break;
}
}
if (udev_device == NULL) {
g_autofree gchar *str = fu_udev_device_get_parent_subsystems(self);
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find device with subsystems %s, only got %s",
subsystems,
str);
return FALSE;
}
if (g_strcmp0(subsystem, "pci") == 0) {
tmp = g_udev_device_get_property(udev_device, "PCI_SLOT_NAME");
if (tmp == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find PCI_SLOT_NAME");
return FALSE;
}
physical_id = g_strdup_printf("PCI_SLOT_NAME=%s", tmp);
} else if (g_strcmp0(subsystem, "usb") == 0 || g_strcmp0(subsystem, "mmc") == 0 ||
g_strcmp0(subsystem, "i2c") == 0 || g_strcmp0(subsystem, "platform") == 0 ||
g_strcmp0(subsystem, "scsi") == 0 || g_strcmp0(subsystem, "mtd") == 0 ||
g_strcmp0(subsystem, "block") == 0 || g_strcmp0(subsystem, "gpio") == 0) {
tmp = g_udev_device_get_property(udev_device, "DEVPATH");
if (tmp == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find DEVPATH");
return FALSE;
}
physical_id = g_strdup_printf("DEVPATH=%s", tmp);
} else if (g_strcmp0(subsystem, "hid") == 0) {
tmp = g_udev_device_get_property(udev_device, "HID_PHYS");
if (tmp == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find HID_PHYS");
return FALSE;
}
physical_id = g_strdup_printf("HID_PHYS=%s", tmp);
} else if (g_strcmp0(subsystem, "tpm") == 0 ||
g_strcmp0(subsystem, "drm_dp_aux_dev") == 0) {
tmp = g_udev_device_get_property(udev_device, "DEVNAME");
if (tmp == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find DEVPATH");
return FALSE;
}
physical_id = g_strdup_printf("DEVNAME=%s", tmp);
} else {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"cannot handle subsystem %s",
subsystem);
return FALSE;
}
/* success */
fu_device_set_physical_id(FU_DEVICE(self), physical_id);
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <gudev.h> is unavailable");
return FALSE;
#endif
}
/**
* fu_udev_device_set_logical_id:
* @self: a #FuUdevDevice
* @subsystem: a subsystem string, e.g. `pci,usb`
* @error: (nullable): optional return location for an error
*
* Sets the logical ID from the device subsystem. Plugins should choose the
* subsystem that most relevant in the udev tree, for instance choosing `hid`
* over `usb` for a mouse device.
*
* Returns: %TRUE if the logical device was set.
*
* Since: 1.5.8
**/
gboolean
fu_udev_device_set_logical_id(FuUdevDevice *self, const gchar *subsystem, GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *tmp;
g_autofree gchar *logical_id = NULL;
g_autoptr(GUdevDevice) udev_device = NULL;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(subsystem != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* nothing to do */
if (priv->udev_device == NULL)
return TRUE;
/* find correct device matching subsystem */
if (g_strcmp0(priv->subsystem, subsystem) == 0) {
udev_device = g_object_ref(priv->udev_device);
} else {
udev_device =
g_udev_device_get_parent_with_subsystem(priv->udev_device, subsystem, NULL);
}
if (udev_device == NULL) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find device with subsystem %s",
subsystem);
return FALSE;
}
/* query each subsystem */
if (g_strcmp0(subsystem, "hid") == 0) {
tmp = g_udev_device_get_property(udev_device, "HID_UNIQ");
if (tmp == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"failed to find HID_UNIQ");
return FALSE;
}
logical_id = g_strdup_printf("HID_UNIQ=%s", tmp);
} else {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"cannot handle subsystem %s",
subsystem);
return FALSE;
}
/* success */
fu_device_set_logical_id(FU_DEVICE(self), logical_id);
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <gudev.h> is unavailable");
return FALSE;
#endif
}
/**
* fu_udev_device_get_fd:
* @self: a #FuUdevDevice
*
* Gets the file descriptor if the device is open.
*
* Returns: positive integer, or -1 if the device is not open
*
* Since: 1.3.3
**/
gint
fu_udev_device_get_fd(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), -1);
return priv->fd;
}
/**
* fu_udev_device_set_fd:
* @self: a #FuUdevDevice
* @fd: a valid file descriptor
*
* Replace the file descriptor to use when the device has already been opened.
* This object will automatically close() @fd when fu_device_close() is called.
*
* Since: 1.3.3
**/
void
fu_udev_device_set_fd(FuUdevDevice *self, gint fd)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_IS_UDEV_DEVICE(self));
if (priv->fd > 0)
close(priv->fd);
priv->fd = fd;
}
/**
* fu_udev_device_set_flags:
* @self: a #FuUdevDevice
* @flags: udev device flags, e.g. %FU_UDEV_DEVICE_FLAG_OPEN_READ
*
* Sets the parameters to use when opening the device.
*
* For example %FU_UDEV_DEVICE_FLAG_OPEN_READ means that fu_device_open()
* would use `O_RDONLY` rather than `O_RDWR` which is the default.
*
* Since: 1.3.6
**/
void
fu_udev_device_set_flags(FuUdevDevice *self, FuUdevDeviceFlags flags)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_IS_UDEV_DEVICE(self));
priv->flags = flags;
#ifdef HAVE_GUDEV
/* overwrite */
if (flags & FU_UDEV_DEVICE_FLAG_USE_CONFIG) {
g_free(priv->device_file);
priv->device_file =
g_build_filename(g_udev_device_get_sysfs_path(priv->udev_device),
"config",
NULL);
}
#endif
}
static gboolean
fu_udev_device_open(FuDevice *device, GError **error)
{
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* open device */
if (priv->device_file != NULL && priv->flags != FU_UDEV_DEVICE_FLAG_NONE) {
gint flags;
if (priv->flags & FU_UDEV_DEVICE_FLAG_OPEN_READ &&
priv->flags & FU_UDEV_DEVICE_FLAG_OPEN_WRITE) {
flags = O_RDWR;
} else if (priv->flags & FU_UDEV_DEVICE_FLAG_OPEN_WRITE) {
flags = O_WRONLY;
} else {
flags = O_RDONLY;
}
#ifdef O_NONBLOCK
if (priv->flags & FU_UDEV_DEVICE_FLAG_OPEN_NONBLOCK)
flags |= O_NONBLOCK;
#endif
#ifdef O_SYNC
if (priv->flags & FU_UDEV_DEVICE_FLAG_OPEN_SYNC)
flags |= O_SYNC;
#endif
priv->fd = g_open(priv->device_file, flags, 0);
if (priv->fd < 0) {
g_set_error(error,
G_IO_ERROR,
#ifdef HAVE_ERRNO_H
g_io_error_from_errno(errno),
#else
G_IO_ERROR_FAILED,
#endif
"failed to open %s: %s",
priv->device_file,
strerror(errno));
return FALSE;
}
}
/* success */
return TRUE;
}
static gboolean
fu_udev_device_rescan(FuDevice *device, GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *sysfs_path;
g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
g_autoptr(GUdevDevice) udev_device = NULL;
/* never set */
if (priv->udev_device == NULL) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"rescan with no previous device");
return FALSE;
}
sysfs_path = g_udev_device_get_sysfs_path(priv->udev_device);
udev_device = g_udev_client_query_by_sysfs_path(udev_client, sysfs_path);
if (udev_device == NULL) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"rescan could not find device %s",
sysfs_path);
return FALSE;
}
fu_udev_device_set_dev(self, udev_device);
fu_device_probe_invalidate(device);
#endif
return fu_device_probe(device, error);
}
static gboolean
fu_udev_device_close(FuDevice *device, GError **error)
{
FuUdevDevice *self = FU_UDEV_DEVICE(device);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
/* close device */
if (priv->fd > 0) {
if (!g_close(priv->fd, error))
return FALSE;
priv->fd = 0;
}
/* success */
return TRUE;
}
/**
* fu_udev_device_ioctl:
* @self: a #FuUdevDevice
* @request: request number
* @buf: a buffer to use, which *must* be large enough for the request
* @rc: (out) (nullable): the raw return value from the ioctl
* @timeout: timeout in ms for the retry action, see %FU_UDEV_DEVICE_FLAG_IOCTL_RETRY
* @error: (nullable): optional return location for an error
*
* Control a device using a low-level request.
*
* Returns: %TRUE for success
*
* Since: 1.8.2
**/
gboolean
fu_udev_device_ioctl(FuUdevDevice *self,
gulong request,
guint8 *buf,
gint *rc,
guint timeout,
GError **error)
{
#ifdef HAVE_IOCTL_H
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
gint rc_tmp;
g_autoptr(GTimer) timer = g_timer_new();
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(request != 0x0, FALSE);
g_return_val_if_fail(buf != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* not open! */
if (priv->fd == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"%s [%s] has not been opened",
fu_device_get_id(FU_DEVICE(self)),
fu_device_get_name(FU_DEVICE(self)));
return FALSE;
}
/* poll if required up to the timeout */
do {
rc_tmp = ioctl(priv->fd, request, buf);
if (rc_tmp >= 0)
break;
} while ((priv->flags & FU_UDEV_DEVICE_FLAG_IOCTL_RETRY) &&
(errno == EINTR || errno == EAGAIN) &&
g_timer_elapsed(timer, NULL) < timeout * 1000.f);
if (rc != NULL)
*rc = rc_tmp;
if (rc_tmp < 0) {
#ifdef HAVE_ERRNO_H
if (errno == EPERM) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_PERMISSION_DENIED,
"permission denied");
return FALSE;
}
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"ioctl error: %s",
strerror(errno));
#else
g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified ioctl error");
#endif
return FALSE;
}
return TRUE;
#else
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as <sys/ioctl.h> not found");
return FALSE;
#endif
}
/**
* fu_udev_device_pread:
* @self: a #FuUdevDevice
* @port: offset address
* @buf: (in): data
* @bufsz: size of @buf
* @error: (nullable): optional return location for an error
*
* Read a buffer from a file descriptor at a given offset.
*
* Returns: %TRUE for success
*
* Since: 1.8.2
**/
gboolean
fu_udev_device_pread(FuUdevDevice *self, goffset port, guint8 *buf, gsize bufsz, GError **error)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(buf != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* not open! */
if (priv->fd == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"%s [%s] has not been opened",
fu_device_get_id(FU_DEVICE(self)),
fu_device_get_name(FU_DEVICE(self)));
return FALSE;
}
#ifdef HAVE_PWRITE
if (pread(priv->fd, buf, bufsz, port) != (gssize)bufsz) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to read from port 0x%04x: %s",
(guint)port,
strerror(errno));
return FALSE;
}
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as pread() is unavailable");
return FALSE;
#endif
}
/**
* fu_udev_device_seek:
* @self: a #FuUdevDevice
* @offset: offset address
* @error: (nullable): optional return location for an error
*
* Seeks a file descriptor to a given offset.
*
* Returns: %TRUE for success
*
* Since: 1.7.2
**/
gboolean
fu_udev_device_seek(FuUdevDevice *self, goffset offset, GError **error)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* not open! */
if (priv->fd == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"%s [%s] has not been opened",
fu_device_get_id(FU_DEVICE(self)),
fu_device_get_name(FU_DEVICE(self)));
return FALSE;
}
#ifdef HAVE_PWRITE
if (lseek(priv->fd, offset, SEEK_SET) < 0) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to seek to 0x%04x: %s",
(guint)offset,
strerror(errno));
return FALSE;
}
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as lseek() is unavailable");
return FALSE;
#endif
}
/**
* fu_udev_device_pwrite:
* @self: a #FuUdevDevice
* @port: offset address
* @buf: (out): data
* @bufsz: size of @data
* @error: (nullable): optional return location for an error
*
* Write a buffer to a file descriptor at a given offset.
*
* Returns: %TRUE for success
*
* Since: 1.8.2
**/
gboolean
fu_udev_device_pwrite(FuUdevDevice *self,
goffset port,
const guint8 *buf,
gsize bufsz,
GError **error)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* not open! */
if (priv->fd == 0) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"%s [%s] has not been opened",
fu_device_get_id(FU_DEVICE(self)),
fu_device_get_name(FU_DEVICE(self)));
return FALSE;
}
#ifdef HAVE_PWRITE
if (pwrite(priv->fd, buf, bufsz, port) != (gssize)bufsz) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to write to port %04x: %s",
(guint)port,
strerror(errno));
return FALSE;
}
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Not supported as pwrite() is unavailable");
return FALSE;
#endif
}
/**
* fu_udev_device_get_parent_name
* @self: a #FuUdevDevice
*
* Returns the name of the direct ancestor of this device
*
* Returns: string or NULL if unset or invalid
*
* Since: 1.4.5
**/
gchar *
fu_udev_device_get_parent_name(FuUdevDevice *self)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_autoptr(GUdevDevice) parent = NULL;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
parent = g_udev_device_get_parent(priv->udev_device);
return parent == NULL ? NULL : g_strdup(g_udev_device_get_name(parent));
#else
return NULL;
#endif
}
/**
* fu_udev_device_get_sysfs_attr:
* @self: a #FuUdevDevice
* @attr: name of attribute to get
* @error: (nullable): optional return location for an error
*
* Reads an arbitrary sysfs attribute 'attr' associated with UDEV device
*
* Returns: string or NULL
*
* Since: 1.4.5
**/
const gchar *
fu_udev_device_get_sysfs_attr(FuUdevDevice *self, const gchar *attr, GError **error)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
const gchar *result;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL);
g_return_val_if_fail(attr != NULL, NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
/* nothing to do */
if (priv->udev_device == NULL) {
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "not yet initialized");
return NULL;
}
result = g_udev_device_get_sysfs_attr(priv->udev_device, attr);
if (result == NULL) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_NOT_FOUND,
"attribute %s returned no data",
attr);
return NULL;
}
return result;
#else
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"getting attributes is not supported as no GUdev support");
return NULL;
#endif
}
/**
* fu_udev_device_get_sysfs_attr_uint64:
* @self: a #FuUdevDevice
* @attr: name of attribute to get
* @value: (out) (optional): value to return
* @error: (nullable): optional return location for an error
*
* Reads an arbitrary sysfs attribute 'attr' associated with UDEV device as a uint64.
*
* Returns: %TRUE for success
*
* Since: 1.7.2
**/
gboolean
fu_udev_device_get_sysfs_attr_uint64(FuUdevDevice *self,
const gchar *attr,
guint64 *value,
GError **error)
{
const gchar *tmp;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(attr != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
tmp = fu_udev_device_get_sysfs_attr(self, attr, error);
if (tmp == NULL)
return FALSE;
return fu_strtoull(tmp, value, 0, G_MAXUINT64, error);
}
/**
* fu_udev_device_write_sysfs:
* @self: a #FuUdevDevice
* @attribute: sysfs attribute name
* @val: data to write into the attribute
* @error: (nullable): optional return location for an error
*
* Writes data into a sysfs attribute
*
* Returns: %TRUE for success
*
* Since: 1.4.5
**/
gboolean
fu_udev_device_write_sysfs(FuUdevDevice *self,
const gchar *attribute,
const gchar *val,
GError **error)
{
#ifdef __linux__
ssize_t n;
int r;
int fd;
g_autofree gchar *path = NULL;
g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE);
g_return_val_if_fail(attribute != NULL, FALSE);
g_return_val_if_fail(val != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
path = g_build_filename(fu_udev_device_get_sysfs_path(self), attribute, NULL);
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0) {
g_set_error(error,
G_IO_ERROR,
g_io_error_from_errno(errno),
"could not open %s: %s",
path,
g_strerror(errno));
return FALSE;
}
do {
n = write(fd, val, strlen(val));
if (n < 1 && errno != EINTR) {
g_set_error(error,
G_IO_ERROR,
g_io_error_from_errno(errno),
"could not write to %s: %s",
path,
g_strerror(errno));
(void)close(fd);
return FALSE;
}
} while (n < 1);
r = close(fd);
if (r < 0 && errno != EINTR) {
g_set_error(error,
G_IO_ERROR,
g_io_error_from_errno(errno),
"could not close %s: %s",
path,
g_strerror(errno));
return FALSE;
}
return TRUE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"sysfs attributes not supported on Windows");
return FALSE;
#endif
}
/**
* fu_udev_device_get_devtype
* @self: a #FuUdevDevice
*
* Returns the Udev device type
*
* Returns: device type specified in the uevent
*
* Since: 1.4.5
**/
const gchar *
fu_udev_device_get_devtype(FuUdevDevice *self)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
return g_udev_device_get_devtype(priv->udev_device);
#else
return NULL;
#endif
}
/**
* fu_udev_device_get_siblings_with_subsystem
* @self: a #FuUdevDevice
* @subsystem: the name of a udev subsystem
*
* Get a list of devices that are siblings of self and have the
* provided subsystem.
*
* Returns: (element-type FuUdevDevice) (transfer full): devices
*
* Since: 1.6.0
*/
GPtrArray *
fu_udev_device_get_siblings_with_subsystem(FuUdevDevice *self, const gchar *const subsystem)
{
g_autoptr(GPtrArray) out = g_ptr_array_new_with_free_func(g_object_unref);
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_autoptr(GUdevDevice) udev_parent = g_udev_device_get_parent(priv->udev_device);
const gchar *udev_parent_path = g_udev_device_get_sysfs_path(udev_parent);
g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
g_autoptr(GList) enumerated = g_udev_client_query_by_subsystem(udev_client, subsystem);
for (GList *element = enumerated; element != NULL; element = element->next) {
g_autoptr(GUdevDevice) enumerated_device = element->data;
g_autoptr(GUdevDevice) enumerated_parent = NULL;
const gchar *enumerated_parent_path;
/* get parent, if it exists */
enumerated_parent = g_udev_device_get_parent(enumerated_device);
if (enumerated_parent == NULL)
break;
enumerated_parent_path = g_udev_device_get_sysfs_path(enumerated_parent);
/* if the sysfs path of self's parent is the same as that of the
* located device's parent, they are siblings */
if (g_strcmp0(udev_parent_path, enumerated_parent_path) == 0) {
FuUdevDevice *dev =
fu_udev_device_new(fu_device_get_context(FU_DEVICE(self)),
g_steal_pointer(&enumerated_device));
g_ptr_array_add(out, dev);
}
}
#endif
return g_steal_pointer(&out);
}
/**
* fu_udev_device_get_parent_with_subsystem
* @self: a #FuUdevDevice
* @subsystem: the name of a udev subsystem
*
* Get the device that is a parent of self and has the provided subsystem.
*
* Returns: (transfer full): device, or %NULL
*
* Since: 1.7.6
*/
FuUdevDevice *
fu_udev_device_get_parent_with_subsystem(FuUdevDevice *self, const gchar *subsystem)
{
#ifdef HAVE_GUDEV
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_autoptr(GUdevDevice) device_tmp = NULL;
device_tmp = g_udev_device_get_parent_with_subsystem(priv->udev_device, subsystem, NULL);
if (device_tmp == NULL)
return NULL;
return fu_udev_device_new(fu_device_get_context(FU_DEVICE(self)), device_tmp);
#else
return NULL;
#endif
}
/**
* fu_udev_device_get_children_with_subsystem
* @self: a #FuUdevDevice
* @subsystem: the name of a udev subsystem
*
* Get a list of devices that are children of self and have the
* provided subsystem.
*
* Returns: (element-type FuUdevDevice) (transfer full): devices
*
* Since: 1.6.2
*/
GPtrArray *
fu_udev_device_get_children_with_subsystem(FuUdevDevice *self, const gchar *const subsystem)
{
g_autoptr(GPtrArray) out = g_ptr_array_new_with_free_func(g_object_unref);
#ifdef HAVE_GUDEV
const gchar *self_path = fu_udev_device_get_sysfs_path(self);
g_autoptr(GUdevClient) udev_client = g_udev_client_new(NULL);
g_autoptr(GList) enumerated = g_udev_client_query_by_subsystem(udev_client, subsystem);
for (GList *element = enumerated; element != NULL; element = element->next) {
g_autoptr(GUdevDevice) enumerated_device = element->data;
g_autoptr(GUdevDevice) enumerated_parent = NULL;
const gchar *enumerated_parent_path;
/* get parent, if it exists */
enumerated_parent = g_udev_device_get_parent(enumerated_device);
if (enumerated_parent == NULL)
break;
enumerated_parent_path = g_udev_device_get_sysfs_path(enumerated_parent);
/* enumerated device is a child of self if its parent is the
* same as self */
if (g_strcmp0(self_path, enumerated_parent_path) == 0) {
FuUdevDevice *dev =
fu_udev_device_new(fu_device_get_context(FU_DEVICE(self)),
g_steal_pointer(&enumerated_device));
g_ptr_array_add(out, dev);
}
}
#endif
return g_steal_pointer(&out);
}
static void
fu_udev_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
FuUdevDevice *self = FU_UDEV_DEVICE(object);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
switch (prop_id) {
case PROP_UDEV_DEVICE:
g_value_set_object(value, priv->udev_device);
break;
case PROP_SUBSYSTEM:
g_value_set_string(value, priv->subsystem);
break;
case PROP_BIND_ID:
g_value_set_string(value, priv->bind_id);
break;
case PROP_DRIVER:
g_value_set_string(value, priv->driver);
break;
case PROP_DEVICE_FILE:
g_value_set_string(value, priv->device_file);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
fu_udev_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
FuUdevDevice *self = FU_UDEV_DEVICE(object);
switch (prop_id) {
case PROP_UDEV_DEVICE:
fu_udev_device_set_dev(self, g_value_get_object(value));
break;
case PROP_SUBSYSTEM:
fu_udev_device_set_subsystem(self, g_value_get_string(value));
break;
case PROP_BIND_ID:
fu_udev_device_set_bind_id(self, g_value_get_string(value));
break;
case PROP_DRIVER:
fu_udev_device_set_driver(self, g_value_get_string(value));
break;
case PROP_DEVICE_FILE:
fu_udev_device_set_device_file(self, g_value_get_string(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
fu_udev_device_finalize(GObject *object)
{
FuUdevDevice *self = FU_UDEV_DEVICE(object);
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
g_free(priv->subsystem);
g_free(priv->bind_id);
g_free(priv->driver);
g_free(priv->device_file);
if (priv->udev_device != NULL)
g_object_unref(priv->udev_device);
if (priv->fd > 0)
g_close(priv->fd, NULL);
G_OBJECT_CLASS(fu_udev_device_parent_class)->finalize(object);
}
static void
fu_udev_device_init(FuUdevDevice *self)
{
FuUdevDevicePrivate *priv = GET_PRIVATE(self);
priv->flags = FU_UDEV_DEVICE_FLAG_OPEN_READ | FU_UDEV_DEVICE_FLAG_OPEN_WRITE;
}
static void
fu_udev_device_class_init(FuUdevDeviceClass *klass)
{
FuDeviceClass *device_class = FU_DEVICE_CLASS(klass);
GObjectClass *object_class = G_OBJECT_CLASS(klass);
GParamSpec *pspec;
object_class->finalize = fu_udev_device_finalize;
object_class->get_property = fu_udev_device_get_property;
object_class->set_property = fu_udev_device_set_property;
device_class->probe = fu_udev_device_probe;
device_class->rescan = fu_udev_device_rescan;
device_class->incorporate = fu_udev_device_incorporate;
device_class->open = fu_udev_device_open;
device_class->close = fu_udev_device_close;
device_class->to_string = fu_udev_device_to_string;
device_class->bind_driver = fu_udev_device_bind_driver;
device_class->unbind_driver = fu_udev_device_unbind_driver;
/**
* FuUdevDevice::changed:
* @self: the #FuUdevDevice instance that emitted the signal
*
* The ::changed signal is emitted when the low-level GUdevDevice has changed.
*
* Since: 1.1.2
**/
signals[SIGNAL_CHANGED] = g_signal_new("changed",
G_TYPE_FROM_CLASS(object_class),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE,
0);
/**
* FuUdevDevice:udev-device:
*
* The low-level GUdevDevice.
*
* Since: 1.1.2
*/
pspec = g_param_spec_object("udev-device",
NULL,
NULL,
G_UDEV_TYPE_DEVICE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_UDEV_DEVICE, pspec);
/**
* FuUdevDevice:subsystem:
*
* The device subsystem.
*
* Since: 1.1.2
*/
pspec = g_param_spec_string("subsystem",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_SUBSYSTEM, pspec);
/**
* FuUdevDevice:bind-id:
*
* The bind ID to use when binding a new driver.
*
* Since: 1.7.2
*/
pspec = g_param_spec_string("bind-id",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_BIND_ID, pspec);
/**
* FuUdevDevice:driver:
*
* The driver being used for the device.
*
* Since: 1.5.3
*/
pspec = g_param_spec_string("driver",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_DRIVER, pspec);
/**
* FuUdevDevice:device-file:
*
* The low level file to use for device access.
*
* Since: 1.3.1
*/
pspec = g_param_spec_string("device-file",
NULL,
NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_DEVICE_FILE, pspec);
}
/**
* fu_udev_device_new:
* @ctx: (nullable): a #FuContext
* @udev_device: a #GUdevDevice
*
* Creates a new #FuUdevDevice.
*
* Returns: (transfer full): a #FuUdevDevice
*
* Since: 1.8.2
**/
FuUdevDevice *
fu_udev_device_new(FuContext *ctx, GUdevDevice *udev_device)
{
#ifdef HAVE_GUDEV
/* create the correct object depending on the subsystem */
if (g_strcmp0(g_udev_device_get_subsystem(udev_device), "i2c-dev") == 0) {
return g_object_new(FU_TYPE_I2C_DEVICE,
"context",
ctx,
"udev-device",
udev_device,
NULL);
}
#endif
return g_object_new(FU_TYPE_UDEV_DEVICE, "context", ctx, "udev-device", udev_device, NULL);
}