windows: Hotplug
This commit is contained in:
parent
1f7a58700f
commit
15af931665
11 changed files with 317 additions and 1 deletions
|
|
@ -12,6 +12,7 @@ rust-version = "1.74"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
atomic-waker = "1.1.2"
|
atomic-waker = "1.1.2"
|
||||||
|
futures-core = "0.3.29"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
slab = "0.4.9"
|
slab = "0.4.9"
|
||||||
|
|
|
||||||
8
examples/hotplug.rs
Normal file
8
examples/hotplug.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,10 @@ use crate::platform::SysfsPath;
|
||||||
|
|
||||||
use crate::{Device, Error};
|
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.
|
/// Information about a device that can be obtained without opening it.
|
||||||
///
|
///
|
||||||
/// Found in the results of [`crate::list_devices`].
|
/// Found in the results of [`crate::list_devices`].
|
||||||
|
|
@ -63,6 +67,29 @@ pub struct DeviceInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// *(Linux-only)* Sysfs path for the device.
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[deprecated = "use `sysfs_path()` instead"]
|
#[deprecated = "use `sysfs_path()` instead"]
|
||||||
|
|
|
||||||
35
src/hotplug.rs
Normal file
35
src/hotplug.rs
Normal 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),
|
||||||
|
}
|
||||||
36
src/lib.rs
36
src/lib.rs
|
|
@ -120,13 +120,15 @@ mod platform;
|
||||||
|
|
||||||
pub mod descriptors;
|
pub mod descriptors;
|
||||||
mod enumeration;
|
mod enumeration;
|
||||||
pub use enumeration::{DeviceInfo, InterfaceInfo, Speed};
|
pub use enumeration::{DeviceId, DeviceInfo, InterfaceInfo, Speed};
|
||||||
|
|
||||||
mod device;
|
mod device;
|
||||||
pub use device::{Device, Interface};
|
pub use device::{Device, Interface};
|
||||||
|
|
||||||
pub mod transfer;
|
pub mod transfer;
|
||||||
|
|
||||||
|
pub mod hotplug;
|
||||||
|
|
||||||
/// OS error returned from operations other than transfers.
|
/// OS error returned from operations other than transfers.
|
||||||
pub type Error = io::Error;
|
pub type Error = io::Error;
|
||||||
|
|
||||||
|
|
@ -146,3 +148,35 @@ pub type Error = io::Error;
|
||||||
pub fn list_devices() -> Result<impl Iterator<Item = DeviceInfo>, Error> {
|
pub fn list_devices() -> Result<impl Iterator<Item = DeviceInfo>, Error> {
|
||||||
platform::list_devices()
|
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()?))
|
||||||
|
}
|
||||||
|
|
|
||||||
15
src/platform/linux_usbfs/hotplug.rs
Normal file
15
src/platform/linux_usbfs/hotplug.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,17 @@ mod device;
|
||||||
pub(crate) use device::LinuxDevice as Device;
|
pub(crate) use device::LinuxDevice as Device;
|
||||||
pub(crate) use device::LinuxInterface as Interface;
|
pub(crate) use device::LinuxInterface as Interface;
|
||||||
|
|
||||||
|
mod hotplug;
|
||||||
|
pub(crate) use hotplug::LinuxHotplugWatch as HotplugWatch;
|
||||||
|
|
||||||
use crate::transfer::TransferError;
|
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 {
|
fn errno_to_transfer_error(e: Errno) -> TransferError {
|
||||||
match e {
|
match e {
|
||||||
Errno::NODEV | Errno::SHUTDOWN => TransferError::Disconnected,
|
Errno::NODEV | Errno::SHUTDOWN => TransferError::Disconnected,
|
||||||
|
|
|
||||||
15
src/platform/macos_iokit/hotplug.rs
Normal file
15
src/platform/macos_iokit/hotplug.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,12 +12,20 @@ mod device;
|
||||||
pub(crate) use device::MacDevice as Device;
|
pub(crate) use device::MacDevice as Device;
|
||||||
pub(crate) use device::MacInterface as Interface;
|
pub(crate) use device::MacInterface as Interface;
|
||||||
|
|
||||||
|
mod hotplug;
|
||||||
|
pub(crate) use hotplug::MacHotplugWatch as HotplugWatch;
|
||||||
|
|
||||||
use crate::transfer::TransferError;
|
use crate::transfer::TransferError;
|
||||||
|
|
||||||
mod iokit;
|
mod iokit;
|
||||||
mod iokit_c;
|
mod iokit_c;
|
||||||
mod iokit_usb;
|
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> {
|
fn status_to_transfer_result(status: IOReturn) -> Result<(), TransferError> {
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
#[deny(unreachable_patterns)]
|
#[deny(unreachable_patterns)]
|
||||||
|
|
|
||||||
161
src/platform/windows_winusb/hotplug.rs
Normal file
161
src/platform/windows_winusb/hotplug.rs
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -14,4 +14,7 @@ mod cfgmgr32;
|
||||||
mod hub;
|
mod hub;
|
||||||
mod registry;
|
mod registry;
|
||||||
pub(crate) use cfgmgr32::DevInst;
|
pub(crate) use cfgmgr32::DevInst;
|
||||||
|
pub(crate) use DevInst as DeviceId;
|
||||||
|
mod hotplug;
|
||||||
mod util;
|
mod util;
|
||||||
|
pub(crate) use hotplug::WindowsHotplugWatch as HotplugWatch;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue