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

Move nusb fork host controller info to macos mod #51

Merged
merged 2 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pci-ids = "0.2.5"
unicode-width = "0.2.0"

[patch.crates-io]
nusb = { git = "https://github.com/tuna-f1sh/nusb", branch = "cyme" }
nusb = { git = "https://github.com/kevinmehall/nusb", branch = "main" }

[dev-dependencies]
diff = "0.1"
Expand All @@ -48,6 +48,11 @@ assert-json-diff = "2.0.2"
udevrs = { version = "^0.3.0", optional = true }
udevlib = { package = "udev", version = "^0.8.0", optional = true }

[target.'cfg(target_os="macos")'.dependencies]
core-foundation = "0.9.3"
core-foundation-sys = "0.8.4"
io-kit-sys = "0.4.0"

[features]
libusb = ["dep:rusb"]
udev = ["dep:udevrs"]
Expand Down Expand Up @@ -83,6 +88,9 @@ pre-build = ["dpkg --add-architecture i386 && apt-get update && apt-get install
[package.metadata.cross.target.x86_64-unknown-linux-gnu]
pre-build = ["apt-get update && apt-get install --assume-yes libusb-1.0-0-dev libudev-dev"]

[package.metadata.cross.target.aarch64-linux-android]
image = "ghcr.io/cross-rs/aarch64-linux-android:main"

[package.metadata.deb]
section = "utility"
copyright = "2024, John Whittington <john@jbrengineering.co.uk>"
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ pub enum ErrorKind {
Udev,
/// Invalid arg for method or cli
InvalidArg,
/// Error calling IOKit
IoKit,
/// Error From other crate without enum variant
Other(&'static str),
/// Invalid device descriptor
Expand Down
16 changes: 9 additions & 7 deletions src/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,14 +1107,14 @@ mod platform {
#[cfg(target_os = "macos")]
mod platform {
use super::*;
use ::nusb::HostControllerInfo;
use macos::HostControllerInfo;

impl From<&HostControllerInfo> for PciInfo {
fn from(pci_info: &HostControllerInfo) -> Self {
impl From<HostControllerInfo> for PciInfo {
fn from(pci_info: HostControllerInfo) -> Self {
PciInfo {
vendor_id: pci_info.vendor_id(),
product_id: pci_info.device_id(),
revision: pci_info.revision_id(),
vendor_id: pci_info.vendor_id,
product_id: pci_info.device_id,
revision: pci_info.revision_id,
}
}
}
Expand All @@ -1126,7 +1126,9 @@ mod platform {

#[cfg(feature = "nusb")]
pub(crate) fn pci_info_from_bus(bus_info: &::nusb::BusInfo) -> Option<PciInfo> {
bus_info.host_controller_info().map(Into::into)
bus_info
.name()
.and_then(|name| macos::get_controller(name).ok().map(|c| c.into()))
}

#[cfg(feature = "nusb")]
Expand Down
227 changes: 225 additions & 2 deletions src/profiler/macos.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,232 @@
//! Parser for macOS `system_profiler` command -json output with SPUSBDataType. Merged with libusb or nusb for extra data.
//! macOS specific code for USB device profiling.
//!
//! Bus and Device structs are used as deserializers for serde. The JSON output with the -json flag is not really JSON; all values are String regardless of contained data so it requires some extra work. Additionally, some values differ slightly from the non json output such as the speed - it is a description rather than numerical.
//! Includes parser for macOS `system_profiler` command -json output with SPUSBDataType. Merged with libusb or nusb for extra data. Also includes IOKit functions for obtaining host controller data - helper code taken from [nusb](https://github.com/kevinmehall/nusb).
//!
//! `system_profiler`: Bus and Device structs are used as deserializers for serde. The JSON output with the -json flag is not really JSON; all values are String regardless of contained data so it requires some extra work. Additionally, some values differ slightly from the non json output such as the speed - it is a description rather than numerical.
use super::*;
use std::process::Command;

use core_foundation::{
base::{CFType, TCFType},
data::CFData,
string::CFString,
ConcreteCFType,
};
use io_kit_sys::{
kIOMasterPortDefault, kIORegistryIterateParents, kIORegistryIterateRecursively,
keys::kIOServicePlane, ret::kIOReturnSuccess, IOIteratorNext, IOObjectRelease,
IORegistryEntryGetRegistryEntryID, IORegistryEntrySearchCFProperty,
IOServiceGetMatchingServices, IOServiceNameMatching,
};

pub(crate) struct IoObject(u32);

impl IoObject {
// Safety: `handle` must be an IOObject handle. Ownership is transferred.
pub unsafe fn new(handle: u32) -> IoObject {
IoObject(handle)
}
pub fn get(&self) -> u32 {
self.0
}
}

impl Drop for IoObject {
fn drop(&mut self) {
unsafe {
IOObjectRelease(self.0);
}
}
}

pub(crate) struct IoService(IoObject);

impl IoService {
// Safety: `handle` must be an IOService handle. Ownership is transferred.
pub unsafe fn new(handle: u32) -> IoService {
IoService(IoObject(handle))
}
pub fn get(&self) -> u32 {
self.0 .0
}
}

pub(crate) struct IoServiceIterator(IoObject);

impl IoServiceIterator {
// Safety: `handle` must be an IoIterator of IoService. Ownership is transferred.
pub unsafe fn new(handle: u32) -> IoServiceIterator {
IoServiceIterator(IoObject::new(handle))
}
}

impl Iterator for IoServiceIterator {
type Item = IoService;

fn next(&mut self) -> Option<Self::Item> {
unsafe {
let handle = IOIteratorNext(self.0.get());
if handle != 0 {
Some(IoService::new(handle))
} else {
None
}
}
}
}

pub(crate) struct HostControllerInfo {
pub(crate) name: String,
pub(crate) class_name: String,
pub(crate) io_name: String,
pub(crate) registry_id: u64,
pub(crate) vendor_id: u16,
pub(crate) device_id: u16,
pub(crate) revision_id: u16,
pub(crate) class_code: u32,
pub(crate) subsystem_vendor_id: Option<u16>,
pub(crate) subsystem_id: Option<u16>,
}

impl std::fmt::Debug for HostControllerInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PciControllerInfo")
.field("name", &self.name)
.field("class_name", &self.class_name)
.field("io_name", &self.io_name)
.field("registry_id", &format!("{:08x}", self.registry_id))
.field("vendor_id", &format!("{:04x}", self.vendor_id))
.field("device_id", &format!("{:04x}", self.device_id))
.field("revision_id", &format!("{:04x}", self.revision_id))
.field("class_code", &format!("{:08x}", self.class_code))
.field("subsystem_vendor_id", &self.subsystem_vendor_id)
.field("subsystem_id", &self.subsystem_id)
.finish()
}
}

pub(crate) fn get_registry_id(device: &IoService) -> Option<u64> {
unsafe {
let mut out = 0;
let r = IORegistryEntryGetRegistryEntryID(device.get(), &mut out);

if r == kIOReturnSuccess {
Some(out)
} else {
// not sure this can actually fail.
log::debug!("IORegistryEntryGetRegistryEntryID failed with {r}");
None
}
}
}

fn get_property<T: ConcreteCFType>(device: &IoService, property: &'static str) -> Option<T> {
unsafe {
let cf_property = CFString::from_static_string(property);

let raw = IORegistryEntrySearchCFProperty(
device.get(),
kIOServicePlane as *mut i8,
cf_property.as_CFTypeRef() as *const _,
std::ptr::null(),
kIORegistryIterateRecursively | kIORegistryIterateParents,
);

if raw.is_null() {
log::debug!("Device does not have property `{property}`");
return None;
}

let res = CFType::wrap_under_create_rule(raw).downcast_into();

if res.is_none() {
log::debug!("Failed to convert device property `{property}`");
}

res
}
}

fn get_string_property(device: &IoService, property: &'static str) -> Option<String> {
get_property::<CFString>(device, property).map(|s| s.to_string())
}

fn get_byte_array_property(device: &IoService, property: &'static str) -> Option<Vec<u8>> {
let d = get_property::<CFData>(device, property)?;
Some(d.bytes().to_vec())
}

fn get_ascii_array_property(device: &IoService, property: &'static str) -> Option<String> {
let d = get_property::<CFData>(device, property)?;
Some(
d.bytes()
.iter()
.map(|b| *b as char)
.filter(|c| *c != '\0')
.collect(),
)
}

pub(crate) fn probe_controller(device: IoService) -> Option<HostControllerInfo> {
let registry_id = get_registry_id(&device)?;
log::debug!("Probing controller {registry_id:08x}");

// name is a CFData of ASCII characters
let name = get_ascii_array_property(&device, "name")?;

let class_name = get_string_property(&device, "IOClass")?;
let io_name = get_string_property(&device, "IOName")?;

let vendor_id =
get_byte_array_property(&device, "vendor-id").map(|v| u16::from_le_bytes([v[0], v[1]]))?;
let device_id =
get_byte_array_property(&device, "device-id").map(|v| u16::from_le_bytes([v[0], v[1]]))?;
let revision_id = get_byte_array_property(&device, "revision-id")
.map(|v| u16::from_le_bytes([v[0], v[1]]))?;
let class_code = get_byte_array_property(&device, "class-code")
.map(|v| u32::from_le_bytes([v[0], v[1], v[2], v[3]]))?;
let subsystem_vendor_id = get_byte_array_property(&device, "subsystem-vendor-id")
.map(|v| u16::from_le_bytes([v[0], v[1]]));
let subsystem_id =
get_byte_array_property(&device, "subsystem-id").map(|v| u16::from_le_bytes([v[0], v[1]]));

Some(HostControllerInfo {
name,
class_name,
io_name,
registry_id,
vendor_id,
device_id,
revision_id,
class_code,
subsystem_vendor_id,
subsystem_id,
})
}

pub(crate) fn get_controller(name: &str) -> Result<HostControllerInfo> {
unsafe {
let dictionary = IOServiceNameMatching(name.as_ptr() as *const i8);
if dictionary.is_null() {
return Err(Error::new(ErrorKind::IoKit, "IOServiceMatching failed"));
}

let mut iterator = 0;
let r = IOServiceGetMatchingServices(kIOMasterPortDefault, dictionary, &mut iterator);
if r != kIOReturnSuccess {
return Err(Error::new(ErrorKind::IoKit, &r.to_string()));
}

IoServiceIterator::new(iterator)
.next()
.and_then(probe_controller)
.ok_or(Error::new(
ErrorKind::IoKit,
&format!("No controller found for {}", name),
))
}
}

/// Runs the system_profiler command for SPUSBDataType and parses the json stdout into a [`SystemProfile`].
///
/// Ok result not contain [`usb::DeviceExtra`] because system_profiler does not provide this. Use `get_spusb_with_extra` to combine with libusb output for [`Device`]s with `extra`
Expand Down