From fc7d24650d05819feffd18484f2fc71217ed3271 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:15:50 -0700 Subject: [PATCH] fix(update): an exited Windows owner with exit code 259 is dead, not running forever (review F10) What: the Windows pid_is_alive opens the process with SYNCHRONIZE and returns WaitForSingleObject(h, 0) == WAIT_TIMEOUT; access denied still reads alive via liveness_from_open_error. Why: GetExitCodeProcess == STILL_ACTIVE (259) is also what a process that exited with code 259 reports while any handle keeps its object, and its creation time still matches the v2 marker, so Busy had no age bound. Python's _early_recovery already uses the wait probe. Red: Windows-only; marker_tests::pid_is_alive_false_for_an_exited_process_whose_exit_code_is_259 (cfg(windows)) needs a native runner. Green: cargo check --tests --target x86_64-pc-windows-msvc clean on the real marker.rs (std-only harness); Linux marker tests 49 passed. --- .../src-tauri/src/marker.rs | 15 ++++++++------ .../src-tauri/src/marker_tests.rs | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/bootstrap-installer/src-tauri/src/marker.rs b/apps/bootstrap-installer/src-tauri/src/marker.rs index 10a43af694..a42fcc68ed 100644 --- a/apps/bootstrap-installer/src-tauri/src/marker.rs +++ b/apps/bootstrap-installer/src-tauri/src/marker.rs @@ -787,9 +787,9 @@ fn liveness_from_open_error(err: u32) -> bool { /// True when a process with `pid` currently exists. #[cfg(windows)] fn pid_is_alive(pid: u32) -> bool { - use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, STILL_ACTIVE}; + use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, WAIT_TIMEOUT}; use windows_sys::Win32::System::Threading::{ - GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, + OpenProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE, }; // pid 0 is the System Idle Process, never an updater (Python: `pid <= 0` is dead). @@ -797,14 +797,17 @@ fn pid_is_alive(pid: u32) -> bool { return false; } unsafe { - let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); + // A process object is signalled once it exits. Never `GetExitCodeProcess == + // STILL_ACTIVE`: a process that exited with code 259 reads "running" for as long as + // any handle keeps its object, and its creation time still matches the v2 marker. + // Same probe as `hermes_cli/_early_recovery._pid_is_running`. + let handle = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); if handle.is_null() { return liveness_from_open_error(GetLastError()); } - let mut code: u32 = 0; - let ok = GetExitCodeProcess(handle, &mut code); + let alive = WaitForSingleObject(handle, 0) == WAIT_TIMEOUT; CloseHandle(handle); - ok != 0 && code == STILL_ACTIVE as u32 + alive } } diff --git a/apps/bootstrap-installer/src-tauri/src/marker_tests.rs b/apps/bootstrap-installer/src-tauri/src/marker_tests.rs index e5b7691ff0..1206ef67a0 100644 --- a/apps/bootstrap-installer/src-tauri/src/marker_tests.rs +++ b/apps/bootstrap-installer/src-tauri/src/marker_tests.rs @@ -466,6 +466,26 @@ fn pid_is_alive_never_interprets_unsigned_pids_as_groups() { assert!(!pid_is_alive(u32::MAX - std::process::id() + 1)); } +#[cfg(windows)] +#[test] +fn pid_is_alive_false_for_an_exited_process_whose_exit_code_is_259() { + // 259 is STILL_ACTIVE: an exit-code probe reads this exited child as running for as long + // as our `Child` keeps its process object open. + let mut exited = std::process::Command::new("cmd") + .args(["/C", "exit 259"]) + .spawn() + .unwrap(); + assert_eq!(exited.wait().unwrap().code(), Some(259)); + assert!(!pid_is_alive(exited.id()), "an exited process is dead"); + let mut running = std::process::Command::new("cmd") + .args(["/C", "ping -n 30 127.0.0.1 >NUL"]) + .spawn() + .unwrap(); + assert!(pid_is_alive(running.id()), "a running process is alive"); + let _ = running.kill(); + let _ = running.wait(); +} + #[test] fn pid_is_alive_false_for_pid_zero() { // pid 0 means the caller's process GROUP to kill(2), so kill(0, 0)