assay_core/kill_switch/
killer.rs1use 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 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 let success = match req.mode {
22 KillMode::Immediate => kill_immediate(req.pid)?,
23 KillMode::Graceful { grace } => kill_graceful(req.pid, grace)?,
24 };
25
26 let children_killed = if req.kill_children {
28 kill_descendants(req.pid).unwrap_or_default()
29 } else {
30 vec![]
31 };
32
33 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 let _ = kill(target, Signal::SIGTERM);
69
70 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 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 #[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 let _ = pid;
108 false
111 }
112}
113
114#[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 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 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 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; }
183 }
184
185 Ok(killed)
186 }
187 #[cfg(not(feature = "kill-switch"))]
188 {
189 let _ = parent_pid;
190 Ok(vec![])
191 }
192}