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.
This commit is contained in:
Teknium
2026-10-04 23:15:50 -07:00
parent e0cd07ee43
commit fc7d24650d
2 changed files with 29 additions and 6 deletions

View File

@@ -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
}
}

View File

@@ -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)