use std::collections::VecDeque;
use std::future::Future;
use std::num::NonZeroU8;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use nusb::descriptors::TransferType;
use nusb::transfer::{
ControlIn, ControlOut, ControlType, Direction, In, Interrupt, Out, Recipient, TransferError,
};
use nusb::{Endpoint, Interface, MaybeFuture};
use crate::descriptor::{ReportDescriptor, ReportKind};
use crate::error::{HidError, HidResult};
use crate::{BusType, DeviceInfo, MAX_REPORT_DESCRIPTOR_SIZE};
const USB_CLASS_HID: u8 = 3;
const GET_DESCRIPTOR: u8 = 0x06;
const DESCRIPTOR_TYPE_HID_REPORT: u8 = 0x22;
const HID_GET_REPORT: u8 = 0x01;
const HID_SET_REPORT: u8 = 0x09;
const REPORT_TYPE_INPUT: u16 = 1;
const REPORT_TYPE_OUTPUT: u16 = 2;
const REPORT_TYPE_FEATURE: u16 = 3;
const CONTROL_TIMEOUT: Duration = Duration::from_millis(1000);
const READER_POLL_INTERVAL: Duration = Duration::from_millis(100);
const MAX_QUEUED_REPORTS: usize = 30;
const US_ENGLISH: u16 = 0x0409;
fn format_path(bus_id: &str, device_address: u8, interface_number: u8) -> String {
format!("usb:{bus_id}:{device_address}:{interface_number}")
}
fn parse_path(path: &str) -> Option<(&str, u8, u8)> {
let rest = path.strip_prefix("usb:")?;
let mut parts = rest.rsplitn(3, ':');
let interface_number = parts.next()?.parse().ok()?;
let device_address = parts.next()?.parse().ok()?;
let bus_id = parts.next()?;
(!bus_id.is_empty()).then_some((bus_id, device_address, interface_number))
}
fn transfer_error(operation: &'static str, err: TransferError) -> HidError {
match err {
TransferError::Disconnected => HidError::Disconnected,
TransferError::Cancelled => HidError::backend(format!("{operation}: timed out")),
e => HidError::backend(format!("{operation}: {e}")),
}
}
fn device_info(dev: &nusb::DeviceInfo, interface_number: u8) -> DeviceInfo {
DeviceInfo {
path: format_path(dev.bus_id(), dev.device_address(), interface_number),
vendor_id: dev.vendor_id(),
product_id: dev.product_id(),
serial_number: dev.serial_number().map(str::to_string),
release_number: dev.device_version(),
manufacturer_string: dev.manufacturer_string().map(str::to_string),
product_string: dev.product_string().map(str::to_string),
interface_number: i32::from(interface_number),
bus_type: BusType::Usb,
..Default::default()
}
}
fn push_usage_entries(mut info: DeviceInfo, usages: &[(u16, u16)], out: &mut Vec<DeviceInfo>) {
match usages {
[] => out.push(info),
[first @ .., last] => {
for &(page, usage) in first {
let mut e = info.clone();
e.usage_page = page;
e.usage = usage;
out.push(e);
}
info.usage_page = last.0;
info.usage = last.1;
out.push(info);
}
}
}
const DESCRIPTOR_TYPE_HID: u8 = 0x21;
fn declared_report_descriptor_len<'a, I>(alt_settings: I, interface_number: u8) -> Option<usize>
where
I: Iterator<Item = nusb::descriptors::InterfaceDescriptor<'a>>,
{
alt_settings
.filter(|alt| alt.interface_number() == interface_number && alt.alternate_setting() == 0)
.flat_map(|alt| alt.descriptors())
.find(|d| d.descriptor_type() == DESCRIPTOR_TYPE_HID && d.len() >= 9)
.map(|d| u16::from_le_bytes([d[7], d[8]]) as usize)
}
fn report_descriptor_request(interface_number: u8, length: usize) -> ControlIn {
ControlIn {
control_type: ControlType::Standard,
recipient: Recipient::Interface,
request: GET_DESCRIPTOR,
value: u16::from(DESCRIPTOR_TYPE_HID_REPORT) << 8,
index: u16::from(interface_number),
length: length.min(MAX_REPORT_DESCRIPTOR_SIZE) as u16,
}
}
fn read_report_descriptor(interface: &Interface) -> Option<Vec<u8>> {
let interface_number = interface.interface_number();
let length = declared_report_descriptor_len(interface.descriptors(), interface_number)
.unwrap_or(MAX_REPORT_DESCRIPTOR_SIZE);
let mut data = interface
.control_in(
report_descriptor_request(interface_number, length),
CONTROL_TIMEOUT,
)
.wait()
.ok()
.filter(|d| !d.is_empty())?;
data.truncate(length);
Some(data)
}
#[cfg(not(target_os = "windows"))]
fn read_report_descriptor_unclaimed(
device: &nusb::Device,
interface_number: u8,
) -> Option<Vec<u8>> {
let length = device
.active_configuration()
.ok()
.and_then(|c| declared_report_descriptor_len(c.interface_alt_settings(), interface_number))
.unwrap_or(MAX_REPORT_DESCRIPTOR_SIZE);
let mut data = device
.control_in(
report_descriptor_request(interface_number, length),
CONTROL_TIMEOUT,
)
.wait()
.ok()
.filter(|d| !d.is_empty())?;
data.truncate(length);
Some(data)
}
#[cfg(target_os = "windows")]
fn read_report_descriptor_unclaimed(
_device: &nusb::Device,
_interface_number: u8,
) -> Option<Vec<u8>> {
None
}
fn transfer_length(max_input_wire: usize, max_packet_size: usize) -> usize {
max_input_wire
.max(max_packet_size)
.div_ceil(max_packet_size)
* max_packet_size
}
pub(crate) struct NusbApi {
_private: (),
}
impl NusbApi {
pub fn new() -> HidResult<Self> {
Ok(NusbApi { _private: () })
}
pub fn enumerate(&self, vendor_id: u16, product_id: u16) -> HidResult<Vec<DeviceInfo>> {
let devices = match nusb::list_devices().wait() {
Ok(devices) => devices,
#[cfg(target_os = "linux")]
Err(e)
if e.kind() == nusb::ErrorKind::Other
&& e.os_error() == Some(libc::ENOENT as u32) =>
{
return Ok(Vec::new())
}
Err(e) => return Err(HidError::backend(format!("listing USB devices: {e}"))),
};
let mut result = Vec::new();
for dev in devices {
let vid_ok = vendor_id == 0 || dev.vendor_id() == vendor_id;
let pid_ok = product_id == 0 || dev.product_id() == product_id;
if !vid_ok || !pid_ok {
continue;
}
let hid_interfaces: Vec<u8> = dev
.interfaces()
.filter(|i| i.class() == USB_CLASS_HID)
.map(|i| i.interface_number())
.collect();
if hid_interfaces.is_empty() {
continue;
}
let opened = dev.open().wait().ok();
for interface_number in hid_interfaces {
let info = device_info(&dev, interface_number);
let usages = opened
.as_ref()
.and_then(|d| read_report_descriptor_unclaimed(d, interface_number))
.and_then(|bytes| ReportDescriptor::parse(&bytes).ok())
.map(|d| d.top_level_usages())
.unwrap_or_default();
push_usage_entries(info, &usages, &mut result);
}
}
Ok(result)
}
pub fn open(
&self,
vendor_id: u16,
product_id: u16,
serial: Option<&str>,
) -> HidResult<NusbDevice> {
let candidates = self.enumerate(vendor_id, product_id)?;
let info = candidates
.into_iter()
.find(|info| match serial {
Some(s) => info.serial_number.as_deref() == Some(s),
None => true,
})
.ok_or(HidError::DeviceNotFound)?;
self.open_path(&info.path)
}
pub fn open_path(&self, path: &str) -> HidResult<NusbDevice> {
let (bus_id, device_address, interface_number) =
parse_path(path).ok_or_else(|| HidError::InvalidData {
message: format!("invalid USB device path: {path}"),
})?;
let mut devices = nusb::list_devices()
.wait()
.map_err(|e| HidError::backend(format!("listing USB devices: {e}")))?;
let dev = devices
.find(|d| d.bus_id() == bus_id && d.device_address() == device_address)
.ok_or(HidError::DeviceNotFound)?;
NusbDevice::open(&dev, interface_number)
}
}
#[derive(Default)]
struct InputQueue {
reports: VecDeque<Vec<u8>>,
wakers: Vec<Waker>,
}
#[derive(Default)]
struct Shared {
queue: Mutex<InputQueue>,
data_available: Condvar,
shutdown: AtomicBool,
disconnected: AtomicBool,
}
impl Shared {
fn push_report(&self, report: Vec<u8>) {
let mut input = self.queue.lock().unwrap();
if input.reports.len() >= MAX_QUEUED_REPORTS {
input.reports.pop_front(); }
input.reports.push_back(report);
let wakers = std::mem::take(&mut input.wakers);
drop(input);
self.data_available.notify_all();
for waker in wakers {
waker.wake();
}
}
fn wake_readers(&self) {
let wakers = std::mem::take(&mut self.queue.lock().unwrap().wakers);
self.data_available.notify_all();
for waker in wakers {
waker.wake();
}
}
fn poll_read(&self, buf: &mut [u8], cx: &mut Context<'_>) -> Poll<HidResult<usize>> {
if buf.is_empty() {
return Poll::Ready(Err(HidError::InvalidData {
message: "read buffer must not be empty".into(),
}));
}
let mut input = self.queue.lock().unwrap();
if let Some(report) = input.reports.pop_front() {
let len = report.len().min(buf.len());
buf[..len].copy_from_slice(&report[..len]);
return Poll::Ready(Ok(len));
}
if self.disconnected.load(Ordering::SeqCst) {
return Poll::Ready(Err(HidError::Disconnected));
}
if self.shutdown.load(Ordering::SeqCst) {
return Poll::Ready(Err(HidError::backend("USB reader thread terminated")));
}
if !input.wakers.iter().any(|w| w.will_wake(cx.waker())) {
input.wakers.push(cx.waker().clone());
}
Poll::Pending
}
}
pub(crate) struct NusbDevice {
device: nusb::Device,
interface: Interface,
interface_number: u8,
info: DeviceInfo,
report_descriptor: Vec<u8>,
out_endpoint: Option<Mutex<Endpoint<Interrupt, Out>>>,
#[allow(dead_code)]
blocking: AtomicBool,
shared: Arc<Shared>,
reader: Option<JoinHandle<()>>,
}
impl NusbDevice {
fn open(dev_info: &nusb::DeviceInfo, interface_number: u8) -> HidResult<Self> {
let device = dev_info.open().wait().map_err(|e| HidError::OpenFailed {
message: format!("opening USB device: {e}"),
})?;
let interface = device
.detach_and_claim_interface(interface_number)
.wait()
.map_err(|e| HidError::OpenFailed {
message: format!("claiming interface {interface_number}: {e}"),
})?;
let report_descriptor = read_report_descriptor(&interface).unwrap_or_default();
let parsed = ReportDescriptor::parse(&report_descriptor).ok();
let max_input_wire = parsed
.as_ref()
.map(|d| d.max_wire_size(ReportKind::Input))
.unwrap_or(0);
let mut in_address = None;
let mut out_address = None;
let alt0 = interface
.descriptors()
.find(|d| d.alternate_setting() == 0)
.or_else(|| interface.descriptor());
if let Some(desc) = alt0 {
for ep in desc.endpoints() {
if ep.transfer_type() != TransferType::Interrupt {
continue;
}
match ep.direction() {
Direction::In if in_address.is_none() => in_address = Some(ep.address()),
Direction::Out if out_address.is_none() => out_address = Some(ep.address()),
_ => {}
}
}
}
let in_address = in_address
.ok_or_else(|| HidError::backend("HID interface has no interrupt IN endpoint"))?;
let in_endpoint: Endpoint<Interrupt, In> = interface
.endpoint(in_address)
.map_err(|e| HidError::backend(format!("opening interrupt IN endpoint: {e}")))?;
let out_endpoint = match out_address {
Some(address) => Some(Mutex::new(
interface.endpoint::<Interrupt, Out>(address).map_err(|e| {
HidError::backend(format!("opening interrupt OUT endpoint: {e}"))
})?,
)),
None => None,
};
let max_packet_size = in_endpoint.max_packet_size();
if max_packet_size == 0 {
return Err(HidError::backend(
"interrupt IN endpoint declares a zero wMaxPacketSize",
));
}
let transfer_len = transfer_length(max_input_wire, max_packet_size);
let mut info = device_info(dev_info, interface_number);
if let Some((page, usage)) = parsed
.as_ref()
.and_then(|d| d.top_level_usages().first().copied())
{
info.usage_page = page;
info.usage = usage;
}
let shared = Arc::new(Shared::default());
let reader = {
let shared = Arc::clone(&shared);
std::thread::Builder::new()
.name("hidra-usb-read".into())
.spawn(move || reader_loop(in_endpoint, shared, transfer_len))
.map_err(|e| HidError::io("spawning USB reader thread", e))?
};
Ok(NusbDevice {
device,
interface,
interface_number,
info,
report_descriptor,
out_endpoint,
blocking: AtomicBool::new(true),
shared,
reader: Some(reader),
})
}
pub fn write(&self, data: &[u8]) -> HidResult<usize> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "write data must contain a report ID byte".into(),
});
}
let report_number = data[0];
let (payload, skipped) = if report_number == 0 {
(&data[1..], 1)
} else {
(data, 0)
};
match &self.out_endpoint {
Some(endpoint) => {
let mut endpoint = endpoint.lock().unwrap();
let completion =
endpoint.transfer_blocking(payload.to_vec().into(), CONTROL_TIMEOUT);
match completion.status {
Ok(()) => Ok(completion.actual_len + skipped),
Err(e) => Err(transfer_error("interrupt OUT write", e)),
}
}
None => {
self.interface
.control_out(
ControlOut {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: HID_SET_REPORT,
value: (REPORT_TYPE_OUTPUT << 8) | u16::from(report_number),
index: u16::from(self.interface_number),
data: payload,
},
CONTROL_TIMEOUT,
)
.wait()
.map_err(|e| transfer_error("SET_REPORT (output)", e))?;
Ok(data.len())
}
}
}
#[allow(dead_code)] pub fn read(&self, buf: &mut [u8]) -> HidResult<usize> {
let timeout = if self.blocking.load(Ordering::Relaxed) {
-1
} else {
0
};
self.read_timeout(buf, timeout)
}
#[allow(dead_code)] pub fn read_timeout(&self, buf: &mut [u8], timeout_ms: i32) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "read buffer must not be empty".into(),
});
}
let deadline =
(timeout_ms > 0).then(|| Instant::now() + Duration::from_millis(timeout_ms as u64));
let mut queue = self.shared.queue.lock().unwrap();
loop {
if let Some(report) = queue.reports.pop_front() {
let len = report.len().min(buf.len());
buf[..len].copy_from_slice(&report[..len]);
return Ok(len);
}
if self.shared.disconnected.load(Ordering::SeqCst) {
return Err(HidError::Disconnected);
}
if self.shared.shutdown.load(Ordering::SeqCst) {
return Err(HidError::backend("USB reader thread terminated"));
}
if timeout_ms == 0 {
return Ok(0);
}
queue = match deadline {
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
return Ok(0);
}
self.shared
.data_available
.wait_timeout(queue, deadline - now)
.unwrap()
.0
}
None => self.shared.data_available.wait(queue).unwrap(),
};
}
}
pub fn read_async<'a>(
&'a self,
buf: &'a mut [u8],
) -> impl Future<Output = HidResult<usize>> + Send + 'a {
ReadAsync {
shared: &self.shared,
buf,
}
}
#[allow(dead_code)] pub fn set_blocking_mode(&self, blocking: bool) -> HidResult<()> {
self.blocking.store(blocking, Ordering::Relaxed);
Ok(())
}
pub fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "feature report must contain a report ID byte".into(),
});
}
let report_number = data[0];
let payload = if report_number == 0 { &data[1..] } else { data };
self.interface
.control_out(
ControlOut {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: HID_SET_REPORT,
value: (REPORT_TYPE_FEATURE << 8) | u16::from(report_number),
index: u16::from(self.interface_number),
data: payload,
},
CONTROL_TIMEOUT,
)
.wait()
.map_err(|e| transfer_error("SET_REPORT (feature)", e))?;
Ok(())
}
fn get_report(
&self,
report_type: u16,
buf: &mut [u8],
operation: &'static str,
) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "buffer must contain a report ID byte".into(),
});
}
let report_number = buf[0];
let offset = usize::from(report_number == 0);
let length = (buf.len() - offset).min(usize::from(u16::MAX)) as u16;
let data = self
.interface
.control_in(
ControlIn {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: HID_GET_REPORT,
value: (report_type << 8) | u16::from(report_number),
index: u16::from(self.interface_number),
length,
},
CONTROL_TIMEOUT,
)
.wait()
.map_err(|e| transfer_error(operation, e))?;
let len = data.len().min(buf.len() - offset);
buf[offset..offset + len].copy_from_slice(&data[..len]);
Ok(len + offset)
}
pub fn get_feature_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(REPORT_TYPE_FEATURE, buf, "GET_REPORT (feature)")
}
pub fn get_input_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(REPORT_TYPE_INPUT, buf, "GET_REPORT (input)")
}
pub fn get_manufacturer_string(&self) -> HidResult<Option<String>> {
Ok(self.info.manufacturer_string.clone())
}
pub fn get_product_string(&self) -> HidResult<Option<String>> {
Ok(self.info.product_string.clone())
}
pub fn get_serial_number_string(&self) -> HidResult<Option<String>> {
Ok(self.info.serial_number.clone())
}
pub fn get_indexed_string(&self, index: u32) -> HidResult<Option<String>> {
let index = u8::try_from(index)
.ok()
.and_then(NonZeroU8::new)
.ok_or_else(|| HidError::InvalidData {
message: "string descriptor index must be in 1..=255".into(),
})?;
let s = self
.device
.get_string_descriptor(index, US_ENGLISH, CONTROL_TIMEOUT)
.wait()
.map_err(|e| match e {
nusb::GetDescriptorError::Transfer(TransferError::Disconnected) => {
HidError::Disconnected
}
e => HidError::backend(format!("reading string descriptor: {e}")),
})?;
Ok(Some(s))
}
pub fn get_report_descriptor(&self, buf: &mut [u8]) -> HidResult<usize> {
if self.report_descriptor.is_empty() {
return Err(HidError::backend(
"the HID report descriptor could not be read when the device was opened",
));
}
let len = self.report_descriptor.len().min(buf.len());
buf[..len].copy_from_slice(&self.report_descriptor[..len]);
Ok(len)
}
pub fn get_device_info(&self) -> HidResult<DeviceInfo> {
Ok(self.info.clone())
}
}
impl Drop for NusbDevice {
fn drop(&mut self) {
self.shared.shutdown.store(true, Ordering::SeqCst);
self.shared.wake_readers();
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
}
}
struct ReadAsync<'a> {
shared: &'a Shared,
buf: &'a mut [u8],
}
impl Future for ReadAsync<'_> {
type Output = HidResult<usize>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
this.shared.poll_read(this.buf, cx)
}
}
fn reader_loop(mut endpoint: Endpoint<Interrupt, In>, shared: Arc<Shared>, transfer_len: usize) {
let buf = endpoint.allocate(transfer_len);
endpoint.submit(buf);
while !shared.shutdown.load(Ordering::SeqCst) {
let Some(completion) = endpoint.wait_next_complete(READER_POLL_INTERVAL) else {
continue; };
match completion.status {
Ok(()) => {
let len = completion.actual_len.min(completion.buffer.len());
if len > 0 {
shared.push_report(completion.buffer[..len].to_vec());
}
endpoint.submit(completion.buffer);
}
Err(TransferError::Disconnected) => {
shared.disconnected.store(true, Ordering::SeqCst);
shared.wake_readers();
break;
}
Err(TransferError::Cancelled) => break,
Err(_) => endpoint.submit(completion.buffer),
}
}
endpoint.cancel_all();
while endpoint.pending() > 0 {
if endpoint
.wait_next_complete(Duration::from_secs(1))
.is_none()
{
break;
}
}
shared.shutdown.store(true, Ordering::SeqCst);
shared.wake_readers();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_round_trip() {
let path = format_path("3", 7, 1);
assert_eq!(path, "usb:3:7:1");
assert_eq!(parse_path(&path), Some(("3", 7, 1)));
assert_eq!(parse_path("usb:0:255:255"), Some(("0", 255, 255)));
}
#[test]
fn path_parse_allows_colons_in_bus_id() {
assert_eq!(parse_path("usb:PCI0@14:5:2"), Some(("PCI0@14", 5, 2)));
assert_eq!(parse_path("usb:a:b:5:2"), Some(("a:b", 5, 2)));
}
#[test]
fn path_parse_rejects_malformed_paths() {
assert_eq!(parse_path(""), None);
assert_eq!(parse_path("usb:"), None);
assert_eq!(parse_path("usb:1:2"), None);
assert_eq!(parse_path("usb::2:3"), None);
assert_eq!(parse_path("usb:1:x:3"), None);
assert_eq!(parse_path("usb:1:2:300"), None);
assert_eq!(parse_path("hid:1:2:3"), None);
assert_eq!(parse_path("/dev/hidraw0"), None);
}
#[test]
fn transfer_length_is_a_packet_multiple() {
assert_eq!(transfer_length(0, 64), 64);
assert_eq!(transfer_length(8, 64), 64);
assert_eq!(transfer_length(64, 64), 64);
assert_eq!(transfer_length(65, 64), 128);
assert_eq!(transfer_length(300, 64), 320);
assert_eq!(transfer_length(17, 8), 24);
}
#[test]
fn api_and_device_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NusbApi>();
assert_send_sync::<NusbDevice>();
}
fn block_on_read(shared: &Shared, buf: &mut [u8]) -> HidResult<usize> {
crate::test_util::block_on(std::future::poll_fn(|cx| shared.poll_read(buf, cx)))
}
#[test]
fn poll_read_pops_queued_reports_with_truncation() {
let shared = Shared::default();
shared.push_report(vec![1, 2, 3, 4]);
shared.push_report(vec![5]);
let mut buf = [0u8; 3];
assert_eq!(block_on_read(&shared, &mut buf).unwrap(), 3);
assert_eq!(buf, [1, 2, 3]);
assert_eq!(block_on_read(&shared, &mut buf).unwrap(), 1);
assert_eq!(buf[0], 5);
}
#[test]
fn poll_read_rejects_empty_buffer() {
let shared = Shared::default();
shared.push_report(vec![1]);
let err = block_on_read(&shared, &mut []).unwrap_err();
assert!(matches!(err, HidError::InvalidData { .. }));
assert_eq!(shared.queue.lock().unwrap().reports.len(), 1);
}
#[test]
fn poll_read_drains_queue_before_reporting_disconnect() {
let shared = Shared::default();
shared.push_report(vec![9]);
shared.disconnected.store(true, Ordering::SeqCst);
let mut buf = [0u8; 4];
assert_eq!(block_on_read(&shared, &mut buf).unwrap(), 1);
let err = block_on_read(&shared, &mut buf).unwrap_err();
assert!(matches!(err, HidError::Disconnected));
}
#[test]
fn poll_read_parks_until_a_report_is_pushed() {
let shared = Arc::new(Shared::default());
let pusher = {
let shared = Arc::clone(&shared);
std::thread::spawn(move || {
while shared.queue.lock().unwrap().wakers.is_empty() {
std::thread::yield_now();
}
shared.push_report(vec![7, 8]);
})
};
let mut buf = [0u8; 4];
assert_eq!(block_on_read(&shared, &mut buf).unwrap(), 2);
assert_eq!(buf[..2], [7, 8]);
pusher.join().unwrap();
}
#[test]
fn poll_read_parks_until_disconnect_is_flagged() {
let shared = Arc::new(Shared::default());
let disconnector = {
let shared = Arc::clone(&shared);
std::thread::spawn(move || {
while shared.queue.lock().unwrap().wakers.is_empty() {
std::thread::yield_now();
}
shared.disconnected.store(true, Ordering::SeqCst);
shared.wake_readers();
})
};
let mut buf = [0u8; 4];
let err = block_on_read(&shared, &mut buf).unwrap_err();
assert!(matches!(err, HidError::Disconnected));
disconnector.join().unwrap();
}
#[test]
fn poll_read_dedups_wakers_of_the_same_task() {
struct CountingWaker(std::sync::atomic::AtomicUsize);
impl std::task::Wake for CountingWaker {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let shared = Shared::default();
let counter = Arc::new(CountingWaker(std::sync::atomic::AtomicUsize::new(0)));
let waker = Waker::from(Arc::clone(&counter));
let mut cx = Context::from_waker(&waker);
let mut buf = [0u8; 4];
assert!(shared.poll_read(&mut buf, &mut cx).is_pending());
assert!(shared.poll_read(&mut buf, &mut cx).is_pending());
assert_eq!(shared.queue.lock().unwrap().wakers.len(), 1);
shared.push_report(vec![1]);
assert_eq!(counter.0.load(Ordering::SeqCst), 1);
assert!(shared.queue.lock().unwrap().wakers.is_empty());
}
#[test]
fn enumerate_does_not_panic() {
let api = NusbApi::new().unwrap();
let devices = api.enumerate(0, 0).unwrap();
for d in &devices {
assert!(d.path().starts_with("usb:"));
assert!(parse_path(d.path()).is_some());
assert_eq!(d.bus_type(), BusType::Usb);
}
}
}