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:
Red Bear OS
2026-07-19 10:11:38 +09:00
parent 9317d4874b
commit 8cd2f71ddd
+42 -16
View File
@@ -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.