From ecb3d1ab22ebfd4ab3a4b6eea5431832769ae5ea Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 22:21:07 +0200 Subject: [PATCH] fix(host/windows): the pad service thread no longer waits on pnputil at its leisure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit driver_store_inventory() ran `pnputil /enum-drivers` synchronously inside the attach-failure diagnose path — on the pad service thread, with no timeout. A busy or wedged driver store blocks that enumeration for tens of seconds, and the thread must keep draining pad slots. The query now runs on its own thread (spawned once per process); the caller waits a bounded 2 s so a fast pnputil still lands in the same report, and a slower one fills the cache for later diagnoses — the WARN then says the store could not be queried yet instead of blocking. Co-Authored-By: Claude Fable 5 --- .../src/inject/windows/gamepad_raii.rs | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/crates/pf-inject/src/inject/windows/gamepad_raii.rs b/crates/pf-inject/src/inject/windows/gamepad_raii.rs index 06ac07ad..b4679169 100644 --- a/crates/pf-inject/src/inject/windows/gamepad_raii.rs +++ b/crates/pf-inject/src/inject/windows/gamepad_raii.rs @@ -825,7 +825,7 @@ impl DriverAttach { Some(false) => { "driver package NOT in the driver store — run: punktfunk-host.exe driver install --gamepad" } - None => "driver store could not be queried (pnputil failed)", + None => "driver store could not be queried (pnputil failed or still enumerating)", }; let devnode = match &self.instance_id { Some(id) => devnode_status_line(id), @@ -850,28 +850,52 @@ impl DriverAttach { } } +/// How long [`driver_store_inventory`] lets the caller wait for the background pnputil query +/// before reporting without it — [`observe`] runs on the pad service thread, which must keep +/// draining pad slots even when the driver store is wedged. +const INVENTORY_WAIT: Duration = Duration::from_secs(2); + /// Driver-store inventory (`pnputil /enum-drivers`), lower-cased, fetched once per process — only -/// consulted on the failure path, so the one-off subprocess cost never hits a healthy session. -fn driver_store_inventory() -> &'static str { +/// consulted on the failure path, so the subprocess cost never hits a healthy session. The query +/// runs on its OWN thread: pnputil can block for tens of seconds on a busy/wedged driver store, +/// and the caller is the pad service thread. `None` = not available yet (query still running) or +/// failed; a query that outlives [`INVENTORY_WAIT`] still lands in the cache for later reports. +fn driver_store_inventory() -> Option<&'static str> { static INV: OnceLock = OnceLock::new(); - INV.get_or_init(|| { - // Resolve pnputil by full System32 path — the host runs as SYSTEM and must not trust PATH / - // the CreateProcess search (which checks the launching EXE's own dir first), or a planted - // `pnputil.exe` beside the host binary would run elevated (security-review 2026-07-17). - let pnputil = std::env::var("SystemRoot") - .map(|r| format!(r"{r}\System32\pnputil.exe")) - .unwrap_or_else(|_| "pnputil.exe".to_string()); - std::process::Command::new(&pnputil) - .arg("/enum-drivers") - .output() - .map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase()) - .unwrap_or_default() - }) + static SPAWN: std::sync::Once = std::sync::Once::new(); + SPAWN.call_once(|| { + std::thread::spawn(|| { + // Resolve pnputil by full System32 path — the host runs as SYSTEM and must not trust + // PATH / the CreateProcess search (which checks the launching EXE's own dir first), or + // a planted `pnputil.exe` beside the host binary would run elevated (security-review + // 2026-07-17). + let pnputil = std::env::var("SystemRoot") + .map(|r| format!(r"{r}\System32\pnputil.exe")) + .unwrap_or_else(|_| "pnputil.exe".to_string()); + let inv = std::process::Command::new(&pnputil) + .arg("/enum-drivers") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).to_ascii_lowercase()) + .unwrap_or_default(); + let _ = INV.set(inv); + }); + }); + let deadline = Instant::now() + INVENTORY_WAIT; + loop { + if let Some(inv) = INV.get() { + return Some(inv); + } + if Instant::now() >= deadline { + return None; + } + std::thread::sleep(Duration::from_millis(50)); + } } -/// Whether the driver store holds `inf` (e.g. `pf_xusb.inf`). `None` = pnputil unavailable/failed. +/// Whether the driver store holds `inf` (e.g. `pf_xusb.inf`). `None` = pnputil unavailable, +/// failed, or still enumerating. fn driver_store_has(inf: &str) -> Option { - let inv = driver_store_inventory(); + let inv = driver_store_inventory()?; if inv.is_empty() { return None; }