fix(windows-host): self-heal the hostless-zombie pf-vdisplay device (adapter cycle + re-probe)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -646,8 +646,10 @@ pub fn open(compositor: Compositor) -> Result<Box<dyn VirtualDisplay>> {
|
|||||||
// The pf-vdisplay all-Rust IddCx driver is the sole virtual-display backend (the legacy SudoVDA
|
// 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.
|
// fallback was removed — its driver is no longer shipped). The compositor arg is moot on Windows.
|
||||||
let _ = compositor;
|
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!(
|
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 \
|
"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)"
|
not loaded (the host installer bundles it; reinstall or check the driver state)"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
/// 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
|
/// `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.
|
/// 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`
|
// 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
|
// 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.
|
// 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 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.
|
// 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
|
// 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() }
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user