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}