feat(windows): pf-vdisplay CLEAR_ALL — reap orphaned virtual monitors on startup

The "5-6 stale monitors that never tear down" failure (also seen with SudoVDA):
an orphan from a crashed/killed previous host lingers because the driver watchdog
is kept reset by a still-pinging new session, so it never fires for the orphan.

- Driver (pf-vdisplay control.rs): new IOCTL_CLEAR_ALL (0x804) -> tear down every
  monitor. A pf-vdisplay extension; SudoVDA returns invalid for it (ignored), so
  the host can issue it unconditionally.
- Host (vdisplay/sudovda.rs): send IOCTL_CLEAR_ALL once on startup (best-effort)
  to reap orphans before creating ours; and surface a failing keepalive PING (the
  old `let _ =` swallowed it, masking a lost control handle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:07:14 +02:00
parent d39da4bc06
commit e27abc065e
2 changed files with 33 additions and 2 deletions
+25 -2
View File
@@ -53,6 +53,9 @@ const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801); const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802); // == 0x0022_2008 const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802); // == 0x0022_2008
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803); const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
/// pf-vdisplay extension (NOT in SudoVDA): tear down every virtual monitor. Sent once on host startup
/// to reap monitors orphaned by a crashed/killed previous host. SudoVDA returns invalid (ignored).
const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_DRIVER_PING: u32 = ctl(0x888); const IOCTL_DRIVER_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF); const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
@@ -722,10 +725,20 @@ unsafe fn create_monitor(device: isize, mode: Mode, watchdog_s: u32) -> Result<M
let stop_t = stop.clone(); let stop_t = stop.clone();
let pinger = thread::spawn(move || { let pinger = thread::spawn(move || {
let h = HANDLE(device_raw as *mut c_void); let h = HANDLE(device_raw as *mut c_void);
let mut warned = false;
while !stop_t.load(Ordering::Relaxed) { while !stop_t.load(Ordering::Relaxed) {
let mut none: [u8; 0] = []; let mut none: [u8; 0] = [];
unsafe { match unsafe { ioctl(h, IOCTL_DRIVER_PING, &[], &mut none) } {
let _ = ioctl(h, IOCTL_DRIVER_PING, &[], &mut none); Ok(_) => warned = false,
// A persistently failing PING means the cached control handle went invalid — the
// driver watchdog will then tear the monitor down mid-session. Surface it once
// (the old `let _ =` swallowed it, which masked exactly this during the bad-state churn).
Err(e) => {
if !warned {
tracing::warn!("SudoVDA keepalive PING failed (control handle lost?): {e:#}");
warned = true;
}
}
} }
thread::sleep(interval); thread::sleep(interval);
} }
@@ -848,6 +861,16 @@ fn mgr_ensure_device(g: &mut Mgr) -> Result<isize> {
3 3
}; };
tracing::info!("SudoVDA watchdog timeout {}s", g.watchdog_s); tracing::info!("SudoVDA watchdog timeout {}s", g.watchdog_s);
// Reap monitors orphaned by a crashed/killed previous host instance before we create ours.
// pf-vdisplay honors IOCTL_CLEAR_ALL; SudoVDA returns invalid (ignored). Without it an orphan
// lingers until the driver watchdog fires — but a still-pinging new session keeps resetting that
// watchdog, so orphans could accumulate (the "5-6 stale monitors that never tear down" failure).
{
let mut none: [u8; 0] = [];
if unsafe { ioctl(device, IOCTL_CLEAR_ALL, &[], &mut none) }.is_ok() {
tracing::info!("cleared orphaned virtual monitors on host startup");
}
}
let raw = device.0 as isize; let raw = device.0 as isize;
g.device = Some(raw); g.device = Some(raw);
Ok(raw) Ok(raw)
@@ -30,6 +30,10 @@ const IOCTL_ADD: u32 = ctl(0x800);
const IOCTL_REMOVE: u32 = ctl(0x801); const IOCTL_REMOVE: u32 = ctl(0x801);
const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802); const IOCTL_SET_RENDER_ADAPTER: u32 = ctl(0x802);
const IOCTL_GET_WATCHDOG: u32 = ctl(0x803); const IOCTL_GET_WATCHDOG: u32 = ctl(0x803);
/// pf-vdisplay extension (NOT in SudoVDA): tear down every monitor. The host issues this on startup to
/// reap monitors orphaned by a crashed/killed previous host instance. SudoVDA returns invalid for it
/// (harmlessly ignored), so the host can send it unconditionally.
const IOCTL_CLEAR_ALL: u32 = ctl(0x804);
const IOCTL_PING: u32 = ctl(0x888); const IOCTL_PING: u32 = ctl(0x888);
const IOCTL_GET_VERSION: u32 = ctl(0x8FF); const IOCTL_GET_VERSION: u32 = ctl(0x8FF);
@@ -112,6 +116,10 @@ pub extern "C-unwind" fn device_io_control(
IOCTL_SET_RENDER_ADAPTER => do_set_render_adapter(request, input_len), IOCTL_SET_RENDER_ADAPTER => do_set_render_adapter(request, input_len),
IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes), IOCTL_GET_WATCHDOG => do_get_watchdog(request, output_len, &mut bytes),
IOCTL_PING => NTSTATUS::STATUS_SUCCESS, IOCTL_PING => NTSTATUS::STATUS_SUCCESS,
IOCTL_CLEAR_ALL => {
disconnect_all_monitors();
NTSTATUS::STATUS_SUCCESS
}
IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes), IOCTL_GET_VERSION => do_get_version(request, output_len, &mut bytes),
_ => NTSTATUS::STATUS_INVALID_DEVICE_REQUEST, _ => NTSTATUS::STATUS_INVALID_DEVICE_REQUEST,
} }