Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add port_number, port_chain for all platform #58

Closed
wants to merge 10 commits into from
26 changes: 15 additions & 11 deletions src/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct DeviceId(pub(crate) crate::platform::DeviceId);
///
/// * Some fields are platform-specific
/// * Linux: `sysfs_path`
/// * Windows: `instance_id`, `parent_instance_id`, `port_number`, `driver`
/// * Windows: `instance_id`, `parent_instance_id`, `driver`
/// * macOS: `registry_id`, `location_id`
#[derive(Clone)]
pub struct DeviceInfo {
Expand All @@ -31,9 +31,6 @@ pub struct DeviceInfo {
#[cfg(target_os = "windows")]
pub(crate) parent_instance_id: OsString,

#[cfg(target_os = "windows")]
pub(crate) port_number: u32,

#[cfg(target_os = "windows")]
pub(crate) devinst: crate::platform::DevInst,

Expand All @@ -47,6 +44,8 @@ pub struct DeviceInfo {
pub(crate) location_id: u32,

pub(crate) bus_number: u8,
pub(crate) port_number: u32,
pub(crate) port_chain: Vec<u32>,
pub(crate) device_address: u8,

pub(crate) vendor_id: u16,
Expand Down Expand Up @@ -114,12 +113,6 @@ impl DeviceInfo {
&self.parent_instance_id
}

/// *(Windows-only)* Port number
#[cfg(target_os = "windows")]
pub fn port_number(&self) -> u32 {
self.port_number
}

/// *(Windows-only)* Driver associated with the device as a whole
#[cfg(target_os = "windows")]
pub fn driver(&self) -> Option<&str> {
Expand All @@ -143,6 +136,16 @@ impl DeviceInfo {
self.bus_number
}

/// Port number
pub fn port_number(&self) -> u32 {
self.port_number
}

/// Port chain
pub fn port_chain(&self) -> impl Iterator<Item = &u32> {
self.port_chain.iter()
}

/// Number identifying the device within the bus.
pub fn device_address(&self) -> u8 {
self.device_address
Expand Down Expand Up @@ -254,6 +257,8 @@ impl std::fmt::Debug for DeviceInfo {
let mut s = f.debug_struct("DeviceInfo");

s.field("bus_number", &self.bus_number)
.field("port_number", &self.port_number)
.field("port_chain", &self.port_chain)
.field("device_address", &self.device_address)
.field("vendor_id", &format_args!("0x{:04X}", self.vendor_id))
.field("product_id", &format_args!("0x{:04X}", self.product_id))
Expand All @@ -278,7 +283,6 @@ impl std::fmt::Debug for DeviceInfo {
{
s.field("instance_id", &self.instance_id);
s.field("parent_instance_id", &self.parent_instance_id);
s.field("port_number", &self.port_number);
s.field("driver", &self.driver);
}

Expand Down
7 changes: 7 additions & 0 deletions src/platform/linux_usbfs/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,15 @@ pub fn list_devices() -> Result<impl Iterator<Item = DeviceInfo>, Error> {

pub fn probe_device(path: SysfsPath) -> Result<DeviceInfo, Error> {
debug!("probe device {path:?}");
let port_chain: Vec<u32> = path
.read_attr::<String>("devpath")?
.split('.')
.flat_map(|v| v.parse::<u32>())
.collect();
Ok(DeviceInfo {
bus_number: path.read_attr("busnum")?,
port_number: *port_chain.last().unwrap_or(&0),
port_chain,
device_address: path.read_attr("devnum")?,
vendor_id: path.read_attr_hex("idVendor")?,
product_id: path.read_attr_hex("idProduct")?,
Expand Down
90 changes: 85 additions & 5 deletions src/platform/macos_iokit/enumeration.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use std::io::ErrorKind;
use std::{collections::VecDeque, io::ErrorKind};

use core_foundation::{
base::{CFType, TCFType},
data::CFData,
number::CFNumber,
string::CFString,
ConcreteCFType,
};
use io_kit_sys::{
kIOMasterPortDefault, kIORegistryIterateParents, kIORegistryIterateRecursively,
keys::kIOServicePlane, ret::kIOReturnSuccess, usb::lib::kIOUSBDeviceClassName,
IORegistryEntryGetChildIterator, IORegistryEntryGetRegistryEntryID,
IORegistryEntrySearchCFProperty, IOServiceGetMatchingServices, IOServiceMatching,
IORegistryEntryGetChildIterator, IORegistryEntryGetParentEntry,
IORegistryEntryGetRegistryEntryID, IORegistryEntrySearchCFProperty,
IOServiceGetMatchingServices, IOServiceMatching,
};
use log::debug;

Expand Down Expand Up @@ -50,10 +52,14 @@ pub(crate) fn probe_device(device: IoService) -> Option<DeviceInfo> {
log::debug!("Probing device {registry_id:08x}");

// Can run `ioreg -p IOUSB -l` to see all properties
let location_id = get_integer_property(&device, "locationID")? as u32;
let port_chain: Vec<u32> = get_port_chain(&device).collect();
Some(DeviceInfo {
registry_id,
location_id: get_integer_property(&device, "locationID")? as u32,
bus_number: 0, // TODO: does this exist on macOS?
location_id,
bus_number: (location_id >> 24) as u8,
port_number: *port_chain.last().unwrap_or(&0),
port_chain,
device_address: get_integer_property(&device, "USB Address")? as u8,
vendor_id: get_integer_property(&device, "idVendor")? as u16,
product_id: get_integer_property(&device, "idProduct")? as u16,
Expand Down Expand Up @@ -127,6 +133,10 @@ fn get_string_property(device: &IoService, property: &'static str) -> Option<Str
get_property::<CFString>(device, property).map(|s| s.to_string())
}

fn get_data_property(device: &IoService, property: &'static str) -> Option<Vec<u8>> {
get_property::<CFData>(device, property).map(|d| d.to_vec())
}

fn get_integer_property(device: &IoService, property: &'static str) -> Option<i64> {
let n = get_property::<CFNumber>(device, property)?;
n.to_i64().or_else(|| {
Expand All @@ -149,6 +159,76 @@ fn get_children(device: &IoService) -> Result<IoServiceIterator, Error> {
}
}

fn get_parent(device: &IoService) -> Result<IoService, Error> {
unsafe {
let mut handle = 0;
let r = IORegistryEntryGetParentEntry(device.get(), kIOServicePlane as *mut _, &mut handle);
if r != kIOReturnSuccess {
debug!("IORegistryEntryGetParentEntry failed: {r}");
return Err(Error::from_raw_os_error(r));
}

Ok(IoService::new(handle))
}
}

fn get_port_number(device: &IoService) -> Option<u32> {
get_integer_property(device, "PortNum").map_or_else(
|| {
if let Ok(parent) = get_parent(device) {
return get_data_property(&parent, "port")
.map(|d| u32::from_ne_bytes(d[0..4].try_into().unwrap()));
}
None
},
|v| Some(v as u32),
)
}

fn get_port_chain(device: &IoService) -> impl Iterator<Item = u32> {
let mut port_chain = VecDeque::new();

if let Some(port_number) = get_port_number(device) {
port_chain.push_back(port_number);
}

if let Ok(mut hub) = get_parent(device) {
loop {
let port_number = match get_port_number(&hub) {
Some(p) => p,
None => break,
};
if port_number == 0 {
break;
}
port_chain.push_front(port_number);

let session_id = match get_integer_property(&hub, "sessionID") {
Some(session_id) => session_id,
None => break,
};

hub = match get_parent(&hub) {
Ok(hub) => hub,
Err(_) => break,
};

// Ignore the same sessionID
if session_id
== match get_integer_property(&hub, "sessionID") {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you observe it reporting a device as its own parent or find documentation indicating that, or is this just a precaution to prevent infinite-looping?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent device may return the same sessionID, which needs to be ignored until a new sessionID is obtained or the obtaining fails.

Some(session_id) => session_id,
None => break,
}
{
port_chain.pop_front();
continue;
}
}
}

port_chain.into_iter()
}

fn map_speed(speed: i64) -> Option<Speed> {
// https://developer.apple.com/documentation/iokit/1425357-usbdevicespeed
match speed {
Expand Down
99 changes: 91 additions & 8 deletions src/platform/windows_winusb/enumeration.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use std::{
collections::{HashMap, VecDeque},
ffi::{OsStr, OsString},
io::ErrorKind,
};

use log::debug;
use windows_sys::Win32::Devices::{
Properties::{
DEVPKEY_Device_Address, DEVPKEY_Device_BusNumber, DEVPKEY_Device_BusReportedDeviceDesc,
DEVPKEY_Device_CompatibleIds, DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId,
DEVPKEY_Device_Parent, DEVPKEY_Device_Service,
DEVPKEY_Device_Address, DEVPKEY_Device_BusReportedDeviceDesc, DEVPKEY_Device_CompatibleIds,
DEVPKEY_Device_EnumeratorName, DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId,
DEVPKEY_Device_LocationInfo, DEVPKEY_Device_LocationPaths, DEVPKEY_Device_Parent,
DEVPKEY_Device_Service,
},
Usb::GUID_DEVINTERFACE_USB_DEVICE,
Usb::{GUID_DEVINTERFACE_USB_DEVICE, GUID_DEVINTERFACE_USB_HOST_CONTROLLER},
};

use crate::{
Expand Down Expand Up @@ -42,8 +44,6 @@ pub fn probe_device(devinst: DevInst) -> Option<DeviceInfo> {
debug!("Probing device {instance_id:?}");

let parent_instance_id = devinst.get_property::<OsString>(DEVPKEY_Device_Parent)?;
let bus_number = devinst.get_property::<u32>(DEVPKEY_Device_BusNumber)?;
let port_number = devinst.get_property::<u32>(DEVPKEY_Device_Address)?;

let hub_port = HubPort::by_child_devinst(devinst).ok()?;
let info = hub_port.get_info().ok()?;
Expand Down Expand Up @@ -98,13 +98,27 @@ pub fn probe_device(devinst: DevInst) -> Option<DeviceInfo> {

interfaces.sort_unstable_by_key(|i| i.interface_number);

let bus_devs = cfgmgr32::list_interfaces(GUID_DEVINTERFACE_USB_HOST_CONTROLLER, None)
.iter()
.flat_map(|i| get_device_interface_property::<WCString>(i, DEVPKEY_Device_InstanceId))
.flat_map(|d| DevInst::from_instance_id(&d))
.flat_map(|d| d.children())
.map(|d| d.instance_id().to_string())
.enumerate()
.map(|v| (v.1, (v.0 + 1) as u8))
.collect::<HashMap<String, u8>>();
Comment on lines +101 to +109
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing bus number using DEVPKEY_Device_BusNumber is indeed wrong (#70) but as mentioned in that issue I'm considering just exposing the bus ID as a string rather than trying to map it to a small integer.


let (bus_number, port_chain) = get_port_chain(devinst, &bus_devs);
let port_chain = port_chain.collect::<Vec<u32>>();

Some(DeviceInfo {
instance_id,
parent_instance_id,
devinst,
port_number,
driver: Some(driver).filter(|s| !s.is_empty()),
bus_number: bus_number as u8,
bus_number,
port_number: *port_chain.last().unwrap_or(&0),
port_chain,
device_address: info.address,
vendor_id: info.device_desc.idVendor,
product_id: info.device_desc.idProduct,
Expand Down Expand Up @@ -232,6 +246,75 @@ pub(crate) fn get_usbccgp_winusb_device_path(child: DevInst) -> Result<WCString,
Ok(path.to_owned())
}

fn get_port_chain(dev: DevInst, bus_devs: &HashMap<String, u8>) -> (u8, impl Iterator<Item = u32>) {
let mut bus_number = 0;
let mut port_chain = VecDeque::new();

if let Some(port_number) = get_port_number(dev) {
port_chain.push_back(port_number);
}

if let Some(mut parent) = dev.parent() {
loop {
if parent
.get_property::<OsString>(DEVPKEY_Device_EnumeratorName)
.unwrap_or_default()
.eq_ignore_ascii_case("USB")
{
if let Some(port_number) = get_port_number(parent) {
if port_number != 0 {
port_chain.push_front(port_number);
if let Some(d) = parent.parent() {
parent = d;
continue;
}
} else {
if let Some(bus_dev) =
parent.get_property::<WCString>(DEVPKEY_Device_InstanceId)
{
bus_number = *bus_devs.get(&bus_dev.to_string()).unwrap_or(&0);
}
}
}
}
break;
}
}

(bus_number, port_chain.into_iter())
}

fn get_port_number(devinst: DevInst) -> Option<u32> {
// Find Port_#xxxx
// Port_#0002.Hub_#000D
if let Some(location_info) = devinst.get_property::<OsString>(DEVPKEY_Device_LocationInfo) {
let s = location_info.to_string_lossy();
if &s[0..6] == "Port_#" {
if let Ok(n) = s[6..10].parse::<u32>() {
return Some(n);
}
}
}
// Find last #USB(x)
// PCIROOT(B2)#PCI(0300)#PCI(0000)#USBROOT(0)#USB(1)#USB(2)#USBMI(3)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, do these #USB(n) elements directly correspond to the port chain that we could just parse from the location path?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the case

if let Some(location_paths) =
devinst.get_property::<Vec<OsString>>(DEVPKEY_Device_LocationPaths)
{
for location_path in location_paths {
let s = location_path.to_string_lossy();
for b in s.split('#').rev() {
if b.contains("USB(") {
if let Ok(n) = b[5..b.len()].parse::<u32>() {
return Some(n);
}
break;
}
}
}
}
devinst.get_property::<u32>(DEVPKEY_Device_Address)
Comment on lines +288 to +315
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason all three of these techniques are used? Would be worth a comment explaining under what circumstances we'd hit each of these.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function references the libusb code.
The DEVPKEY_Device_LocationInfo and DEVPKEY_Device_LocationPaths parameters may not return a matching format.

}

fn get_interface_number(intf_dev: DevInst) -> Option<u8> {
let hw_ids = intf_dev.get_property::<Vec<OsString>>(DEVPKEY_Device_HardwareIds);
hw_ids
Expand Down
6 changes: 5 additions & 1 deletion src/platform/windows_winusb/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ impl<'a> Iterator for NulSepListIter<'a> {
fn next(&mut self) -> Option<Self::Item> {
if let Some(next_nul) = self.0.iter().copied().position(|x| x == 0) {
let (i, next) = self.0.split_at(next_nul + 1);
self.0 = next;
self.0 = if next.is_empty() || next.first() == Some(&0) {
&[]
} else {
next
};
Some(unsafe { WCStr::from_slice_unchecked(i) })
} else {
None
Expand Down
Loading