usbhubd: bound EP1 wait via worker thread + recv_timeout poll fallback
Root cause of hub-child enumeration stalling: after a synchronous action (power-on, reset) the port's state changes WITHOUT a new interrupt ever being generated, and the loop's next step was a blocking EP1 transfer_read with no timeout — the daemon never woke to observe the state it had just caused, so devices behind a hub sat enabled-but-never-attached forever. Move the blocking transfer_read onto a worker thread that forwards each interrupt bitmap as a u64 mask over an mpsc channel. The main loop waits with recv_timeout(POLL_FALLBACK_MS): a real bitmap processes only the changed ports (interrupt fast path), while timeout/disconnect/error falls back to an all-ports poll (progress guarantee). Matches the Linux model where hub_irq is the accelerator but port status reads are always authoritative.
This commit is contained in:
@@ -396,7 +396,6 @@ fn main() {
|
||||
// off-by-one that processed every event on the wrong port.
|
||||
const POLL_FALLBACK_MS: u64 = 200;
|
||||
let bitmap_size = (ports as usize + 8) / 8;
|
||||
let mut bitmap = vec![0u8; bitmap_size];
|
||||
|
||||
// Mask with bits 1..=ports set (every real port; bit 0 = hub, skipped).
|
||||
let all_ports_mask: u64 = ((1u64 << ports) - 1) << 1;
|
||||
@@ -407,27 +406,54 @@ fn main() {
|
||||
// first EP1 read would otherwise hang the daemon before any scan).
|
||||
let mut first_scan = true;
|
||||
|
||||
// EP1 status-change events are produced on a worker thread so the
|
||||
// main loop can bound its wait: the blocking transfer_read has no
|
||||
// timeout, and after a synchronous action (power-on, reset) the port
|
||||
// state changes WITHOUT a new interrupt — a pure blocking read would
|
||||
// never wake to observe it. The worker forwards each interrupt bitmap
|
||||
// as a u64 mask; the main loop falls back to an all-ports poll on
|
||||
// timeout, so interrupt delivery is a fast path, never a requirement.
|
||||
let (event_tx, event_rx) = std::sync::mpsc::channel::<u64>();
|
||||
let intr_ep_available = intr_ep_handle.is_some();
|
||||
if let Some(mut ep) = intr_ep_handle.take() {
|
||||
thread::spawn(move || {
|
||||
let mut buf = vec![0u8; bitmap_size];
|
||||
loop {
|
||||
match ep.transfer_read(&mut buf) {
|
||||
Ok(_) => {
|
||||
let mut mask = 0u64;
|
||||
for (byte_idx, &byte) in buf.iter().enumerate() {
|
||||
if byte != 0 {
|
||||
mask |= (byte as u64) << (byte_idx * 8);
|
||||
}
|
||||
}
|
||||
buf.iter_mut().for_each(|b| *b = 0);
|
||||
if event_tx.send(mask).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if event_tx.send(u64::MAX).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loop {
|
||||
// Build a port-change mask.
|
||||
// Bit N set = port N needs processing (bit 0 = hub status change).
|
||||
let changed: u64 = if first_scan {
|
||||
first_scan = false;
|
||||
all_ports_mask
|
||||
} else if let Some(ref mut ep) = intr_ep_handle {
|
||||
match ep.transfer_read(&mut bitmap) {
|
||||
Ok(_) => {
|
||||
let mut mask = 0u64;
|
||||
for (byte_idx, &byte) in bitmap.iter().enumerate() {
|
||||
if byte != 0 {
|
||||
mask |= (byte as u64) << (byte_idx * 8);
|
||||
}
|
||||
}
|
||||
mask
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: interrupt transfer failed ({}), falling back to poll", e);
|
||||
all_ports_mask
|
||||
}
|
||||
} else if intr_ep_available {
|
||||
match event_rx.recv_timeout(time::Duration::from_millis(POLL_FALLBACK_MS)) {
|
||||
// A real interrupt bitmap: process only the changed ports.
|
||||
Ok(mask) if mask != u64::MAX => mask,
|
||||
// Error sentinel, timeout, or dead worker: poll everything.
|
||||
_ => all_ports_mask,
|
||||
}
|
||||
} else {
|
||||
// Polling mode: process all ports.
|
||||
|
||||
Reference in New Issue
Block a user