use std::sync::{Mutex, MutexGuard};
pub(crate) struct PidGate {
inner: Mutex<GateState>,
}
struct GateState {
pid: Option<u32>,
retired: bool,
}
impl PidGate {
pub(crate) fn new(pid: Option<u32>) -> Self {
Self {
inner: Mutex::new(GateState {
pid,
retired: false,
}),
}
}
pub(crate) fn with_live_pid<R>(&self, default: R, f: impl FnOnce(u32) -> R) -> R {
let guard = self.lock();
if guard.retired {
return default;
}
match guard.pid {
Some(pid) => f(pid),
None => default,
}
}
pub(crate) fn retire(&self) {
self.lock().retired = true;
}
pub(crate) fn is_retired(&self) -> bool {
self.lock().retired
}
pub(crate) fn reap_under_lock(&self, reap: impl FnOnce() -> bool) -> bool {
let mut guard = self.lock();
let reaped = reap();
if reaped {
guard.retired = true;
}
reaped
}
fn lock(&self) -> MutexGuard<'_, GateState> {
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
}
pub(crate) fn force_kill(gate: &PidGate) {
gate.with_live_pid((), raw_force_kill);
}
fn raw_force_kill(pid: u32) {
#[cfg(unix)]
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
}
#[cfg(windows)]
unsafe {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_TERMINATE, TerminateProcess,
};
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if !handle.is_null() {
TerminateProcess(handle, 1);
CloseHandle(handle);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[test]
fn a_retired_gate_runs_no_raw_kill() {
let gate = PidGate::new(Some(4321));
gate.retire();
let mut fired = false;
let ran = gate.with_live_pid(false, |_pid| {
fired = true;
true
});
assert!(!ran, "with_live_pid must report the closure did not run");
assert!(!fired, "no raw kill may run once the pid is retired");
}
#[test]
fn a_live_gate_runs_the_kill_with_the_pid() {
let gate = PidGate::new(Some(4321));
let seen = gate.with_live_pid(None, Some);
assert_eq!(seen, Some(4321), "a live gate hands the pid to the closure");
}
#[test]
fn a_pid_less_gate_never_kills() {
let gate = PidGate::new(None);
let mut fired = false;
gate.with_live_pid((), |_| fired = true);
assert!(!fired, "a pid-less gate has no OS process to kill");
}
#[test]
fn reap_under_lock_retires_in_one_critical_section() {
let gate = PidGate::new(Some(4321));
assert!(
gate.reap_under_lock(|| true),
"the probe reported the child reaped"
);
assert!(gate.is_retired(), "a successful reap retires the pid");
let mut fired = false;
gate.with_live_pid((), |_| fired = true);
assert!(!fired, "no kill runs after reap_under_lock retired the pid");
}
#[test]
fn a_poll_driven_reap_retires_only_when_the_poll_reaps() {
let gate = PidGate::new(Some(4321));
for _ in 0..2 {
assert!(
!gate.reap_under_lock(|| false),
"a Pending poll does not reap"
);
assert!(
!gate.is_retired(),
"an un-reaped child leaves the gate live"
);
let mut fired = false;
gate.with_live_pid((), |_| fired = true);
assert!(fired, "a live child stays killable between reap polls");
}
let freed = AtomicBool::new(false);
let reaped = gate.reap_under_lock(|| {
freed.store(true, Ordering::SeqCst); true
});
assert!(
reaped && freed.load(Ordering::SeqCst),
"the reaping poll reaps"
);
assert!(
gate.is_retired(),
"the reaping poll retires the gate atomically"
);
let mut fired_after = false;
gate.with_live_pid((), |_| fired_after = true);
assert!(
!fired_after,
"no kill runs after the reaping poll freed the pid"
);
}
#[test]
fn reap_under_lock_leaves_a_live_child_killable() {
let gate = PidGate::new(Some(4321));
assert!(
!gate.reap_under_lock(|| false),
"the probe reported the child still running"
);
assert!(!gate.is_retired(), "an un-reaped child is not retired");
let mut fired = false;
gate.with_live_pid((), |_| fired = true);
assert!(fired, "a still-live child stays killable by the watchdog");
}
#[test]
fn no_gated_kill_ever_lands_after_the_pid_is_freed() {
for _ in 0..2_000 {
let gate = Arc::new(PidGate::new(Some(4321)));
let freed = Arc::new(AtomicBool::new(false));
let killed_after_free = Arc::new(AtomicUsize::new(0));
let barrier = Arc::new(Barrier::new(2));
let reaper = {
let gate = gate.clone();
let freed = freed.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
gate.reap_under_lock(|| {
freed.store(true, Ordering::SeqCst);
true
});
})
};
let killer = {
let gate = gate.clone();
let freed = freed.clone();
let killed_after_free = killed_after_free.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
gate.with_live_pid((), |_pid| {
if freed.load(Ordering::SeqCst) {
killed_after_free.fetch_add(1, Ordering::SeqCst);
}
});
})
};
reaper.join().expect("reaper thread");
killer.join().expect("killer thread");
assert_eq!(
killed_after_free.load(Ordering::SeqCst),
0,
"a raw kill must never run after the pid was freed"
);
}
}
#[test]
fn a_retire_before_a_separate_pid_free_still_bars_a_racing_kill() {
for _ in 0..2_000 {
let gate = Arc::new(PidGate::new(Some(4321)));
let freed = Arc::new(AtomicBool::new(false));
let killed_after_free = Arc::new(AtomicUsize::new(0));
let barrier = Arc::new(Barrier::new(2));
let dropper = {
let gate = gate.clone();
let freed = freed.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
gate.retire();
freed.store(true, Ordering::SeqCst);
})
};
let killer = {
let gate = gate.clone();
let freed = freed.clone();
let killed_after_free = killed_after_free.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
gate.with_live_pid((), |_pid| {
if freed.load(Ordering::SeqCst) {
killed_after_free.fetch_add(1, Ordering::SeqCst);
}
});
})
};
dropper.join().expect("dropper thread");
killer.join().expect("killer thread");
assert_eq!(
killed_after_free.load(Ordering::SeqCst),
0,
"a raw kill must never run after the structural drop freed the pid"
);
}
}
}