1use anyhow::Result;
2
3pub fn is_alive(pid: u32) -> bool {
5 #[cfg(unix)]
6 {
7 unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
8 }
9 #[cfg(windows)]
10 {
11 use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE, WAIT_TIMEOUT};
12 use windows_sys::Win32::System::Threading::{
13 GetExitCodeProcess, OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
14 };
15
16 unsafe {
17 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
18 if handle.is_null() {
19 return false;
20 }
21 let wait = WaitForSingleObject(handle, 0);
22 if wait == WAIT_TIMEOUT {
23 CloseHandle(handle);
24 return true;
25 }
26 let mut exit_code: u32 = 0;
27 GetExitCodeProcess(handle, &mut exit_code);
28 CloseHandle(handle);
29 exit_code == STILL_ACTIVE as u32
30 }
31 }
32}
33
34pub fn terminate_gracefully(pid: u32) -> Result<()> {
37 #[cfg(unix)]
38 {
39 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
40 if ret != 0 {
41 anyhow::bail!(
42 "Failed to send SIGTERM to PID {pid}: {}",
43 std::io::Error::last_os_error()
44 );
45 }
46 Ok(())
47 }
48 #[cfg(windows)]
49 {
50 force_kill(pid)
51 }
52}
53
54pub fn force_kill(pid: u32) -> Result<()> {
56 #[cfg(unix)]
57 {
58 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
59 if ret != 0 {
60 anyhow::bail!(
61 "Failed to send SIGKILL to PID {pid}: {}",
62 std::io::Error::last_os_error()
63 );
64 }
65 Ok(())
66 }
67 #[cfg(windows)]
68 {
69 use windows_sys::Win32::Foundation::CloseHandle;
70 use windows_sys::Win32::System::Threading::{
71 OpenProcess, TerminateProcess, PROCESS_TERMINATE,
72 };
73
74 unsafe {
75 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
76 if handle.is_null() {
77 anyhow::bail!(
78 "Failed to open PID {pid} for termination: {}",
79 std::io::Error::last_os_error()
80 );
81 }
82 let ok = TerminateProcess(handle, 1);
83 CloseHandle(handle);
84 if ok == 0 {
85 anyhow::bail!(
86 "Failed to terminate PID {pid}: {}",
87 std::io::Error::last_os_error()
88 );
89 }
90 Ok(())
91 }
92 }
93}
94
95pub fn find_pids_by_name(name: &str) -> Vec<u32> {
98 let my_pid = std::process::id();
99 let mut pids = Vec::new();
100
101 #[cfg(unix)]
102 {
103 if let Ok(output) = std::process::Command::new("pgrep")
105 .arg("-x")
106 .arg(name)
107 .output()
108 {
109 collect_pids(&output.stdout, my_pid, &mut pids);
110 }
111
112 if let Ok(output) = std::process::Command::new("pgrep")
115 .arg("-f")
116 .arg(format!("/{name}(\\s|$)"))
117 .output()
118 {
119 collect_pids(&output.stdout, my_pid, &mut pids);
120 }
121
122 pids.sort_unstable();
123 pids.dedup();
124 }
125
126 #[cfg(windows)]
127 {
128 if let Ok(output) = std::process::Command::new("tasklist")
129 .args([
130 "/FI",
131 &format!("IMAGENAME eq {name}.exe"),
132 "/FO",
133 "CSV",
134 "/NH",
135 ])
136 .output()
137 {
138 let stdout = String::from_utf8_lossy(&output.stdout);
139 for line in stdout.lines() {
140 let parts: Vec<&str> = line.split(',').collect();
141 if parts.len() >= 2 {
142 let pid_str = parts[1].trim().trim_matches('"');
143 if let Ok(pid) = pid_str.parse::<u32>() {
144 if pid != my_pid {
145 pids.push(pid);
146 }
147 }
148 }
149 }
150 }
151 }
152
153 pids
154}
155
156#[cfg(unix)]
157fn collect_pids(stdout: &[u8], exclude_pid: u32, out: &mut Vec<u32>) {
158 let text = String::from_utf8_lossy(stdout);
159 for line in text.lines() {
160 if let Ok(pid) = line.trim().parse::<u32>() {
161 if pid != exclude_pid {
162 out.push(pid);
163 }
164 }
165 }
166}
167
168pub fn find_killable_pids(name: &str) -> Vec<u32> {
172 let all = find_pids_by_name(name);
173 let mcp_pids = find_mcp_server_pids(name);
174 all.into_iter().filter(|p| !mcp_pids.contains(p)).collect()
175}
176
177#[cfg(unix)]
178fn find_mcp_server_pids(name: &str) -> Vec<u32> {
179 find_pids_by_name(name)
180 .into_iter()
181 .filter(|&pid| is_mcp_stdio_process(pid))
182 .collect()
183}
184
185#[cfg(not(unix))]
186fn find_mcp_server_pids(_name: &str) -> Vec<u32> {
187 Vec::new()
188}
189
190#[cfg(unix)]
191fn is_mcp_stdio_process(pid: u32) -> bool {
192 if let Ok(output) = std::process::Command::new("ps")
193 .args(["-o", "ppid=,command=", "-p", &pid.to_string()])
194 .output()
195 {
196 let text = String::from_utf8_lossy(&output.stdout);
197 let t = text.trim();
198 if t.contains("Cursor") || t.contains("cursor") || t.contains("code") {
199 return true;
200 }
201 let parts: Vec<&str> = t.split_whitespace().collect();
202 if let Some(ppid_str) = parts.first() {
203 if let Ok(ppid) = ppid_str.parse::<u32>() {
204 if let Ok(pp_out) = std::process::Command::new("ps")
205 .args(["-o", "command=", "-p", &ppid.to_string()])
206 .output()
207 {
208 let pp_cmd = String::from_utf8_lossy(&pp_out.stdout);
209 if pp_cmd.contains("Cursor")
210 || pp_cmd.contains("cursor")
211 || pp_cmd.contains("code")
212 {
213 return true;
214 }
215 }
216 }
217 }
218 let cmd_part = parts.get(1..).map(|p| p.join(" ")).unwrap_or_default();
219 if (cmd_part.ends_with("/lean-ctx") || cmd_part == "lean-ctx")
221 && !cmd_part.contains("proxy")
222 && !cmd_part.contains("dashboard")
223 && !cmd_part.contains("daemon")
224 && !cmd_part.contains("stop")
225 && !cmd_part.contains("hook")
226 {
227 return true;
228 }
229 if cmd_part.contains("hook observe")
231 || cmd_part.contains("hook rewrite")
232 || cmd_part.contains("hook redirect")
233 {
234 return true;
235 }
236 }
237 false
238}
239
240pub fn kill_all_by_name(name: &str) -> usize {
243 let pids = find_killable_pids(name);
244 if pids.is_empty() {
245 return 0;
246 }
247
248 for &pid in &pids {
249 let _ = terminate_gracefully(pid);
250 }
251
252 std::thread::sleep(std::time::Duration::from_millis(500));
253
254 let mut killed = 0;
255 for &pid in &pids {
256 if is_alive(pid) {
257 let _ = force_kill(pid);
258 }
259 killed += 1;
260 }
261
262 std::thread::sleep(std::time::Duration::from_millis(200));
263
264 killed
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn current_process_is_alive() {
273 assert!(is_alive(std::process::id()));
274 }
275
276 #[test]
277 fn bogus_pid_is_not_alive() {
278 assert!(!is_alive(u32::MAX - 42));
279 }
280}