Skip to main content

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}