fix: improve reliability with typed error handling and poison recovery
- Replace string-based USB error classification with ErrorKind matching: nusb TransferError is now preserved through io::Error instead of being destroyed by format!(). Stall→ConnectionReset→EPIPE, Cancelled→ Interrupted→ENOENT, Disconnected→ConnectionAborted→ESHUTDOWN. - Replace fragile string matching in interrupt IN retry loop with direct TransferError::Cancelled pattern match. - Replace 21 production Mutex::lock().unwrap() calls with .unwrap_or_else(|e| e.into_inner()) to recover from mutex poisoning instead of cascading panics across the server. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
804a4910a0
commit
5120b1a3b9
6 changed files with 52 additions and 78 deletions
|
|
@ -65,12 +65,12 @@
|
|||
### 11. Error status mapping via string matching
|
||||
- **File:** `lib/src/lib.rs:584-592`
|
||||
- **Issue:** Fragile string-based error classification depends on nusb/rusb message text.
|
||||
- **Status:** [ ] TODO
|
||||
- **Status:** [x] DONE — Preserve nusb `TransferError` through `io::Error`; classify via `ErrorKind` instead of string matching.
|
||||
|
||||
### 12. Poisoned mutex panics
|
||||
- **Files:** `host.rs`, `device.rs` (multiple locations)
|
||||
- **Issue:** All `Mutex::lock().unwrap()` calls panic if the mutex is poisoned.
|
||||
- **Status:** [ ] TODO
|
||||
- **Status:** [x] DONE — Replaced 21 production `.lock().unwrap()` with `.lock().unwrap_or_else(|e| e.into_inner())`.
|
||||
|
||||
### 13. `debug_assert` for path/bus_id length
|
||||
- **File:** `lib/src/device.rs:252-260`
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl UsbInterfaceHandler for UsbCdcAcmHandler {
|
|||
} else {
|
||||
// bulk in
|
||||
let max_packet_size = ep.max_packet_size as usize;
|
||||
let mut tx_buffer = self.tx_buffer.lock().unwrap();
|
||||
let mut tx_buffer = self.tx_buffer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let resp = if tx_buffer.len() > max_packet_size {
|
||||
tx_buffer.drain(..max_packet_size).collect::<Vec<_>>()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ impl UsbDevice {
|
|||
debug!("Get BOS descriptor");
|
||||
if self.device_handler.is_some() {
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
return handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -448,7 +448,7 @@ impl UsbDevice {
|
|||
// IADs, class-specific descriptors, etc. are preserved.
|
||||
if self.device_handler.is_some() {
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
return handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -554,7 +554,7 @@ impl UsbDevice {
|
|||
// Forward unknown string indices to the device handler
|
||||
// (host passthrough: the real device knows its own strings)
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -573,7 +573,7 @@ impl UsbDevice {
|
|||
debug!("Get device qualifier descriptor");
|
||||
if self.device_handler.is_some() {
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
return handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -671,7 +671,7 @@ impl UsbDevice {
|
|||
// to device
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -700,7 +700,7 @@ impl UsbDevice {
|
|||
// Forward to physical device handler if present,
|
||||
// so endpoints are properly reset on the device
|
||||
if let Some(ref handler_lock) = self.device_handler {
|
||||
let mut handler = handler_lock.lock().unwrap();
|
||||
let mut handler = handler_lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
@ -782,7 +782,7 @@ impl UsbDevice {
|
|||
// to device
|
||||
// see https://www.beyondlogic.org/usbnutshell/usb6.shtml
|
||||
let lock = self.device_handler.as_ref().unwrap();
|
||||
let mut handler = lock.lock().unwrap();
|
||||
let mut handler = lock.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handler.handle_urb(UrbRequest {
|
||||
ep,
|
||||
transfer_buffer_length,
|
||||
|
|
|
|||
|
|
@ -133,10 +133,10 @@ impl UsbInterfaceHandler for UsbHidKeyboardHandler {
|
|||
// interrupt transfer
|
||||
if let Direction::In = ep.direction() {
|
||||
// interrupt in
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match *state {
|
||||
UsbHidKeyboardHandlerState::Idle => {
|
||||
if let Some(report) = self.pending_key_events.lock().unwrap().pop_front() {
|
||||
if let Some(report) = self.pending_key_events.lock().unwrap_or_else(|e| e.into_inner()).pop_front() {
|
||||
let mut resp = vec![report.modifier, 0];
|
||||
resp.extend_from_slice(&report.keys);
|
||||
info!("HID key down");
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ impl UsbInterfaceHandler for RusbUsbHostInterfaceHandler {
|
|||
debug!("To host device: ep={ep:?} setup={setup:?} req={req:?}",);
|
||||
let mut buffer = vec![0u8; transfer_buffer_length as usize];
|
||||
let timeout = std::time::Duration::new(1, 0);
|
||||
let handle = self.handle.lock().unwrap();
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if ep.attributes == EndpointAttributes::Control as u8 {
|
||||
// control
|
||||
if let Direction::In = ep.direction() {
|
||||
|
|
@ -151,7 +151,7 @@ impl UsbDeviceHandler for RusbUsbHostDeviceHandler {
|
|||
debug!("To host device: setup={setup:?} req={req:?}");
|
||||
let mut buffer = vec![0u8; transfer_buffer_length as usize];
|
||||
let timeout = std::time::Duration::new(1, 0);
|
||||
let handle = self.handle.lock().unwrap();
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// control
|
||||
if setup.request_type & 0x80 == 0 {
|
||||
// control out
|
||||
|
|
@ -213,10 +213,8 @@ impl NusbUsbHostInterfaceHandler {
|
|||
|
||||
impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
||||
fn set_alt_setting(&self, alt: u8) -> Result<()> {
|
||||
let handle = self.handle.lock().unwrap();
|
||||
handle.set_alt_setting(alt).wait().map_err(|e| {
|
||||
std::io::Error::other(format!("Failed to set alt setting {alt}: {e}"))
|
||||
})
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
handle.set_alt_setting(alt).wait().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn handle_urb(
|
||||
|
|
@ -237,7 +235,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
0x01 => std::time::Duration::from_secs(5), // isochronous
|
||||
_ => std::time::Duration::from_secs(1), // control, bulk
|
||||
};
|
||||
let handle = self.handle.lock().unwrap();
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if ep.attributes == EndpointAttributes::Control as u8 {
|
||||
// control
|
||||
if let Direction::In = ep.direction() {
|
||||
|
|
@ -271,9 +269,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
index: setup.index,
|
||||
length: transfer_buffer_length.min(u16::MAX as u32) as u16,
|
||||
};
|
||||
let data = handle.control_in(control_in, timeout).wait().map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("USB control IN failed: {e}"))
|
||||
})?;
|
||||
let data = handle.control_in(control_in, timeout).wait().map_err(std::io::Error::from)?;
|
||||
return Ok(UrbResponse { data, ..Default::default() });
|
||||
} else {
|
||||
// control out
|
||||
|
|
@ -309,12 +305,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
handle
|
||||
.control_out(control_out, timeout)
|
||||
.wait()
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("USB control OUT failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Interrupt as u8 {
|
||||
// interrupt
|
||||
|
|
@ -340,22 +331,18 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
Ok(()) => {
|
||||
return Ok(UrbResponse { data: completion.buffer.to_vec(), ..Default::default() });
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("timed out") || msg.contains("cancel") {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
format!("USB interrupt IN timed out after {timeout:?}"),
|
||||
));
|
||||
}
|
||||
// Retry silently
|
||||
continue;
|
||||
Err(nusb::transfer::TransferError::Cancelled) => {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"USB interrupt IN timed out",
|
||||
));
|
||||
}
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("USB interrupt IN failed: {e}"),
|
||||
));
|
||||
// Retry silently
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::from(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,9 +357,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
buffer.extend_from_slice(req);
|
||||
drop(handle);
|
||||
let completion = endpoint.transfer_blocking(buffer, timeout);
|
||||
completion.status.map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("USB interrupt OUT failed: {e}"))
|
||||
})?;
|
||||
completion.status.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Bulk as u8 {
|
||||
// bulk
|
||||
|
|
@ -387,9 +372,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
})?;
|
||||
let buffer = endpoint.allocate(alloc_len);
|
||||
let completion = endpoint.transfer_blocking(buffer, timeout);
|
||||
completion.status.map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("USB bulk IN failed: {e}"))
|
||||
})?;
|
||||
completion.status.map_err(std::io::Error::from)?;
|
||||
// Return only the actual bytes received (may be less than alloc_len)
|
||||
return Ok(UrbResponse { data: completion.buffer.to_vec(), ..Default::default() });
|
||||
} else {
|
||||
|
|
@ -402,9 +385,7 @@ impl UsbInterfaceHandler for NusbUsbHostInterfaceHandler {
|
|||
let mut buffer = endpoint.allocate(req.len());
|
||||
buffer.extend_from_slice(req);
|
||||
let completion = endpoint.transfer_blocking(buffer, timeout);
|
||||
completion.status.map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("USB bulk OUT failed: {e}"))
|
||||
})?;
|
||||
completion.status.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else if ep.attributes == EndpointAttributes::Isochronous as u8 {
|
||||
// Isochronous transfer
|
||||
|
|
@ -545,7 +526,7 @@ impl UsbDeviceHandler for NusbUsbHostDeviceHandler {
|
|||
let req = &request.data;
|
||||
debug!("To host device: setup={setup:?} req={req:?}");
|
||||
let timeout = std::time::Duration::new(1, 0);
|
||||
let handle = self.handle.lock().unwrap();
|
||||
let handle = self.handle.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// control
|
||||
if cfg!(not(target_os = "windows")) {
|
||||
if setup.request_type & 0x80 == 0 {
|
||||
|
|
@ -584,12 +565,7 @@ impl UsbDeviceHandler for NusbUsbHostDeviceHandler {
|
|||
handle
|
||||
.control_out(control_out, timeout)
|
||||
.wait()
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("USB control OUT failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
.map_err(std::io::Error::from)?;
|
||||
}
|
||||
} else {
|
||||
// control in
|
||||
|
|
@ -624,9 +600,7 @@ impl UsbDeviceHandler for NusbUsbHostDeviceHandler {
|
|||
index: setup.index,
|
||||
length: transfer_buffer_length.min(u16::MAX as u32) as u16,
|
||||
};
|
||||
let data = handle.control_in(control_in, timeout).wait().map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, format!("USB control IN failed: {e}"))
|
||||
})?;
|
||||
let data = handle.control_in(control_in, timeout).wait().map_err(std::io::Error::from)?;
|
||||
return Ok(UrbResponse { data, ..Default::default() });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
|
|||
Err(_) => {
|
||||
// Don't time out if there are in-flight URBs (e.g. pending
|
||||
// interrupt transfers) — the kernel is still alive.
|
||||
if !in_flight.lock().unwrap().is_empty() {
|
||||
if !in_flight.lock().unwrap_or_else(|e| e.into_inner()).is_empty() {
|
||||
continue;
|
||||
}
|
||||
warn!("URB read timed out after {URB_READ_TIMEOUT:?}");
|
||||
|
|
@ -556,7 +556,7 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
|
|||
|
||||
// Reject if too many URBs are already in-flight to prevent
|
||||
// resource exhaustion from a malicious client.
|
||||
if in_flight.lock().unwrap().len() >= MAX_IN_FLIGHT_URBS {
|
||||
if in_flight.lock().unwrap_or_else(|e| e.into_inner()).len() >= MAX_IN_FLIGHT_URBS {
|
||||
warn!(
|
||||
"Too many in-flight URBs ({MAX_IN_FLIGHT_URBS}), rejecting seqnum={seqnum}"
|
||||
);
|
||||
|
|
@ -588,14 +588,14 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
|
|||
let tx = response_tx.clone();
|
||||
let device = device.clone();
|
||||
let cancel = CancellationToken::new();
|
||||
in_flight.lock().unwrap().insert(seqnum, cancel.clone());
|
||||
in_flight.lock().unwrap_or_else(|e| e.into_inner()).insert(seqnum, cancel.clone());
|
||||
let in_flight_ref = in_flight.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
|
||||
if cancel.is_cancelled() {
|
||||
in_flight_ref.lock().unwrap().remove(&seqnum);
|
||||
in_flight_ref.lock().unwrap_or_else(|e| e.into_inner()).remove(&seqnum);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -661,16 +661,16 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
|
|||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.to_string();
|
||||
// Map USB errors to appropriate Linux URB status codes
|
||||
let status = if msg.contains("stall") {
|
||||
(-32i32) as u32 // EPIPE: endpoint stalled
|
||||
} else if msg.contains("cancel") {
|
||||
(-2i32) as u32 // ENOENT: URB was unlinked/cancelled
|
||||
} else if msg.contains("timed out") || msg.contains("TimedOut") {
|
||||
(-110i32) as u32 // ETIMEDOUT
|
||||
} else {
|
||||
(-71i32) as u32 // EPROTO: protocol error
|
||||
// Map io::ErrorKind to appropriate Linux URB status codes.
|
||||
// nusb's TransferError → io::Error preserves the kind:
|
||||
// Stall → ConnectionReset, Cancelled → Interrupted,
|
||||
// Disconnected → ConnectionAborted
|
||||
let status = match err.kind() {
|
||||
std::io::ErrorKind::ConnectionReset => (-32i32) as u32, // EPIPE: stall
|
||||
std::io::ErrorKind::Interrupted => (-2i32) as u32, // ENOENT: cancelled
|
||||
std::io::ErrorKind::TimedOut => (-110i32) as u32, // ETIMEDOUT
|
||||
std::io::ErrorKind::ConnectionAborted => (-108i32) as u32, // ESHUTDOWN: disconnected
|
||||
_ => (-71i32) as u32, // EPROTO: protocol error
|
||||
};
|
||||
warn!("Error handling URB seqnum={seqnum}: {err} (status={status})", status = status as i32);
|
||||
UsbIpResponse::UsbIpRetSubmit {
|
||||
|
|
@ -689,11 +689,11 @@ pub async fn handle_urb_loop<T: AsyncReadExt + AsyncWriteExt + Unpin + Send + 's
|
|||
};
|
||||
|
||||
if cancel.is_cancelled() {
|
||||
in_flight_ref.lock().unwrap().remove(&seqnum);
|
||||
in_flight_ref.lock().unwrap_or_else(|e| e.into_inner()).remove(&seqnum);
|
||||
return;
|
||||
}
|
||||
|
||||
in_flight_ref.lock().unwrap().remove(&seqnum);
|
||||
in_flight_ref.lock().unwrap_or_else(|e| e.into_inner()).remove(&seqnum);
|
||||
|
||||
match res.to_bytes() {
|
||||
Ok(bytes) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue