Skip to main content

tokio_process_tools/process_handle/
signal.rs

1//! Manual signal delivery: user-callable shortcuts for sending a single platform signal to the
2//! child's process group.
3//!
4//! These helpers sit alongside the termination flow (`process_handle::termination`) but are not
5//! part of it. They preflight-reap an already-exited child, send the signal, and then re-probe on
6//! signal-send failure so a freshly-exited child is observed as exited rather than as still
7//! running. They are synchronous on purpose to keep the public `send_*_signal` API non-`async`.
8
9use super::ProcessHandle;
10use super::termination::TerminationDiagnostics;
11use crate::error::{TerminationAction, TerminationError};
12use crate::output_stream::OutputStream;
13use std::io;
14use std::process::ExitStatus;
15
16#[cfg(any(unix, windows))]
17impl<Stdout, Stderr> ProcessHandle<Stdout, Stderr>
18where
19    Stdout: OutputStream,
20    Stderr: OutputStream,
21{
22    /// Manually send `SIGINT` to this process's process group via `killpg`.
23    ///
24    /// `SIGINT` is the dedicated user-interrupt signal, distinct from the `SIGTERM` delivered by
25    /// [`Self::send_terminate_signal`]. The signal targets the child's process group, so any
26    /// grandchildren the child has fork-execed are signaled together with the leader.
27    ///
28    /// If the process has already exited, this reaps it and returns `Ok(())` instead of
29    /// attempting to signal a stale PID or process group. If the signal send fails because the
30    /// child exited after the preflight check, this also reaps it and returns `Ok(())`.
31    ///
32    /// Prefer to call `terminate` instead, if you want to make sure this process is terminated.
33    ///
34    /// This method is Unix-only because Windows has no targetable `SIGINT` analogue:
35    /// `GenerateConsoleCtrlEvent` only accepts `CTRL_BREAK_EVENT` for nonzero process groups.
36    /// On Windows, use `send_ctrl_break_signal` instead.
37    ///
38    /// # Notes
39    ///
40    /// The post-failure exit-status probe is a single synchronous `try_wait`. On the rare race
41    /// where the child has just exited but Tokio's SIGCHLD reaper has not yet observed it, the
42    /// probe returns "still running" and this method surfaces the OS-level signal-send error
43    /// (typically `EPERM` on macOS or `ESRCH` on Linux) as
44    /// [`TerminationError::SignalFailed`]. The synchronous shape is deliberate so the public
45    /// `send_*_signal` API stays non-async; reach for [`Self::terminate`] when you need the
46    /// bounded SIGCHLD-grace wait.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`TerminationError`] if the process status could not be checked or if `SIGINT`
51    /// could not be sent.
52    #[cfg(unix)]
53    pub fn send_interrupt_signal(&mut self) -> Result<(), TerminationError> {
54        self.send_signal_with_reaper(
55            "SIGINT",
56            |this| this.group.send_interrupt(),
57            Self::try_reap_exit_status,
58        )
59    }
60
61    /// Manually send `SIGTERM` to this process's process group via `killpg`.
62    ///
63    /// `SIGTERM` is the conventional "asked to terminate" signal sent by service supervisors and
64    /// the operating system at shutdown. The signal targets the child's process group, so any
65    /// grandchildren the child has fork-execed are signaled together with the leader.
66    ///
67    /// If the process has already exited, this reaps it and returns `Ok(())` instead of
68    /// attempting to signal a stale PID or process group. If the signal send fails because the
69    /// child exited after the preflight check, this also reaps it and returns `Ok(())`.
70    ///
71    /// Prefer to call `terminate` instead, if you want to make sure this process is terminated.
72    ///
73    /// This method is Unix-only because Windows has no targetable `SIGTERM` analogue:
74    /// `GenerateConsoleCtrlEvent` only accepts `CTRL_BREAK_EVENT` for nonzero process groups.
75    /// On Windows, use `send_ctrl_break_signal` instead.
76    ///
77    /// # Notes
78    ///
79    /// The post-failure exit-status probe is a single synchronous `try_wait`. On the rare race
80    /// where the child has just exited but Tokio's SIGCHLD reaper has not yet observed it, the
81    /// probe returns "still running" and this method surfaces the OS-level signal-send error
82    /// (typically `EPERM` on macOS or `ESRCH` on Linux) as
83    /// [`TerminationError::SignalFailed`]. The synchronous shape is deliberate so the public
84    /// `send_*_signal` API stays non-async; reach for [`Self::terminate`] when you need the
85    /// bounded SIGCHLD-grace wait.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`TerminationError`] if the process status could not be checked or if `SIGTERM`
90    /// could not be sent.
91    #[cfg(unix)]
92    pub fn send_terminate_signal(&mut self) -> Result<(), TerminationError> {
93        self.send_signal_with_reaper(
94            "SIGTERM",
95            |this| this.group.send_terminate(),
96            Self::try_reap_exit_status,
97        )
98    }
99
100    /// Manually deliver `CTRL_BREAK_EVENT` to this process's console process group via
101    /// `GenerateConsoleCtrlEvent`.
102    ///
103    /// `CTRL_BREAK_EVENT` is the only console control event that can be targeted at a nonzero
104    /// process group: `CTRL_C_EVENT` requires `dwProcessGroupId = 0` and would be broadcast to
105    /// every process sharing the calling console (including the parent), so it is not usable to
106    /// terminate a single child group. There is therefore no separate `SIGINT` vs. `SIGTERM`
107    /// distinction on Windows; this single method covers the entire graceful-shutdown surface.
108    ///
109    /// If the process has already exited, this reaps it and returns `Ok(())` instead of
110    /// attempting to signal a stale PID or process group. If the signal send fails because the
111    /// child exited after the preflight check, this also reaps it and returns `Ok(())`.
112    ///
113    /// Prefer to call `terminate` instead, if you want to make sure this process is terminated.
114    ///
115    /// This method is Windows-only. On Unix, use `send_interrupt_signal` or
116    /// `send_terminate_signal` instead.
117    ///
118    /// # Notes
119    ///
120    /// The post-failure exit-status probe is a single synchronous `try_wait`. On the rare race
121    /// where the child has just exited but Tokio has not yet observed it, the probe returns
122    /// "still running" and this method surfaces the OS-level
123    /// `GenerateConsoleCtrlEvent` failure as [`TerminationError::SignalFailed`]. The synchronous
124    /// shape is deliberate so the public `send_*_signal` API stays non-async; reach for
125    /// [`Self::terminate`] when you need the bounded reaper-grace wait.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`TerminationError`] if the process status could not be checked or if
130    /// `CTRL_BREAK_EVENT` could not be delivered.
131    #[cfg(windows)]
132    pub fn send_ctrl_break_signal(&mut self) -> Result<(), TerminationError> {
133        self.send_signal_with_reaper(
134            "CTRL_BREAK_EVENT",
135            |this| this.group.send_ctrl_break(),
136            Self::try_reap_exit_status,
137        )
138    }
139
140    /// Test-only fault-injection seam underneath `send_*_signal`.
141    ///
142    /// Drives a single signal-send with caller-supplied hooks for the signal send and the
143    /// preflight/post-failure exit-status poll. Production code should call
144    /// [`send_interrupt_signal`](Self::send_interrupt_signal),
145    /// [`send_terminate_signal`](Self::send_terminate_signal), or
146    /// [`send_ctrl_break_signal`](Self::send_ctrl_break_signal) instead.
147    #[doc(hidden)]
148    pub fn send_signal_with_reaper<SignalSender, Reaper>(
149        &mut self,
150        signal_name: &'static str,
151        send_signal: SignalSender,
152        mut try_reap_exit_status: Reaper,
153    ) -> Result<(), TerminationError>
154    where
155        SignalSender: FnOnce(&mut Self) -> Result<(), io::Error>,
156        Reaper: FnMut(&mut Self) -> Result<Option<ExitStatus>, io::Error>,
157    {
158        let mut diagnostics = TerminationDiagnostics::default();
159
160        match try_reap_exit_status(self) {
161            Ok(Some(_)) => {
162                self.must_not_be_terminated();
163                Ok(())
164            }
165            Ok(None) => match send_signal(self) {
166                Ok(()) => Ok(()),
167                // Sync probe only - the SIGCHLD-grace bounded wait lives on the `terminate()`
168                // path. Keeping this sync avoids making the public `send_*_signal` APIs async.
169                Err(signal_error) => match try_reap_exit_status(self) {
170                    Ok(Some(_)) => {
171                        self.must_not_be_terminated();
172                        Ok(())
173                    }
174                    Ok(None) => {
175                        diagnostics
176                            .record(TerminationAction::SendSignal { signal_name }, signal_error);
177                        Err(diagnostics.into_signal_failed(self.name.clone()))
178                    }
179                    Err(reap_error) => {
180                        diagnostics
181                            .record(TerminationAction::SendSignal { signal_name }, signal_error);
182                        diagnostics.record(TerminationAction::CheckStatus, reap_error);
183                        Err(diagnostics.into_signal_failed(self.name.clone()))
184                    }
185                },
186            },
187            Err(status_error) => {
188                diagnostics.record(TerminationAction::CheckStatus, status_error);
189                Err(diagnostics.into_signal_failed(self.name.clone()))
190            }
191        }
192    }
193}