Skip to main content

planarian_backend_linux/
process.rs

1//! `ProcessBackend` and `Clock` on the real system.
2
3use planarian_schema::{Clock, Millis, ProcessBackend, Timestamp};
4use rustix::process::{Pid, Signal, kill_process_group, test_kill_process};
5use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
6
7#[derive(Clone, Copy, Debug, Default)]
8pub struct Processes;
9
10pub const KILL_GRACE: Duration = Duration::from_secs(5);
11
12impl ProcessBackend for Processes {
13    /// SIGTERM the group, wait up to 5 s, then SIGKILL; true once the leader is gone.
14    fn kill_group(&self, pid: u32) -> bool {
15        let Some(p) = Pid::from_raw(pid as i32) else { return true };
16        if test_kill_process(p).is_err() {
17            return true;
18        }
19        let _ = kill_process_group(p, Signal::TERM);
20        let deadline = Instant::now() + KILL_GRACE;
21        while Instant::now() < deadline {
22            if test_kill_process(p).is_err() {
23                return true;
24            }
25            std::thread::sleep(Duration::from_millis(50));
26        }
27        let _ = kill_process_group(p, Signal::KILL);
28        std::thread::sleep(Duration::from_millis(200));
29        test_kill_process(p).is_err()
30    }
31
32    fn alive(&self, pid: u32) -> bool {
33        Pid::from_raw(pid as i32).is_some_and(|p| test_kill_process(p).is_ok())
34    }
35}
36
37fn boottime_ms() -> u64 {
38    let t = rustix::time::clock_gettime(rustix::time::ClockId::Boottime);
39    t.tv_sec as u64 * 1000 + t.tv_nsec as u64 / 1_000_000
40}
41
42/// The wall clock and the boot clock, read once at the process's first use.
43static ANCHOR: std::sync::OnceLock<(u64, u64)> = std::sync::OnceLock::new();
44
45impl Clock for Processes {
46    /// Wall-clock time, read once when the process first asks and advanced
47    /// from then on by `CLOCK_BOOTTIME` (ยง12h): every interval this process
48    /// measures (cooldowns, settle and activation windows, probe freshness)
49    /// is immune to the wall clock being moved, and the times it records
50    /// stay wall-clock times, comparable across boots.
51    fn now(&self) -> Timestamp {
52        let (wall0, boot0) = *ANCHOR.get_or_init(|| {
53            let wall = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0);
54            (wall, boottime_ms())
55        });
56        Timestamp(wall0.saturating_add(boottime_ms().saturating_sub(boot0)))
57    }
58
59    fn boot_now(&self) -> Timestamp {
60        Timestamp(boottime_ms())
61    }
62
63    fn sleep(&self, d: Millis) {
64        std::thread::sleep(Duration::from_millis(d.0));
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn kills_a_group() {
74        let mut c = std::process::Command::new("sh");
75        c.args(["-c", "sleep 30 & sleep 30"]);
76        std::os::unix::process::CommandExt::process_group(&mut c, 0);
77        let mut child = c.spawn().unwrap();
78        let pid = child.id();
79        assert!(Processes.alive(pid));
80        let reaper = std::thread::spawn(move || child.wait());
81        assert!(Processes.kill_group(pid));
82        let _ = reaper.join();
83        assert!(!Processes.alive(pid));
84    }
85}