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]
|
||||
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
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};
|
||||
|
||||
/// 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
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;
|
||||
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()?))
|
||||
}
|
||||
|
|
|
|||
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::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,
|
||||
|
|
|
|||
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::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)]
|
||||
|
|
|
|||
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 registry;
|
||||
pub(crate) use cfgmgr32::DevInst;
|
||||
pub(crate) use DevInst as DeviceId;
|
||||
mod hotplug;
|
||||
mod util;
|
||||
pub(crate) use hotplug::WindowsHotplugWatch as HotplugWatch;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue