rootcx_platform/
process.rs1use std::time::Duration;
2use tracing::{info, warn};
3
4pub fn process_alive(pid: u32) -> bool {
5 #[cfg(unix)]
6 { unsafe { libc::kill(pid as i32, 0) == 0 } }
7 #[cfg(windows)]
8 {
9 std::process::Command::new("tasklist")
10 .args(["/FI", &format!("PID eq {pid}"), "/NH"])
11 .stdout(std::process::Stdio::piped())
12 .stderr(std::process::Stdio::null())
13 .output()
14 .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
15 .unwrap_or(false)
16 }
17}
18
19pub async fn kill_gracefully(pid: u32, timeout: Duration) {
20 #[cfg(unix)]
21 {
22 unsafe { libc::kill(pid as i32, libc::SIGTERM); }
23 let poll = Duration::from_millis(100);
24 for _ in 0..(timeout.as_millis() / poll.as_millis()).max(1) {
25 tokio::time::sleep(poll).await;
26 if !process_alive(pid) { info!(pid, "exited after SIGTERM"); return; }
27 }
28 warn!(pid, "SIGTERM timeout → SIGKILL");
29 unsafe { libc::kill(pid as i32, libc::SIGKILL); }
30 }
31 #[cfg(windows)]
32 {
33 let _ = timeout; match tokio::process::Command::new("taskkill").args(["/PID", &pid.to_string(), "/F"]).status().await {
35 Ok(s) if s.success() => info!(pid, "killed via taskkill"),
36 Ok(s) => warn!(pid, code = ?s.code(), "taskkill non-zero"),
37 Err(e) => warn!(pid, "taskkill: {e}"),
38 }
39 }
40}
41
42pub async fn kill_listeners_on_port(port: u16) {
43 let pids = find_listeners(port).await;
44 if pids.is_empty() { return; }
45 info!(?pids, port, "killing stale listeners");
46 for &pid in &pids { kill_gracefully(pid, Duration::from_millis(500)).await; }
47}
48
49#[cfg(target_os = "macos")]
51async fn find_listeners(port: u16) -> Vec<u32> {
52 tokio::process::Command::new("lsof")
53 .args(["-ti", &format!(":{port}")])
54 .output().await
55 .inspect_err(|e| warn!(port, "lsof: {e}"))
56 .map(|o| String::from_utf8_lossy(&o.stdout).lines()
57 .filter_map(|s| s.trim().parse().ok()).collect())
58 .unwrap_or_default()
59}
60
61#[cfg(target_os = "linux")]
63async fn find_listeners(port: u16) -> Vec<u32> {
64 let hex = format!("{port:04X}");
65 let inodes = tcp_inodes(&hex).await;
66 if inodes.is_empty() { return vec![]; }
67 pids_for_inodes(&inodes).await
68}
69
70#[cfg(target_os = "linux")]
73async fn tcp_inodes(hex_port: &str) -> Vec<u64> {
74 let mut out = Vec::new();
75 for path in ["/proc/net/tcp", "/proc/net/tcp6"] {
76 let Ok(s) = tokio::fs::read_to_string(path).await else { continue };
77 for line in s.lines().skip(1) {
78 let f: Vec<&str> = line.split_whitespace().collect();
79 if f.len() < 10 || f[3] != "0A" { continue }
80 if f[1].split(':').nth(1).is_some_and(|p| p.eq_ignore_ascii_case(hex_port)) {
81 if let Ok(i) = f[9].parse() { out.push(i); }
82 }
83 }
84 }
85 out
86}
87
88#[cfg(target_os = "linux")]
89async fn pids_for_inodes(inodes: &[u64]) -> Vec<u32> {
90 let mut out = Vec::new();
91 let Ok(mut dir) = tokio::fs::read_dir("/proc").await else { return out };
92 while let Ok(Some(e)) = dir.next_entry().await {
93 let Ok(pid) = e.file_name().to_string_lossy().parse::<u32>() else { continue };
94 let Ok(mut fds) = tokio::fs::read_dir(e.path().join("fd")).await else { continue };
95 'fd: while let Ok(Some(fd)) = fds.next_entry().await {
96 let Ok(lnk) = tokio::fs::read_link(fd.path()).await else { continue };
97 if let Some(i) = lnk.to_string_lossy()
98 .strip_prefix("socket:[").and_then(|s| s.strip_suffix(']'))
99 .and_then(|s| s.parse::<u64>().ok())
100 {
101 if inodes.contains(&i) { out.push(pid); break 'fd; }
102 }
103 }
104 }
105 out
106}
107
108#[cfg(windows)]
109async fn find_listeners(port: u16) -> Vec<u32> {
110 tokio::process::Command::new("netstat").args(["-ano", "-p", "TCP"])
111 .output().await
112 .inspect_err(|e| warn!(port, "netstat: {e}"))
113 .map(|o| {
114 let needle = format!(":{port}");
115 String::from_utf8_lossy(&o.stdout).lines()
116 .filter(|l| l.contains(&needle) && l.contains("LISTENING"))
117 .filter_map(|l| l.split_whitespace().last()?.parse().ok())
118 .collect::<std::collections::HashSet<u32>>().into_iter().collect()
119 })
120 .unwrap_or_default()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 #[test] fn alive_check() {
127 assert!(process_alive(std::process::id()));
128 assert!(!process_alive(4_000_000));
129 }
130}