From 6f8fb15c9b014ec763b4a33c1373d511e7c2b0aa Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 3 Jul 2026 17:12:43 +0000 Subject: [PATCH] fix(windows-host): self-heal the hostless-zombie pf-vdisplay device (adapter cycle + re-probe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fault-injection on-glass showed a killed/crashed WUDFHost leaves the devnode "started" but HOSTLESS: PnP Status OK, no WUDFHost process, zero device- interface instances — is_available() then fails every future session at the vdisplay::open gate (and a reopen inside VdisplayDriver::open finds nothing), until something cycles the device. Port reset-pf-vdisplay.ps1's adapter disable→enable step in-process (restart_vdisplay_device): the open gate now uses ensure_available() (cycle once + bounded re-probe; a genuinely uninstalled driver — no adapter devnode — still fails fast), and VdisplayDriver::open retries open_device over a short arrival window after a cycle, covering the manager's reopen path too. Co-Authored-By: Claude Fable 5 --- crates/punktfunk-host/src/vdisplay.rs | 4 +- .../src/vdisplay/windows/pf_vdisplay.rs | 96 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs index bbc5c0d..28f039b 100644 --- a/crates/punktfunk-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -646,8 +646,10 @@ pub fn open(compositor: Compositor) -> Result> { // The pf-vdisplay all-Rust IddCx driver is the sole virtual-display backend (the legacy SudoVDA // fallback was removed — its driver is no longer shipped). The compositor arg is moot on Windows. let _ = compositor; + // `ensure_available` self-heals the hostless-zombie state a WUDFHost crash leaves (adapter + // devnode present, interface gone): one device cycle + re-probe before giving up. anyhow::ensure!( - pf_vdisplay::is_available(), + pf_vdisplay::ensure_available(), "pf-vdisplay driver interface not found — the pf-vdisplay IddCx driver is not installed or \ not loaded (the host installer bundles it; reinstall or check the driver state)" ); diff --git a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs index 9b5a052..3ed1774 100644 --- a/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay/windows/pf_vdisplay.rs @@ -127,6 +127,56 @@ fn reap_ghost_monitors() -> u32 { } } +/// Kick the pf-vdisplay ADAPTER device (disable → enable) — the in-process equivalent of +/// `reset-pf-vdisplay.ps1` step 3. A crashed/killed WUDFHost can leave the devnode "started" yet +/// HOSTLESS (PnP Status OK, no WUDFHost process, zero device-interface instances) — a zombie no +/// session can open until the stack reloads; on-glass, only a device cycle recovered it. Called by +/// [`VdisplayDriver::open`] when `open_device` finds no openable interface; the caller retries the +/// open afterwards. Best-effort + bounded (~7 s inside the script). Returns whether a punktfunk +/// adapter devnode was found (and therefore cycled) — `false` means the driver genuinely is not +/// installed and a retry is pointless. +fn restart_vdisplay_device() -> bool { + // Mirrors reset-pf-vdisplay.ps1's Get-PfAdapter selector ('punktfunk Virtual Display' is the INF + // device description — locale-invariant). Same spawn shape as `reap_ghost_monitors` above. + const CYCLE_PS: &str = "$ErrorActionPreference='SilentlyContinue'; \ + $ad = Get-PnpDevice -Class Display | Where-Object { $_.FriendlyName -match 'punktfunk Virtual Display' } | Select-Object -First 1; \ + if ($ad) { \ + Disable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false; Start-Sleep -Seconds 3; \ + Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false; Start-Sleep -Seconds 3; \ + $st = (Get-PnpDevice -InstanceId $ad.InstanceId).Status; \ + if ($st -ne 'OK') { Enable-PnpDevice -InstanceId $ad.InstanceId -Confirm:$false; Start-Sleep -Seconds 2; \ + $st = (Get-PnpDevice -InstanceId $ad.InstanceId).Status }; \ + Write-Output $st \ + } else { Write-Output 'ABSENT' }"; + let ps = std::env::var("SystemRoot") + .map(|r| format!(r"{r}\System32\WindowsPowerShell\v1.0\powershell.exe")) + .unwrap_or_else(|_| "powershell.exe".to_string()); + match std::process::Command::new(&ps) + .args([ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + CYCLE_PS, + ]) + .output() + { + Ok(o) => { + let status = String::from_utf8_lossy(&o.stdout).trim().to_string(); + tracing::warn!( + %status, + "pf-vdisplay: cycled the adapter device (hostless-zombie recovery)" + ); + status != "ABSENT" + } + Err(e) => { + tracing::warn!(error = %e, "pf-vdisplay: adapter cycle could not spawn powershell"); + false + } + } +} + /// True if `e`'s chain carries the IddCx monitor-slot-exhaustion wedge HRESULT (0x80070490, /// `ERROR_NOT_FOUND`) — the `IOCTL_ADD` failure that ghost-PDO accumulation produces. The hex code is /// locale-invariant (the OS message text is not), so we match on it. @@ -294,7 +344,30 @@ impl VdisplayDriver for PfVdisplayDriver { // SAFETY: `open_device` is `unsafe` only because it issues SetupAPI enumeration + `CreateFileW` // FFI; it takes no arguments and returns an owned raw `HANDLE` (or `Err`). Called here on the // backend-init thread, with no precondition beyond a valid thread context. - let device = unsafe { open_device()? }; + let device = match unsafe { open_device() } { + Ok(d) => d, + Err(first) => { + // No openable interface. If a WUDFHost crash left the devnode a hostless zombie + // (validated on-glass: PnP Status OK, zero interface instances), a device cycle + // reloads the stack — kick it once and retry the open over a short arrival window. + if !restart_vdisplay_device() { + return Err(first); // no adapter devnode at all — genuinely not installed + } + let mut reopened = Err(first); + for _ in 0..8 { + std::thread::sleep(std::time::Duration::from_millis(500)); + // SAFETY: as above — plain SetupAPI + CreateFileW FFI, no preconditions. + match unsafe { open_device() } { + Ok(d) => { + reopened = Ok(d); + break; + } + Err(e) => reopened = Err(e), + } + } + reopened.context("pf-vdisplay interface still absent after an adapter cycle")? + } + }; // Wrap IMMEDIATELY: every `?` below must close the device exactly once — the old // wrap-on-success-only shape leaked the raw handle whenever GET_INFO itself failed. // SAFETY: `device` is the valid handle `open_device` just returned; ownership transfers into @@ -566,6 +639,27 @@ pub fn is_available() -> bool { unsafe { open_device().map(|h| CloseHandle(h)).is_ok() } } +/// [`is_available`], with self-heal: an interface-less driver whose adapter devnode EXISTS is the +/// hostless-zombie state a WUDFHost crash leaves behind (validated on-glass — PnP reports Status OK +/// with no WUDFHost process and zero interface instances, and every session fails at this gate until +/// the device reloads). Cycle the adapter once and re-probe over a short arrival window. A genuinely +/// uninstalled driver (no adapter devnode) fails fast without the wait. +pub fn ensure_available() -> bool { + if is_available() { + return true; + } + if !restart_vdisplay_device() { + return false; + } + for _ in 0..8 { + std::thread::sleep(std::time::Duration::from_millis(500)); + if is_available() { + return true; + } + } + false +} + #[cfg(test)] mod tests { use super::*;