a3s_box_runtime/
process.rs1#[cfg(unix)]
8pub fn is_process_alive(pid: u32) -> bool {
9 let Ok(pid) = i32::try_from(pid) else {
10 return false;
11 };
12 let result = unsafe { libc::kill(pid, 0) };
13 result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
14}
15
16#[cfg(windows)]
17pub fn is_process_alive(pid: u32) -> bool {
18 use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE};
19 use windows_sys::Win32::System::Threading::{
20 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
21 };
22
23 unsafe {
24 let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
25 if handle == 0 {
26 return false;
27 }
28 let mut exit_code = 0u32;
29 let ok = GetExitCodeProcess(handle, &mut exit_code);
30 CloseHandle(handle);
31 ok != 0 && exit_code == STILL_ACTIVE as u32
32 }
33}
34
35#[cfg(not(any(unix, windows)))]
36pub fn is_process_alive(_pid: u32) -> bool {
37 false
38}
39
40#[cfg(target_os = "linux")]
47pub fn pid_start_time(pid: u32) -> Option<u64> {
48 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
49 linux_process_identity_from_stat(&stat).map(|(_, start_time)| start_time)
50}
51
52#[cfg(not(target_os = "linux"))]
53pub fn pid_start_time(_pid: u32) -> Option<u64> {
54 None
55}
56
57pub fn is_process_alive_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
62 if !is_process_alive(pid) {
63 return false;
64 }
65
66 match expected_start_time {
67 Some(expected) => pid_start_time(pid) == Some(expected),
68 None => true,
69 }
70}
71
72#[cfg(target_os = "linux")]
79pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
80 let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
81 return false;
82 };
83 linux_process_identity_from_stat(&stat).is_some_and(|(state, start_time)| {
84 is_linux_process_state_running(state)
85 && expected_start_time
86 .map(|expected| expected == start_time)
87 .unwrap_or(true)
88 })
89}
90
91#[cfg(not(target_os = "linux"))]
92pub fn is_process_running_with_identity(pid: u32, expected_start_time: Option<u64>) -> bool {
93 is_process_alive_with_identity(pid, expected_start_time)
94}
95
96#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
104pub(crate) fn wait_for_process_stop_with_identity(
105 pid: u32,
106 expected_start_time: u64,
107 timeout: std::time::Duration,
108) -> bool {
109 let deadline = std::time::Instant::now() + timeout;
110 loop {
111 if !is_process_running_with_identity(pid, Some(expected_start_time)) {
112 let _ = try_reap_exited_child_with_identity(pid, expected_start_time);
117 return true;
118 }
119 if std::time::Instant::now() >= deadline {
120 return false;
121 }
122 std::thread::sleep(std::time::Duration::from_millis(10));
123 }
124}
125
126#[cfg(target_os = "linux")]
135pub(crate) fn wait_for_process_exit_with_identity(
136 pid: u32,
137 expected_start_time: u64,
138 timeout: std::time::Duration,
139) -> bool {
140 let deadline = std::time::Instant::now() + timeout;
141 loop {
142 if !is_process_alive_with_identity(pid, Some(expected_start_time)) {
143 return true;
144 }
145 if !is_process_running_with_identity(pid, Some(expected_start_time))
146 && try_reap_exited_child_with_identity(pid, expected_start_time)
147 {
148 return true;
149 }
150 if std::time::Instant::now() >= deadline {
151 return false;
152 }
153 std::thread::sleep(std::time::Duration::from_millis(10));
154 }
155}
156
157#[cfg(target_os = "linux")]
162fn try_reap_exited_child_with_identity(pid: u32, expected_start_time: u64) -> bool {
163 if !is_process_alive_with_identity(pid, Some(expected_start_time)) {
164 return true;
165 }
166
167 let Ok(raw_pid) = i32::try_from(pid) else {
168 return false;
169 };
170 let mut status = 0;
171 let waited = unsafe { libc::waitpid(raw_pid, &mut status, libc::WNOHANG) };
172 waited == raw_pid || !is_process_alive_with_identity(pid, Some(expected_start_time))
173}
174
175#[cfg(not(target_os = "linux"))]
176#[allow(dead_code)]
177fn try_reap_exited_child_with_identity(pid: u32, expected_start_time: u64) -> bool {
178 !is_process_alive_with_identity(pid, Some(expected_start_time))
179}
180
181#[cfg(target_os = "linux")]
182fn linux_process_identity_from_stat(stat: &str) -> Option<(char, u64)> {
183 let fields: Vec<&str> = stat
186 .get(stat.rfind(')')? + 1..)?
187 .split_whitespace()
188 .collect();
189 let state = fields.first()?.chars().next()?;
190 let start_time = fields.get(19)?.parse().ok()?;
191 Some((state, start_time))
192}
193
194#[cfg(target_os = "linux")]
195const fn is_linux_process_state_running(state: char) -> bool {
196 !matches!(state, 'Z' | 'X' | 'x')
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn current_process_is_alive() {
205 assert!(is_process_alive(std::process::id()));
206 }
207
208 #[test]
209 fn missing_process_is_not_alive() {
210 assert!(!is_process_alive(0x7fff_fffe));
211 }
212
213 #[cfg(target_os = "linux")]
214 #[test]
215 fn parses_start_time_after_complex_command_name() {
216 let stat =
217 "123 (command (with) spaces) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242";
218 assert_eq!(linux_process_identity_from_stat(stat), Some(('S', 4242)));
219 assert_eq!(linux_process_identity_from_stat("malformed"), None);
220 assert_eq!(linux_process_identity_from_stat("123 (short) S 1"), None);
221 }
222
223 #[cfg(target_os = "linux")]
224 #[test]
225 fn identity_rejects_a_reused_pid() {
226 let pid = std::process::id();
227 let start_time = pid_start_time(pid);
228 assert!(start_time.is_some());
229 assert!(is_process_alive_with_identity(pid, start_time));
230 assert!(!is_process_alive_with_identity(pid, Some(u64::MAX)));
231 assert!(is_process_alive_with_identity(pid, None));
232 assert!(!is_process_alive_with_identity(0x7fff_fffe, None));
233 assert!(is_process_running_with_identity(pid, start_time));
234 }
235
236 #[cfg(target_os = "linux")]
237 #[test]
238 fn classifies_zombie_and_dead_states_as_completed() {
239 for state in ['Z', 'X', 'x'] {
240 let stat = format!(
241 "123 (completed worker) {state} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 4242"
242 );
243 assert_eq!(linux_process_identity_from_stat(&stat), Some((state, 4242)));
244 assert!(!is_linux_process_state_running(state));
245 }
246 for state in ['R', 'S', 'D', 'T', 't', 'I'] {
247 assert!(is_linux_process_state_running(state));
248 }
249 }
250
251 #[cfg(target_os = "linux")]
252 #[test]
253 #[allow(clippy::zombie_processes)] fn recovered_identity_reaps_an_exited_child() {
255 let child = std::process::Command::new("true").spawn().unwrap();
256 let pid = child.id();
257 let start_time = pid_start_time(pid).unwrap();
258 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
259 while is_process_running_with_identity(pid, Some(start_time))
260 && std::time::Instant::now() < deadline
261 {
262 std::thread::sleep(std::time::Duration::from_millis(5));
263 }
264
265 assert!(is_process_alive_with_identity(pid, Some(start_time)));
266 assert!(!is_process_running_with_identity(pid, Some(start_time)));
267 assert!(wait_for_process_exit_with_identity(
268 pid,
269 start_time,
270 std::time::Duration::from_secs(1),
271 ));
272 assert!(!is_process_alive_with_identity(pid, Some(start_time)));
273 }
274
275 #[cfg(target_os = "linux")]
276 #[test]
277 #[allow(clippy::zombie_processes)] fn stop_waiter_reaps_an_exited_child() {
279 let child = std::process::Command::new("true").spawn().unwrap();
280 let pid = child.id();
281 let start_time = pid_start_time(pid).unwrap();
282 drop(child);
283
284 assert!(wait_for_process_stop_with_identity(
285 pid,
286 start_time,
287 std::time::Duration::from_secs(1),
288 ));
289 assert!(!is_process_alive_with_identity(pid, Some(start_time)));
290 }
291}