From 15af931665c2789cdd464adddf36384f6d0a3b2a Mon Sep 17 00:00:00 2001 From: Kevin Mehall Date: Sat, 23 Dec 2023 13:24:48 -0700 Subject: [PATCH] windows: Hotplug --- Cargo.toml | 1 + examples/hotplug.rs | 8 ++ src/enumeration.rs | 27 +++++ src/hotplug.rs | 35 ++++++ src/lib.rs | 36 +++++- src/platform/linux_usbfs/hotplug.rs | 15 +++ src/platform/linux_usbfs/mod.rs | 9 ++ src/platform/macos_iokit/hotplug.rs | 15 +++ src/platform/macos_iokit/mod.rs | 8 ++ src/platform/windows_winusb/hotplug.rs | 161 +++++++++++++++++++++++++ src/platform/windows_winusb/mod.rs | 3 + 11 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 examples/hotplug.rs create mode 100644 src/hotplug.rs create mode 100644 src/platform/linux_usbfs/hotplug.rs create mode 100644 src/platform/macos_iokit/hotplug.rs create mode 100644 src/platform/windows_winusb/hotplug.rs diff --git a/Cargo.toml b/Cargo.toml index 3994ddf..0adae90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ rust-version = "1.74" [dependencies] atomic-waker = "1.1.2" +futures-core = "0.3.29" log = "0.4.20" once_cell = "1.18.0" slab = "0.4.9" diff --git a/examples/hotplug.rs b/examples/hotplug.rs new file mode 100644 index 0000000..52dffd2 --- /dev/null +++ b/examples/hotplug.rs @@ -0,0 +1,8 @@ +use futures_lite::stream; + +fn main() { + env_logger::init(); + for event in stream::block_on(nusb::watch_devices().unwrap()) { + println!("{:#?}", event); + } +} diff --git a/src/enumeration.rs b/src/enumeration.rs index d22a2ae..8eed606 100644 --- a/src/enumeration.rs +++ b/src/enumeration.rs @@ -6,6 +6,10 @@ use crate::platform::SysfsPath; use crate::{Device, Error}; +/// Opaque device identifier +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +pub struct DeviceId(pub(crate) crate::platform::DeviceId); + /// Information about a device that can be obtained without opening it. /// /// Found in the results of [`crate::list_devices`]. @@ -63,6 +67,29 @@ pub struct DeviceInfo { } impl DeviceInfo { + /// Opaque identifier for the device. + pub fn id(&self) -> DeviceId { + #[cfg(target_os = "windows")] + { + DeviceId(self.devinst) + } + + #[cfg(target_os = "linux")] + { + DeviceId(crate::platform::DeviceId { + bus: self.bus_number, + addr: self.device_address, + }) + } + + #[cfg(target_os = "macos")] + { + DeviceId(crate::platform::DeviceId { + registry_id: self.registry_id, + }) + } + } + /// *(Linux-only)* Sysfs path for the device. #[doc(hidden)] #[deprecated = "use `sysfs_path()` instead"] diff --git a/src/hotplug.rs b/src/hotplug.rs new file mode 100644 index 0000000..7bdc681 --- /dev/null +++ b/src/hotplug.rs @@ -0,0 +1,35 @@ +//! Types for receiving notifications when USB devices are connected or +//! disconnected from the system. +//! +//! See [`super::watch_devices`] for a usage example. + +use futures_core::Stream; + +use crate::{DeviceId, DeviceInfo}; + +/// Stream of device connection / disconnection events. +/// +/// Call [`super::watch_devices`] to begin watching device +/// events and create a `HotplugWatch`. +pub struct HotplugWatch(pub(crate) crate::platform::HotplugWatch); + +impl Stream for HotplugWatch { + type Item = HotplugEvent; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.poll_next(cx).map(Some) + } +} + +/// Event returned from the [`HotplugWatch`] stream. +#[derive(Debug)] +pub enum HotplugEvent { + /// A device has been connected. + Connected(DeviceInfo), + + /// A device has been disconnected. + Disconnected(DeviceId), +} diff --git a/src/lib.rs b/src/lib.rs index 7d882c6..553309b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,13 +120,15 @@ mod platform; pub mod descriptors; mod enumeration; -pub use enumeration::{DeviceInfo, InterfaceInfo, Speed}; +pub use enumeration::{DeviceId, DeviceInfo, InterfaceInfo, Speed}; mod device; pub use device::{Device, Interface}; pub mod transfer; +pub mod hotplug; + /// OS error returned from operations other than transfers. pub type Error = io::Error; @@ -146,3 +148,35 @@ pub type Error = io::Error; pub fn list_devices() -> Result, Error> { platform::list_devices() } + +/// Get a [`Stream`][`futures_core::Stream`] that yields an +/// [event][`hotplug::HotplugEvent`] when a USB device is connected or +/// disconnected from the system. +/// +/// Events will be returned for devices connected or disconnected beginning at +/// the time this function is called. To maintain a list of connected devices, +/// call [`list_devices`] after creating the watch with this function to avoid +/// potentially missing a newly-attached device: +/// +/// ## Example +/// +/// ```no_run +/// use std::collections::HashMap; +/// use nusb::{DeviceInfo, DeviceId, hotplug::HotplugEvent}; +/// let watch = nusb::watch_devices().unwrap(); +/// let mut devices: HashMap = nusb::list_devices().unwrap() +/// .map(|d| (d.id(), d)).collect(); +/// for event in futures_lite::stream::block_on(watch) { +/// match event { +/// HotplugEvent::Connected(d) => { +/// devices.insert(d.id(), d); +/// } +/// HotplugEvent::Disconnected(id) => { +/// devices.remove(&id); +/// } +/// } +/// } +/// ``` +pub fn watch_devices() -> Result { + Ok(hotplug::HotplugWatch(platform::HotplugWatch::new()?)) +} diff --git a/src/platform/linux_usbfs/hotplug.rs b/src/platform/linux_usbfs/hotplug.rs new file mode 100644 index 0000000..ba0c6a9 --- /dev/null +++ b/src/platform/linux_usbfs/hotplug.rs @@ -0,0 +1,15 @@ +use std::{io::ErrorKind, task::Poll}; + +use crate::{hotplug::HotplugEvent, Error}; + +pub(crate) struct LinuxHotplugWatch {} + +impl LinuxHotplugWatch { + pub(crate) fn new() -> Result { + Err(Error::new(ErrorKind::Unsupported, "Not implemented.")) + } + + pub(crate) fn poll_next(&mut self, cx: &mut std::task::Context<'_>) -> Poll { + Poll::Pending + } +} diff --git a/src/platform/linux_usbfs/mod.rs b/src/platform/linux_usbfs/mod.rs index 9671aa0..6159028 100644 --- a/src/platform/linux_usbfs/mod.rs +++ b/src/platform/linux_usbfs/mod.rs @@ -11,8 +11,17 @@ mod device; pub(crate) use device::LinuxDevice as Device; pub(crate) use device::LinuxInterface as Interface; +mod hotplug; +pub(crate) use hotplug::LinuxHotplugWatch as HotplugWatch; + use crate::transfer::TransferError; +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub struct DeviceId { + pub(crate) bus: u8, + pub(crate) addr: u8, +} + fn errno_to_transfer_error(e: Errno) -> TransferError { match e { Errno::NODEV | Errno::SHUTDOWN => TransferError::Disconnected, diff --git a/src/platform/macos_iokit/hotplug.rs b/src/platform/macos_iokit/hotplug.rs new file mode 100644 index 0000000..0394ee0 --- /dev/null +++ b/src/platform/macos_iokit/hotplug.rs @@ -0,0 +1,15 @@ +use std::{io::ErrorKind, task::Poll}; + +use crate::{hotplug::HotplugEvent, Error}; + +pub(crate) struct MacHotplugWatch {} + +impl MacHotplugWatch { + pub(crate) fn new() -> Result { + Err(Error::new(ErrorKind::Unsupported, "Not implemented.")) + } + + pub(crate) fn poll_next(&mut self, cx: &mut std::task::Context<'_>) -> Poll { + Poll::Pending + } +} diff --git a/src/platform/macos_iokit/mod.rs b/src/platform/macos_iokit/mod.rs index e29a099..3b296a0 100644 --- a/src/platform/macos_iokit/mod.rs +++ b/src/platform/macos_iokit/mod.rs @@ -12,12 +12,20 @@ mod device; pub(crate) use device::MacDevice as Device; pub(crate) use device::MacInterface as Interface; +mod hotplug; +pub(crate) use hotplug::MacHotplugWatch as HotplugWatch; + use crate::transfer::TransferError; mod iokit; mod iokit_c; mod iokit_usb; +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub struct DeviceId { + pub(crate) registry_id: u64, +} + fn status_to_transfer_result(status: IOReturn) -> Result<(), TransferError> { #[allow(non_upper_case_globals)] #[deny(unreachable_patterns)] diff --git a/src/platform/windows_winusb/hotplug.rs b/src/platform/windows_winusb/hotplug.rs new file mode 100644 index 0000000..67cf96b --- /dev/null +++ b/src/platform/windows_winusb/hotplug.rs @@ -0,0 +1,161 @@ +use std::{ + collections::VecDeque, + ffi::c_void, + io::ErrorKind, + mem::size_of, + ptr::addr_of, + sync::Mutex, + task::{Context, Poll}, +}; + +use atomic_waker::AtomicWaker; +use log::{debug, error}; +use windows_sys::Win32::{ + Devices::{ + DeviceAndDriverInstallation::{ + CM_Register_Notification, CM_Unregister_Notification, CM_NOTIFY_ACTION, + CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL, CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL, + CM_NOTIFY_EVENT_DATA, CM_NOTIFY_FILTER, CM_NOTIFY_FILTER_0, CM_NOTIFY_FILTER_0_2, + CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE, CR_SUCCESS, HCMNOTIFICATION, + }, + Properties::DEVPKEY_Device_InstanceId, + Usb::GUID_DEVINTERFACE_USB_DEVICE, + }, + Foundation::ERROR_SUCCESS, +}; + +use crate::{ + hotplug::HotplugEvent, + platform::windows_winusb::{cfgmgr32::get_device_interface_property, util::WCString}, + DeviceId, Error, +}; + +use super::{enumeration::probe_device, util::WCStr}; + +use super::DevInst; + +pub(crate) struct WindowsHotplugWatch { + inner: *mut HotplugInner, + registration: HCMNOTIFICATION, +} + +struct HotplugInner { + waker: AtomicWaker, + events: Mutex>, +} + +#[derive(Debug)] +enum Action { + Connect, + Disconnect, +} + +impl WindowsHotplugWatch { + pub fn new() -> Result { + let inner = Box::into_raw(Box::new(HotplugInner { + events: Mutex::new(VecDeque::new()), + waker: AtomicWaker::new(), + })); + + let mut registration = 0; + let filter = CM_NOTIFY_FILTER { + cbSize: size_of::() as u32, + Flags: 0, + FilterType: CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE, + Reserved: 0, + u: CM_NOTIFY_FILTER_0 { + DeviceInterface: CM_NOTIFY_FILTER_0_2 { + ClassGuid: GUID_DEVINTERFACE_USB_DEVICE, + }, + }, + }; + + let cr = unsafe { + CM_Register_Notification( + &filter, + inner as *mut c_void, + Some(hotplug_callback), + &mut registration, + ) + }; + + if cr != CR_SUCCESS { + error!("CM_Register_Notification failed: {cr}"); + return Err(Error::new( + ErrorKind::Other, + "Failed to initialize hotplug notifications", + )); + } + + Ok(WindowsHotplugWatch { + inner, + registration, + }) + } + + fn inner(&self) -> &HotplugInner { + unsafe { &*self.inner } + } + + pub fn poll_next(&mut self, cx: &mut Context) -> Poll { + self.inner().waker.register(cx.waker()); + let event = self.inner().events.lock().unwrap().pop_front(); + match event { + Some((Action::Connect, devinst)) => { + if let Some(dev) = probe_device(devinst) { + return Poll::Ready(HotplugEvent::Connected(dev)); + }; + } + Some((Action::Disconnect, devinst)) => { + return Poll::Ready(HotplugEvent::Disconnected(DeviceId(devinst))); + } + None => {} + } + Poll::Pending + } +} + +impl Drop for WindowsHotplugWatch { + fn drop(&mut self) { + unsafe { + // According to [1], `CM_Unregister_Notification` waits for + // callbacks to finish, so it should be safe to drop `inner` + // immediately afterward without races. + // [1]: https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/nf-cfgmgr32-cm_unregister_notification + CM_Unregister_Notification(self.registration); + drop(Box::from_raw(self.inner)); + } + } +} + +unsafe extern "system" fn hotplug_callback( + _hnotify: HCMNOTIFICATION, + context: *const ::core::ffi::c_void, + action: CM_NOTIFY_ACTION, + eventdata: *const CM_NOTIFY_EVENT_DATA, + _eventdatasize: u32, +) -> u32 { + let inner = unsafe { &*(context as *const HotplugInner) }; + + let action = match action { + CM_NOTIFY_ACTION_DEVICEINTERFACEARRIVAL => Action::Connect, + CM_NOTIFY_ACTION_DEVICEINTERFACEREMOVAL => Action::Disconnect, + _ => { + debug!("Hotplug callback: unknown action {action}"); + return ERROR_SUCCESS; + } + }; + + let device_interface = + unsafe { WCStr::from_ptr(addr_of!((*eventdata).u.DeviceInterface.SymbolicLink[0])) }; + + let device_instance = + get_device_interface_property::(device_interface, DEVPKEY_Device_InstanceId) + .unwrap(); + let devinst = DevInst::from_instance_id(&device_instance).unwrap(); + + debug!("Hotplug callback: action={action:?}, instance={device_instance}"); + inner.events.lock().unwrap().push_back((action, devinst)); + inner.waker.wake(); + return ERROR_SUCCESS; +} diff --git a/src/platform/windows_winusb/mod.rs b/src/platform/windows_winusb/mod.rs index bb03e17..ef4c815 100644 --- a/src/platform/windows_winusb/mod.rs +++ b/src/platform/windows_winusb/mod.rs @@ -14,4 +14,7 @@ mod cfgmgr32; mod hub; mod registry; pub(crate) use cfgmgr32::DevInst; +pub(crate) use DevInst as DeviceId; +mod hotplug; mod util; +pub(crate) use hotplug::WindowsHotplugWatch as HotplugWatch;