windows: Hotplug

This commit is contained in:
Kevin Mehall 2023-12-23 13:24:48 -07:00
parent 1f7a58700f
commit 15af931665
11 changed files with 317 additions and 1 deletions

View file

@ -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"

8
examples/hotplug.rs Normal file
View file

@ -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);
}
}

View file

@ -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"]

35
src/hotplug.rs Normal file
View file

@ -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<Option<Self::Item>> {
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),
}

View file

@ -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<impl Iterator<Item = DeviceInfo>, 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<DeviceId, DeviceInfo> = 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<hotplug::HotplugWatch, Error> {
Ok(hotplug::HotplugWatch(platform::HotplugWatch::new()?))
}

View file

@ -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<Self, Error> {
Err(Error::new(ErrorKind::Unsupported, "Not implemented."))
}
pub(crate) fn poll_next(&mut self, cx: &mut std::task::Context<'_>) -> Poll<HotplugEvent> {
Poll::Pending
}
}

View file

@ -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,

View file

@ -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<Self, Error> {
Err(Error::new(ErrorKind::Unsupported, "Not implemented."))
}
pub(crate) fn poll_next(&mut self, cx: &mut std::task::Context<'_>) -> Poll<HotplugEvent> {
Poll::Pending
}
}

View file

@ -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)]

View file

@ -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<VecDeque<(Action, DevInst)>>,
}
#[derive(Debug)]
enum Action {
Connect,
Disconnect,
}
impl WindowsHotplugWatch {
pub fn new() -> Result<WindowsHotplugWatch, Error> {
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::<CM_NOTIFY_FILTER>() 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<HotplugEvent> {
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::<WCString>(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;
}

View file

@ -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;