command_stream/process_stop.rs
1//! Stopping a [`ProcessRunner`]: an explicit kill, or the
2//! [`RunOptions::timeout`](crate::RunOptions::timeout) deadline (issue #227).
3
4use std::future::Future;
5use std::time::Duration;
6use tokio::time::Instant;
7
8use crate::{signal, utils, ProcessRunner, Result};
9
10impl ProcessRunner {
11 /// Stop the process using the configured kill signal
12 /// ([`RunOptions::kill_signal`](crate::RunOptions::kill_signal), default `SIGTERM`).
13 ///
14 /// Mirrors the JavaScript `kill()` with no argument.
15 pub fn kill(&mut self) -> Result<()> {
16 let signal = self.options.kill_signal.clone();
17 self.kill_with(&signal)
18 }
19
20 /// Stop the process using an explicit signal, overriding
21 /// [`RunOptions::kill_signal`](crate::RunOptions::kill_signal) for this call.
22 ///
23 /// Mirrors the JavaScript `kill(signal)`. The signal is delivered to the
24 /// child and its process group, so grandchildren behind a `sh -c` wrapper
25 /// are stopped too - except for a child sharing the caller's terminal,
26 /// which stays in the caller's group so CTRL+C keeps reaching it. The child
27 /// then has [`RunOptions::kill_grace_ms`](crate::RunOptions::kill_grace_ms) to run its own handler before
28 /// `SIGKILL` follows, so a process that ignores the signal still
29 /// terminates.
30 ///
31 /// ```no_run
32 /// use command_stream::{ProcessRunner, RunOptions};
33 ///
34 /// # #[tokio::main]
35 /// # async fn main() -> command_stream::Result<()> {
36 /// let mut runner = ProcessRunner::new("sleep 30", RunOptions::default());
37 /// runner.start().await?;
38 /// runner.kill_with("SIGINT")?; // the CTRL+C signal
39 /// # Ok(())
40 /// # }
41 /// ```
42 pub fn kill_with(&mut self, signal: &str) -> Result<()> {
43 self.cancelled = true;
44 utils::trace_lazy("ProcessRunner", || format!("kill | signal={signal}"));
45
46 let Some(child) = self.child.as_mut() else {
47 return Ok(());
48 };
49
50 // Windows stops the tree before the parent exits, while its descendants
51 // can still be found. Direct-child termination is the fallback.
52 // The `#[cfg(unix)]` block below is stripped on Windows, which leaves
53 // this one as the function's tail expression - hence no `return`.
54 #[cfg(not(unix))]
55 {
56 if let Some(pid) = child.id() {
57 signal::send_signal_to_process(pid, signal, signal::Delivery::ProcessAndGroup);
58 }
59 child.start_kill()?;
60 utils::trace_lazy("ProcessRunner", || {
61 "Direct-child termination requested".to_string()
62 });
63 Ok(())
64 }
65
66 // Without a pid the process never spawned (or was already reaped);
67 // fall back to the forceful stop so `kill()` still terminates it.
68 #[cfg(unix)]
69 {
70 let Some(pid) = child.id() else {
71 child.start_kill()?;
72 return Ok(());
73 };
74
75 // `SIGKILL` cannot be handled, so there is nothing to wait for.
76 //
77 // A zero grace period means the child is given no opportunity to
78 // handle the signal either, so the requested signal is not
79 // delivered at all. Anything done between it and `SIGKILL` - even a
80 // single syscall - is a window the child can be scheduled in, which
81 // made "no grace" a race the child occasionally won rather than a
82 // guarantee. The reported exit code still comes from the signal
83 // that was requested.
84 let grace = self.options.kill_grace_ms;
85 let delivery = if self.own_process_group {
86 signal::Delivery::ProcessAndGroup
87 } else {
88 signal::Delivery::ProcessOnly
89 };
90 if grace == 0 || signal == "SIGKILL" {
91 signal::send_signal_to_process(pid, "SIGKILL", delivery);
92 let _ = child.start_kill();
93 return Ok(());
94 }
95
96 signal::send_signal_to_process(pid, signal, delivery);
97
98 // Otherwise escalate in the background so the child keeps its grace
99 // period without blocking the caller, which may not be inside an
100 // await point.
101 tokio::spawn(async move {
102 tokio::time::sleep(std::time::Duration::from_millis(grace)).await;
103 // Best effort: if the child already exited on the first signal
104 // this delivery simply fails, and the pid has not been reused
105 // because the `Child` handle above has not reaped it yet. That
106 // unreaped leader is also what keeps the group id alive, so the
107 // group delivery still reaches a grandchild that outlived it.
108 signal::send_signal_to_process(pid, "SIGKILL", delivery);
109 });
110
111 Ok(())
112 }
113 }
114
115 /// Whether the [`RunOptions::timeout`](crate::RunOptions::timeout) elapsed
116 /// and stopped the command.
117 ///
118 /// Mirrors the JavaScript `result.timedOut`. An explicit kill, or a
119 /// command finishing on its own, leaves it `false`.
120 pub fn timed_out(&self) -> bool {
121 self.timed_out
122 }
123
124 /// How a timed-out child is stopped: the configured signal, delivered the
125 /// way [`kill_with`](Self::kill_with) would deliver it.
126 pub(crate) fn stop_plan(&self) -> StopPlan {
127 #[cfg(unix)]
128 let delivery = if self.own_process_group {
129 signal::Delivery::ProcessAndGroup
130 } else {
131 signal::Delivery::ProcessOnly
132 };
133 #[cfg(not(unix))]
134 let delivery = signal::Delivery::ProcessAndGroup;
135 StopPlan {
136 pid: self.pid,
137 signal: self.options.kill_signal.clone(),
138 grace_ms: self.options.kill_grace_ms,
139 delivery,
140 }
141 }
142}
143
144/// How to stop a child whose deadline has passed.
145pub(crate) struct StopPlan {
146 pid: Option<u32>,
147 signal: String,
148 grace_ms: u64,
149 delivery: signal::Delivery,
150}
151
152impl StopPlan {
153 fn send(&self, signal: &str) {
154 if let Some(pid) = self.pid {
155 signal::send_signal_to_process(pid, signal, self.delivery);
156 }
157 }
158}
159
160/// Await `work` - draining and reaping the child - but stop the child if
161/// `deadline` passes first.
162///
163/// Returns the work's output, or `None` when the child and its pipes did not
164/// let go even after `SIGKILL` and the exit-pump grace period, and whether the
165/// deadline fired. The signals are sent while `work` still holds the unreaped
166/// child, so its pid and process group cannot have been reused.
167pub(crate) async fn bounded<T>(
168 work: impl Future<Output = T>,
169 deadline: Option<Instant>,
170 plan: StopPlan,
171) -> (Option<T>, bool) {
172 tokio::pin!(work);
173 let Some(deadline) = deadline else {
174 return (Some(work.await), false);
175 };
176 tokio::select! {
177 output = &mut work => return (Some(output), false),
178 () = tokio::time::sleep_until(deadline) => {}
179 }
180 utils::trace_lazy("ProcessRunner", || {
181 format!(
182 "Timeout elapsed | pid={:?} signal={}",
183 plan.pid, plan.signal
184 )
185 });
186 // As in `kill_with`, no grace means `SIGKILL` alone.
187 if cfg!(unix) && plan.grace_ms > 0 && plan.signal != "SIGKILL" {
188 plan.send(&plan.signal);
189 let grace = Duration::from_millis(plan.grace_ms);
190 if let Ok(output) = tokio::time::timeout(grace, &mut work).await {
191 return (Some(output), true);
192 }
193 }
194 plan.send("SIGKILL");
195 let pump = Duration::from_millis(crate::stream::DEFAULT_EXIT_PUMP_GRACE_MS);
196 (tokio::time::timeout(pump, &mut work).await.ok(), true)
197}