assay_core/kill_switch/
killer.rs

1use super::{KillMode, KillReport, KillRequest};
2#[allow(unused_imports)]
3use anyhow::Context;
4
5#[cfg(unix)]
6use nix::sys::signal::{kill, Signal};
7#[cfg(unix)]
8use nix::unistd::Pid;
9
10pub fn kill_pid(req: KillRequest) -> anyhow::Result<KillReport> {
11    let mut incident_dir = None;
12
13    // Optional: capture before kill
14    if req.capture_state {
15        if let Some(dir) = super::incident::write_incident_bundle_pre_kill(&req)? {
16            incident_dir = Some(dir);
17        }
18    }
19
20    // Kill main pid
21    let success = match req.mode {
22        KillMode::Immediate => kill_immediate(req.pid)?,
23        KillMode::Graceful { grace } => kill_graceful(req.pid, grace)?,
24    };
25
26    // Kill children if requested
27    let children_killed = if req.kill_children {
28        kill_descendants(req.pid).unwrap_or_default()
29    } else {
30        vec![]
31    };
32
33    // Post-kill update incident bundle
34    if let Some(ref dir) = incident_dir {
35        super::incident::write_incident_bundle_post_kill(dir, &req, success, &children_killed)?;
36    }
37
38    Ok(KillReport {
39        pid: req.pid,
40        success,
41        children_killed,
42        incident_dir,
43        error: if success {
44            None
45        } else {
46            Some("failed to terminate process".into())
47        },
48    })
49}
50
51#[cfg(unix)]
52fn kill_immediate(pid: u32) -> anyhow::Result<bool> {
53    kill(Pid::from_raw(pid as i32), Signal::SIGKILL)
54        .with_context(|| format!("SIGKILL failed for pid={pid}"))?;
55    Ok(true)
56}
57
58#[cfg(not(unix))]
59fn kill_immediate(_pid: u32) -> anyhow::Result<bool> {
60    anyhow::bail!("Kill Switch is not supported on this platform in v1.8 (Windows coming in v1.9)")
61}
62
63#[cfg(unix)]
64fn kill_graceful(pid: u32, grace: std::time::Duration) -> anyhow::Result<bool> {
65    let target = Pid::from_raw(pid as i32);
66
67    // SIGTERM
68    let _ = kill(target, Signal::SIGTERM);
69
70    // wait a bit (polling)
71    let start = std::time::Instant::now();
72    while start.elapsed() < grace {
73        if !is_running(pid) {
74            return Ok(true);
75        }
76        std::thread::sleep(std::time::Duration::from_millis(50));
77    }
78
79    // SIGKILL
80    let _ = kill(target, Signal::SIGKILL);
81    Ok(!is_running(pid))
82}
83
84#[cfg(not(unix))]
85fn kill_graceful(_pid: u32, _grace: std::time::Duration) -> anyhow::Result<bool> {
86    anyhow::bail!("Kill Switch is not supported on this platform in v1.8 (Windows coming in v1.9)")
87}
88
89#[allow(dead_code)]
90fn is_running(pid: u32) -> bool {
91    #[cfg(unix)]
92    {
93        // Portable enough: /proc exists on Linux; on macOS it doesn't.
94        // For macOS we fall back to sysinfo if available.
95        #[cfg(target_os = "linux")]
96        {
97            std::path::Path::new(&format!("/proc/{pid}")).exists()
98        }
99        #[cfg(not(target_os = "linux"))]
100        {
101            is_running_sysinfo(pid)
102        }
103    }
104    #[cfg(not(unix))]
105    {
106        // Suppress unused warning for the argument when this branch is taken
107        let _ = pid;
108        // Logic for Windows is technically not supported yet, so this function returning false is fine/irrelevant
109        // since kill_graceful (the only caller) bails out early anyway.
110        false
111    }
112}
113
114// Suppress dead code warning on Linux where this might not be called if /proc is used exclusively
115#[allow(dead_code)]
116fn is_running_sysinfo(pid: u32) -> bool {
117    #[cfg(feature = "kill-switch")]
118    {
119        use sysinfo::{Pid as SPid, System};
120        let mut sys = System::new();
121        sys.refresh_processes();
122        sys.process(SPid::from_u32(pid)).is_some()
123    }
124    #[cfg(not(feature = "kill-switch"))]
125    {
126        let _ = pid;
127        false
128    }
129}
130
131fn kill_descendants(parent_pid: u32) -> anyhow::Result<Vec<u32>> {
132    #[cfg(feature = "kill-switch")]
133    {
134        use sysinfo::{Pid as SPid, System};
135
136        let mut sys = System::new_all();
137        sys.refresh_processes();
138
139        #[allow(unused_mut)]
140        let mut killed = vec![];
141        let parent = SPid::from_u32(parent_pid);
142
143        // Build descendants list
144        let mut to_kill = vec![];
145        for (pid, proc_) in sys.processes() {
146            if let Some(ppid) = proc_.parent() {
147                if ppid == parent {
148                    to_kill.push(pid.as_u32());
149                }
150            }
151        }
152
153        // Naive BFS (good enough for v1.8 P0)
154        let mut idx = 0;
155        while idx < to_kill.len() {
156            let cur = SPid::from_u32(to_kill[idx]);
157            for (pid, proc_) in sys.processes() {
158                if proc_.parent() == Some(cur) {
159                    to_kill.push(pid.as_u32());
160                }
161            }
162            idx += 1;
163        }
164
165        // Kill children first (reverse order helps)
166        to_kill.sort_unstable();
167        to_kill.dedup();
168        to_kill.reverse();
169
170        for pid in to_kill {
171            #[cfg(unix)]
172            {
173                let _ = nix::sys::signal::kill(
174                    nix::unistd::Pid::from_raw(pid as i32),
175                    nix::sys::signal::Signal::SIGKILL,
176                );
177                killed.push(pid);
178            }
179            #[cfg(not(unix))]
180            {
181                let _ = pid; // Suppress unused variable on Windows
182            }
183        }
184
185        Ok(killed)
186    }
187    #[cfg(not(feature = "kill-switch"))]
188    {
189        let _ = parent_pid;
190        Ok(vec![])
191    }
192}