1use std::cell::RefCell;
33use std::sync::atomic::{AtomicBool, Ordering};
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36
37pub const SUBPROCESS_TERM_GRACE: Duration = Duration::from_secs(2);
42
43#[derive(Clone, Default)]
44struct OpInterrupt {
45 cancel: Option<Arc<AtomicBool>>,
46 deadline: Option<Instant>,
47}
48
49thread_local! {
50 static CURRENT: RefCell<Option<OpInterrupt>> = const { RefCell::new(None) };
51}
52
53pub struct OpInterruptGuard {
57 #[allow(clippy::option_option)]
60 prev: Option<Option<OpInterrupt>>,
61}
62
63impl Drop for OpInterruptGuard {
64 fn drop(&mut self) {
65 if let Some(prev) = self.prev.take() {
66 CURRENT.with(|slot| *slot.borrow_mut() = prev);
67 }
68 }
69}
70
71pub fn install(cancel: Option<Arc<AtomicBool>>, deadline: Option<Instant>) -> OpInterruptGuard {
76 let prev = CURRENT.with(|slot| slot.borrow_mut().replace(OpInterrupt { cancel, deadline }));
77 OpInterruptGuard { prev: Some(prev) }
78}
79
80pub fn installed() -> bool {
85 CURRENT.with(|slot| slot.borrow().is_some())
86}
87
88pub fn requested() -> bool {
92 CURRENT.with(|slot| {
93 let ctx = slot.borrow();
94 let Some(ctx) = ctx.as_ref() else {
95 return false;
96 };
97 if ctx
98 .cancel
99 .as_ref()
100 .is_some_and(|token| token.load(Ordering::SeqCst))
101 {
102 return true;
103 }
104 ctx.deadline
105 .is_some_and(|deadline| Instant::now() >= deadline)
106 })
107}
108
109pub fn configure_kill_group(command: &mut std::process::Command) {
114 #[cfg(unix)]
115 {
116 use std::os::unix::process::CommandExt;
117 command.process_group(0);
118 }
119 #[cfg(not(unix))]
120 {
121 let _ = command;
122 }
123}
124
125pub fn signal_pid_and_group(pid: u32, signal: i32) {
127 #[cfg(unix)]
128 {
129 extern "C" {
132 fn kill(pid: i32, sig: i32) -> i32;
133 }
134 unsafe {
135 kill(-(pid as i32), signal);
136 kill(pid as i32, signal);
137 }
138 }
139 #[cfg(not(unix))]
140 {
141 let _ = (pid, signal);
142 }
143}
144
145pub fn signal_pid_tree_and_group(pid: u32, signal: i32) {
150 #[cfg(unix)]
151 {
152 for child_pid in descendant_pids(pid) {
153 signal_pid_and_group(child_pid, signal);
154 }
155 signal_pid_and_group(pid, signal);
156 }
157 #[cfg(not(unix))]
158 {
159 let _ = (pid, signal);
160 }
161}
162
163#[cfg(unix)]
164fn descendant_pids(root: u32) -> Vec<u32> {
165 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
166
167 let mut sys = System::new();
168 sys.refresh_processes_specifics(ProcessesToUpdate::All, false, ProcessRefreshKind::nothing());
169 let edges = sys
170 .processes()
171 .iter()
172 .filter_map(|(pid, process)| Some((pid.as_u32(), process.parent()?.as_u32())))
173 .collect::<Vec<_>>();
174 descendant_pids_from_parent_edges(root, &edges)
175}
176
177#[cfg(unix)]
178fn descendant_pids_from_parent_edges(root: u32, edges: &[(u32, u32)]) -> Vec<u32> {
179 use std::collections::{HashMap, HashSet};
180
181 let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
182 for &(pid, parent) in edges {
183 children.entry(parent).or_default().push(pid);
184 }
185
186 let mut seen = HashSet::new();
187 let mut stack = vec![(root, 0usize)];
188 let mut descendants = Vec::new();
189 while let Some((pid, depth)) = stack.pop() {
190 if !seen.insert(pid) {
191 continue;
192 }
193 if pid != root {
194 descendants.push((pid, depth));
195 }
196 if let Some(kids) = children.get(&pid) {
197 for &child in kids {
198 stack.push((child, depth + 1));
199 }
200 }
201 }
202
203 descendants.sort_by(|(left_pid, left_depth), (right_pid, right_depth)| {
204 right_depth
205 .cmp(left_depth)
206 .then_with(|| left_pid.cmp(right_pid))
207 });
208 descendants.into_iter().map(|(pid, _depth)| pid).collect()
209}
210
211pub enum ChildWait {
213 Exited(std::process::ExitStatus),
215 TimedOut,
217 Interrupted(Option<std::process::ExitStatus>),
221}
222
223pub fn wait_child_interruptible(
232 child: &mut std::process::Child,
233 timeout: Option<Duration>,
234) -> std::io::Result<ChildWait> {
235 let deadline = timeout.map(|limit| Instant::now() + limit);
236 loop {
237 if let Some(status) = child.try_wait()? {
238 return Ok(ChildWait::Exited(status));
239 }
240 if requested() {
241 let status = terminate_child_group(child);
242 return Ok(ChildWait::Interrupted(status));
243 }
244 if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
245 if let Some(pid) = child_pid(child) {
247 signal_pid_tree_and_group(pid, 9);
248 }
249 let _ = child.kill();
250 let _ = child.wait();
251 return Ok(ChildWait::TimedOut);
252 }
253 std::thread::sleep(Duration::from_millis(20));
254 }
255}
256
257pub fn terminate_child_group(child: &mut std::process::Child) -> Option<std::process::ExitStatus> {
263 #[cfg(unix)]
264 {
265 if let Some(pid) = child_pid(child) {
266 const SIGTERM: i32 = 15;
267 signal_pid_tree_and_group(pid, SIGTERM);
268 let grace_deadline = Instant::now() + SUBPROCESS_TERM_GRACE;
269 loop {
270 match child.try_wait() {
271 Ok(Some(status)) => {
272 signal_pid_tree_and_group(pid, 9);
275 return Some(status);
276 }
277 Ok(None) => {
278 if Instant::now() >= grace_deadline {
279 break;
280 }
281 std::thread::sleep(Duration::from_millis(20));
282 }
283 Err(_) => break,
284 }
285 }
286 signal_pid_tree_and_group(pid, 9);
287 }
288 }
289 let _ = child.kill();
290 child.wait().ok()
291}
292
293fn child_pid(child: &std::process::Child) -> Option<u32> {
294 let pid = child.id();
295 (pid > 0).then_some(pid)
296}
297
298pub(crate) fn drain_captured_pipe(
308 rx: &std::sync::mpsc::Receiver<Vec<u8>>,
309 killed: bool,
310 child_pid: u32,
311) -> Vec<u8> {
312 use std::sync::mpsc::RecvTimeoutError;
313 if killed {
314 return rx
315 .recv_timeout(Duration::from_millis(100))
316 .unwrap_or_default();
317 }
318 loop {
319 match rx.recv_timeout(Duration::from_millis(20)) {
320 Ok(buf) => return buf,
321 Err(RecvTimeoutError::Disconnected) => return Vec::new(),
322 Err(RecvTimeoutError::Timeout) => {
323 if requested() {
324 const SIGTERM: i32 = 15;
325 signal_pid_tree_and_group(child_pid, SIGTERM);
326 if let Ok(buf) = rx.recv_timeout(SUBPROCESS_TERM_GRACE) {
327 signal_pid_tree_and_group(child_pid, 9);
328 return buf;
329 }
330 signal_pid_tree_and_group(child_pid, 9);
331 return rx
332 .recv_timeout(Duration::from_millis(100))
333 .unwrap_or_default();
334 }
335 }
336 }
337 }
338}
339
340pub(crate) fn spawn_pipe_drain<R: std::io::Read + Send + 'static>(
342 mut reader: R,
343) -> std::sync::mpsc::Receiver<Vec<u8>> {
344 let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
345 std::thread::spawn(move || {
346 let mut buf = Vec::new();
347 let _ = reader.read_to_end(&mut buf);
348 let _ = tx.send(buf);
349 });
350 rx
351}
352
353pub fn capture_output_interruptible(
360 command: &mut std::process::Command,
361) -> std::io::Result<std::process::Output> {
362 use std::process::Stdio;
363 command
364 .stdout(Stdio::piped())
365 .stderr(Stdio::piped())
366 .stdin(Stdio::null());
367 configure_kill_group(command);
368 let mut child = command.spawn()?;
369 let pid = child.id();
370 let rx_out = child.stdout.take().map(spawn_pipe_drain);
371 let rx_err = child.stderr.take().map(spawn_pipe_drain);
372
373 let (status, killed) = match wait_child_interruptible(&mut child, None)? {
374 ChildWait::Exited(status) => (status, false),
375 ChildWait::TimedOut => (std::process::ExitStatus::default(), true),
377 ChildWait::Interrupted(status) => (status.unwrap_or_default(), true),
378 };
379 let stdout = rx_out
380 .map(|rx| drain_captured_pipe(&rx, killed, pid))
381 .unwrap_or_default();
382 let stderr = rx_err
383 .map(|rx| drain_captured_pipe(&rx, killed, pid))
384 .unwrap_or_default();
385 Ok(std::process::Output {
386 status,
387 stdout,
388 stderr,
389 })
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn requested_is_false_without_context() {
398 assert!(!requested());
399 }
400
401 #[test]
402 fn installed_tracks_guard_lifetime() {
403 assert!(!installed());
404 let guard = install(None, None);
405 assert!(installed());
406 drop(guard);
407 assert!(!installed());
408 }
409
410 #[test]
411 fn cancel_token_trips_requested_and_guard_restores() {
412 let token = Arc::new(AtomicBool::new(false));
413 let guard = install(Some(token.clone()), None);
414 assert!(!requested());
415 token.store(true, Ordering::SeqCst);
416 assert!(requested());
417 drop(guard);
418 assert!(!requested());
419 }
420
421 #[test]
422 fn deadline_trips_requested() {
423 let expired = Instant::now()
424 .checked_sub(Duration::from_millis(1))
425 .expect("monotonic clock supports a 1ms test lookback");
426 let _guard = install(None, Some(expired));
427 assert!(requested());
428 }
429
430 #[test]
431 fn nested_installs_restore_in_order() {
432 let outer_token = Arc::new(AtomicBool::new(true));
433 let _outer = install(Some(outer_token), None);
434 assert!(requested());
435 {
436 let _inner = install(None, None);
437 assert!(!requested());
438 }
439 assert!(requested());
440 }
441
442 #[cfg(unix)]
443 #[test]
444 fn descendant_pids_from_parent_edges_returns_deepest_first_tree_only() {
445 let edges = [
446 (20, 10),
447 (30, 20),
448 (40, 20),
449 (50, 30),
450 (60, 99),
451 (70, 60),
452 (80, 90),
454 (90, 80),
455 ];
456
457 assert_eq!(
458 descendant_pids_from_parent_edges(10, &edges),
459 vec![50, 30, 40, 20]
460 );
461 assert_eq!(descendant_pids_from_parent_edges(99, &edges), vec![70, 60]);
462 assert_eq!(
463 descendant_pids_from_parent_edges(123, &edges),
464 Vec::<u32>::new()
465 );
466 }
467
468 #[cfg(unix)]
469 #[test]
470 fn interrupted_wait_kills_process_group() {
471 let mut command = std::process::Command::new("sh");
473 command.args(["-c", "sleep 30 & wait"]);
474 configure_kill_group(&mut command);
475 let mut child = command.spawn().expect("spawn sh");
476 let pgid = child.id();
477
478 let cancel = Arc::new(AtomicBool::new(true));
479 let _guard = install(Some(cancel), None);
480 let started = Instant::now();
481 let outcome = wait_child_interruptible(&mut child, None).expect("wait");
482 assert!(matches!(outcome, ChildWait::Interrupted(_)));
483 assert!(started.elapsed() < Duration::from_secs(10));
484
485 extern "C" {
487 fn kill(pid: i32, sig: i32) -> i32;
488 }
489 let group_gone = || unsafe { kill(-(pgid as i32), 0) } != 0;
490 let deadline = Instant::now() + Duration::from_secs(5);
491 while !group_gone() && Instant::now() < deadline {
492 std::thread::sleep(Duration::from_millis(50));
493 }
494 assert!(group_gone(), "process group {pgid} survived interrupt");
495 }
496}