From 4c095a1045eff4a1ebcdb488887f8167ac2773e7 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Wed, 20 Nov 2024 23:05:49 -0800 Subject: [PATCH] feat(CompositeDeviceConfig): add udev-based source device matching --- .../schema/composite_device_v1.json | 67 +++ src/config/mod.rs | 143 ++++++ src/input/manager.rs | 425 +++++++++++------- src/udev/device.rs | 94 ++++ 4 files changed, 571 insertions(+), 158 deletions(-) diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index dd5f08f..f988cc1 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -168,6 +168,9 @@ "type": "boolean", "default": false }, + "udev": { + "$ref": "#/definitions/Udev" + }, "evdev": { "$ref": "#/definitions/Evdev" }, @@ -187,6 +190,70 @@ ], "title": "SourceDevice" }, + "Udev": { + "description": "Source device to manage. Properties support globbing patterns.", + "type": "object", + "additionalProperties": false, + "properties": { + "attributes": { + "description": "Device attributes to match. Attributes can be found by running `udevadm info --attribute-walk /path/to/device` and looking at fields that look like: `ATTR{name}==\"value\"`.", + "type": "array", + "items": { + "$ref": "#/definitions/UdevKeyValue" + } + }, + "dev_node": { + "description": "Full device node path to match. E.g. '/dev/hidraw3', '/dev/input/event*'", + "type": "string" + }, + "dev_path": { + "description": "Full kernel device path to match. The path does not contain the sys mount point, but does start with a `/`. For example, the dev_path for `hidraw3` could be `/devices/pci0000:00/0000:00:08.1/.../hidraw/hidraw3`.", + "type": "string" + }, + "driver": { + "description": "Driver being used by the device (or parent devices) to match. E.g. `playstation`, `microsoft`", + "type": "string" + }, + "properties": { + "description": "Device properties to match. Properties can be found by running `udevadm info -q property /path/to/device`.", + "type": "array", + "items": { + "$ref": "#/definitions/UdevKeyValue" + } + }, + "subsystem": { + "description": "Subsystem to match. E.g. `input`, `hidraw`, `iio`", + "type": "string" + }, + "sys_name": { + "description": "Sysname to match. The sysname is typically the last part of the path to the device. E.g. `hidraw3`, `event6`", + "type": "string" + }, + "sys_path": { + "description": "Syspath to match. The syspath is an absolute path and includes the sys mount point. For example, the syspath for `hidraw3` could be `/sys/devices/pci0000:00/0000:00:08.1/.../hidraw/hidraw3`, which includes the sys mount point `/sys`.", + "type": "string" + } + }, + "required": [], + "title": "Udev" + }, + "UdevKeyValue": { + "description": "Udev attribute or property key/value pair" + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the property or attribute to match. Does NOT support globbing patterns.", + "type": "string" + }, + "value": { + "description": "Value of the property or attribute to match. Supports globbing patterns.", + "type": "string" + }, + }, + "required": ["name"], + "title": "UdevKeyValue" + }, "Evdev": { "description": "Source device to manage. Properties support globbing patterns.", "type": "object", diff --git a/src/config/mod.rs b/src/config/mod.rs index 6b0c7b5..06184b1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -307,6 +307,7 @@ pub struct SourceDevice { pub evdev: Option, pub hidraw: Option, pub iio: Option, + pub udev: Option, pub unique: Option, pub blocked: Option, pub ignore: Option, @@ -332,6 +333,26 @@ pub struct Hidraw { pub name: Option, } +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct Udev { + pub attributes: Option>, + pub dev_node: Option, + pub dev_path: Option, + pub driver: Option, + pub properties: Option>, + pub subsystem: Option, + pub sys_name: Option, + pub sys_path: Option, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct UdevAttribute { + pub name: String, + pub value: Option, +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[allow(clippy::upper_case_acronyms)] @@ -397,6 +418,18 @@ impl CompositeDeviceConfig { /// Returns a [SourceDevice] if it matches the given [UdevDevice]. pub fn get_matching_device(&self, udevice: &UdevDevice) -> Option { + // Check udev matches first + for config in self.source_devices.iter() { + let Some(udev_config) = config.udev.as_ref() else { + continue; + }; + + if self.has_matching_udev(udevice, udev_config) { + return Some(config.clone()); + } + } + + // Deprecated method for device matching based on subsystem let subsystem = udevice.subsystem(); match subsystem.as_str() { "input" => { @@ -431,6 +464,116 @@ impl CompositeDeviceConfig { None } + /// Returns true if a given device matches the given udev config + pub fn has_matching_udev(&self, device: &UdevDevice, udev_config: &Udev) -> bool { + log::trace!("Checking udev config '{:?}'", udev_config); + + if let Some(attributes) = udev_config.attributes.as_ref() { + let device_attributes = device.get_attributes(); + + for attribute in attributes { + let Some(device_attr_value) = device_attributes.get(&attribute.name) else { + // If the device does not have this attribute, return false + return false; + }; + + // If no value was specified in the config, then only match on + // the presence of the attribute and not the value. + let Some(attr_value) = attribute.value.as_ref() else { + continue; + }; + + // Glob match on the attribute value + log::trace!("Checking attribute: {attr_value} against {device_attr_value}"); + if !glob_match(attr_value.as_str(), device_attr_value.as_str()) { + return false; + } + } + } + + if let Some(dev_node) = udev_config.dev_node.as_ref() { + let device_dev_node = device.devnode(); + log::trace!("Checking dev_node: {dev_node} against {device_dev_node}"); + if !glob_match(dev_node.as_str(), device_dev_node.as_str()) { + return false; + } + } + + if let Some(dev_path) = udev_config.dev_path.as_ref() { + let device_dev_path = device.devpath(); + log::trace!("Checking dev_path: {dev_path} against {device_dev_path}"); + if !glob_match(dev_path.as_str(), device_dev_path.as_str()) { + return false; + } + } + + if let Some(driver) = udev_config.driver.as_ref() { + let all_drivers = device.drivers(); + let mut has_matches = false; + + for device_driver in all_drivers { + log::trace!("Checking driver: {driver} against {device_driver}"); + if glob_match(driver.as_str(), device_driver.as_str()) { + has_matches = true; + break; + } + } + + if !has_matches { + return false; + } + } + + if let Some(properties) = udev_config.properties.as_ref() { + let device_properties = device.get_properties(); + + for property in properties { + let Some(device_prop_value) = device_properties.get(&property.name) else { + // If the device does not have this property, return false + return false; + }; + + // If no value was specified in the config, then only match on + // the presence of the property and not the value. + let Some(prop_value) = property.value.as_ref() else { + continue; + }; + + // Glob match on the property value + log::trace!("Checking property: {prop_value} against {device_prop_value}"); + if !glob_match(prop_value.as_str(), device_prop_value.as_str()) { + return false; + } + } + } + + if let Some(subsystem) = udev_config.subsystem.as_ref() { + let device_subsystem = device.subsystem(); + log::trace!("Checking subsystem: {subsystem} against {device_subsystem}"); + if !glob_match(subsystem.as_str(), device_subsystem.as_str()) { + return false; + } + } + + if let Some(sys_name) = udev_config.sys_name.as_ref() { + let device_sys_name = device.sysname(); + log::trace!("Checking sys_name: {sys_name} against {device_sys_name}"); + if !glob_match(sys_name.as_str(), device_sys_name.as_str()) { + return false; + } + } + + if let Some(sys_path) = udev_config.sys_path.as_ref() { + let device_sys_path = device.syspath(); + log::trace!("Checking sys_path: {sys_path} against {device_sys_path}"); + if !glob_match(sys_path.as_str(), device_sys_path.as_str()) { + return false; + } + } + + true + } + /// Returns true if a given hidraw device is within a list of hidraw configs. pub fn has_matching_hidraw(&self, device: &UdevDevice, hidraw_config: &Hidraw) -> bool { log::trace!("Checking hidraw config '{:?}'", hidraw_config,); diff --git a/src/input/manager.rs b/src/input/manager.rs index 649558f..ab83d7e 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -699,82 +699,158 @@ impl Manager { continue; }; log::debug!("Checking if existing composite device {composite_device:?} with config {:?} is missing device: {id:?}", config.name); - let source_devices = config.source_devices.clone(); + + // If the CompositeDevice only allows a single source device, skip its + // consideration. + if config.single_source.unwrap_or(false) { + log::trace!("{:?} is a single source device. Skipping.", config.name); + continue; + } + log::trace!( + "Composite device has {} source devices defined", + config.source_devices.len() + ); + + // Check if this device matches any source udev configs of the running + // CompositeDevice. + for source_device in config.source_devices.iter() { + log::trace!("Checking if existing composite device is missing udev device {id}"); + let Some(udev_config) = source_device.udev.as_ref() else { + continue; + }; + if !config.has_matching_udev(&device, udev_config) { + continue; + } + + // Check if the device has already been used in this config or not, + // stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!( + "Ignoring device {:?}, not adding to composite device: {}", + source_device, + composite_device + ); + break 'start; + } + } + if let Some(unique) = source_device.clone().unique { + if unique { + log::trace!( + "Found unique device {:?}, not adding to composite device {}", + source_device, + composite_device + ); + break 'start; + } + // Default to being unique + } else { + log::trace!( + "Found unique device {:?}, not adding to composite device {}", + source_device, + composite_device + ); + break 'start; + } + } + } + + log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); + let client = self.composite_devices.get(composite_device.as_str()); + if client.is_none() { + log::error!("No existing composite device found for key {composite_device:?}"); + continue; + } + self.add_device_to_composite_device(device, client.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); + } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + self.source_devices.insert(id, source_device.clone()); + + return Ok(()); + } + // TODO: Consolidate these match device.subsystem().as_str() { "input" => { log::trace!( - "Checking if existing composite device is missing event device {id}" - ); - - if config.single_source.unwrap_or(false) { - log::trace!("{:?} is a single source device. Skipping.", config.name); - continue; - } - log::trace!( - "Composite device has {} source devices defined", - source_devices.len() + "Checking if existing composite device is missing evdev device: {:?}", + device.name() ); - for source_device in source_devices { - if source_device.evdev.is_none() { + for source_device in config.source_devices.iter() { + let Some(evdev_config) = source_device.evdev.as_ref() else { log::trace!("Evdev section is empty"); continue; + }; + if !config.has_matching_evdev(&device, evdev_config) { + continue; } - if config.has_matching_evdev(&device, &source_device.clone().evdev.unwrap()) - { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { - continue; - } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - break 'start; - } + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); + break 'start; } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - // Default to being unique - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + // Default to being unique + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); - let client = self.composite_devices.get(composite_device.as_str()); - if client.is_none() { - log::error!( - "No existing composite device found for key {composite_device:?}" - ); - continue; - } - self.add_device_to_composite_device(device, client.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - self.source_devices.insert(id, source_device.clone()); - - return Ok(()); + log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); + let client = self.composite_devices.get(composite_device.as_str()); + if client.is_none() { + log::error!( + "No existing composite device found for key {composite_device:?}" + ); + continue; + } + self.add_device_to_composite_device(device, client.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + self.source_devices.insert(id, source_device.clone()); + + return Ok(()); } } "hidraw" => { @@ -782,128 +858,126 @@ impl Manager { "Checking if existing composite device is missing hidraw device: {:?}", device.name() ); - for source_device in source_devices { - if source_device.hidraw.is_none() { + for source_device in config.source_devices.iter() { + let Some(hidraw_config) = source_device.hidraw.as_ref() else { + continue; + }; + if !config.has_matching_hidraw(&device, hidraw_config) { continue; } - if config - .has_matching_hidraw(&device, &source_device.clone().hidraw.unwrap()) - { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { - continue; - } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - break 'start; - } + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); + break 'start; } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); - let handle = self.composite_devices.get(composite_device.as_str()); - if handle.is_none() { - log::error!( - "No existing composite device found for key {}", - composite_device.as_str() - ); - continue; - } - self.add_device_to_composite_device(device, handle.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - - self.source_devices.insert(id, source_device.clone()); - return Ok(()); + log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); + let handle = self.composite_devices.get(composite_device.as_str()); + if handle.is_none() { + log::error!( + "No existing composite device found for key {}", + composite_device.as_str() + ); + continue; } + self.add_device_to_composite_device(device, handle.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); + } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + + self.source_devices.insert(id, source_device.clone()); + return Ok(()); } } "iio" => { - log::trace!("Checking if existing composite device is missing hidraw device"); - for source_device in source_devices { - if source_device.iio.is_none() { + log::trace!("Checking if existing composite device is missing iio device"); + for source_device in config.source_devices.iter() { + let Some(iio_config) = source_device.iio.as_ref() else { + continue; + }; + if !config.has_matching_iio(&device, iio_config) { continue; } - if config.has_matching_iio(&device, &source_device.clone().iio.unwrap()) { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); continue; } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - continue; - } - } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); - let handle = self.composite_devices.get(composite_device.as_str()); - if handle.is_none() { - log::error!( - "No existing composite device found for key {}", - composite_device.as_str() - ); - continue; - } - self.add_device_to_composite_device(device, handle.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - - self.source_devices.insert(id, source_device.clone()); - return Ok(()); + log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); + let handle = self.composite_devices.get(composite_device.as_str()); + if handle.is_none() { + log::error!( + "No existing composite device found for key {}", + composite_device.as_str() + ); + continue; + } + self.add_device_to_composite_device(device, handle.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + + self.source_devices.insert(id, source_device.clone()); + return Ok(()); } } _ => (), @@ -944,6 +1018,41 @@ impl Manager { continue; } + // Check if this device matches any source udev configs + for source_device in config.source_devices.iter() { + let Some(udev_config) = source_device.udev.as_ref() else { + continue; + }; + if !config.has_matching_udev(&device, udev_config) { + continue; + } + + if let Some(ignored) = source_device.ignore { + if ignored { + log::trace!("Event device configured to ignore: {:?}", device); + return Ok(()); + } + } + log::info!("Found a matching udev device {id}, creating CompositeDevice"); + let dev = self + .create_composite_device_from_config(&config, device) + .await?; + + // Get the target input devices from the config + let target_devices_config = config.target_devices.clone(); + + // Create the composite deivce + self.start_composite_device( + dev, + config.clone(), + target_devices_config, + source_device.clone(), + ) + .await?; + + return Ok(()); + } + let source_devices = config.source_devices.clone(); match device.subsystem().as_str() { "input" => { diff --git a/src/udev/device.rs b/src/udev/device.rs index 65a1d68..9382314 100644 --- a/src/udev/device.rs +++ b/src/udev/device.rs @@ -26,10 +26,15 @@ pub trait AttributeGetter { fn product(&self) -> String; fn serial_number(&self) -> String; fn uniq(&self) -> String; + fn get_attributes(&self) -> HashMap; /// Returns the value of the given property from the device fn get_property(&self, property: &str) -> Option; /// Returns device properties for the device. E.g. {"ID_INPUT": "1", ...} fn get_properties(&self) -> HashMap; + /// Returns a list of all drivers used for this device. This list will be + /// in ascending order, with the first item in the list being the first + /// discovered driver in the device tree. + fn drivers(&self) -> Vec; } impl AttributeGetter for ::udev::Device { @@ -164,6 +169,35 @@ impl AttributeGetter for ::udev::Device { attr } + /// Returns a list of all drivers used for this device. This list will be + /// in ascending order, with the first item in the list being the first + /// discovered driver in the device tree. + fn drivers(&self) -> Vec { + let mut drivers = vec![]; + if let Some(driver) = self.driver() { + let value = driver.to_string_lossy().to_string(); + if !value.is_empty() { + drivers.push(value); + } + } + + // Walk up the device tree and query for each driver + let mut parent = self.parent(); + while parent.is_some() { + let current_parent = parent.unwrap(); + if let Some(driver) = current_parent.driver() { + let value = driver.to_string_lossy().to_string(); + if !value.is_empty() { + drivers.push(value); + } + } + + parent = current_parent.parent(); + } + + drivers + } + /// Looks for the given attribute at the given path using sysfs. fn get_attribute_from_sysfs(&self, path: &str, attribute: &str) -> Option { let parent = self.parent()?; @@ -197,6 +231,37 @@ impl AttributeGetter for ::udev::Device { None } + /// Recursively gets attributes for this device and all parent devices. + fn get_attributes(&self) -> HashMap { + let mut attributes = HashMap::new(); + for attr in self.attributes() { + let key = attr.name().to_string_lossy().to_string(); + if attributes.contains_key(&key) { + continue; + } + let value = attr.value().to_string_lossy().to_string(); + attributes.insert(key, value); + } + + // Walk up the device tree and query each device + let mut parent = self.parent(); + while parent.is_some() { + let current_parent = parent.unwrap(); + for attr in current_parent.attributes() { + let key = attr.name().to_string_lossy().to_string(); + if attributes.contains_key(&key) { + continue; + } + let value = attr.value().to_string_lossy().to_string(); + attributes.insert(key, value); + } + + parent = current_parent.parent(); + } + + attributes + } + /// Gets an attribute from the first device in the device tree to match the attribute. fn get_attribute_from_tree(&self, attribute: &str) -> String { // Check if the current device has this attribute @@ -345,6 +410,14 @@ impl UdevDevice { device.devpath().to_string_lossy().to_string() } + /// Recursively returns all drivers associated with the device. + pub fn drivers(&self) -> Vec { + let Ok(device) = self.get_device() else { + return vec![]; + }; + device.drivers() + } + /// Return the bustype attribute from the device pub fn id_bustype(&self) -> u16 { if let Some(bus_type) = self.bus_type { @@ -476,6 +549,27 @@ impl UdevDevice { } } + /// Recursively gets attributes for this device and all parent devices. + pub fn get_attributes(&self) -> HashMap { + let Ok(device) = self.get_device() else { + return HashMap::new(); + }; + device.get_attributes() + } + + /// Gets an attribute from the first device in the device tree to match the attribute. + pub fn get_attribute_from_tree(&self, attribute: &str) -> Option { + let Ok(device) = self.get_device() else { + return None; + }; + let value = device.get_attribute_from_tree(attribute); + if value.is_empty() { + None + } else { + Some(value) + } + } + /// Returns the value of the given property from the device pub fn get_property(&self, property: &str) -> Option { let Ok(device) = self.get_device() else {