tokio_process_tools/process_handle/termination/shutdown.rs
1//! Graceful-shutdown policy types passed to [`ProcessHandle::terminate`] and friends.
2//!
3//! See [`GracefulShutdown`] for the cross-platform value, [`UnixGracefulShutdown`] for the Unix
4//! phase model, and [`WindowsGracefulShutdown`] for the Windows single-phase model.
5//!
6//! [`ProcessHandle::terminate`]: crate::ProcessHandle::terminate
7
8#![cfg(any(unix, windows))]
9
10use std::marker::PhantomData;
11use std::time::Duration;
12
13/// Per-platform graceful-shutdown policy passed to [`ProcessHandle::terminate`] and related APIs.
14///
15/// Carries a sequence of one or more graceful-shutdown signals to dispatch before falling back to
16/// the implicit forceful kill. The shape is platform-conditional because the available
17/// graceful-shutdown signals themselves are platform-conditional:
18///
19/// - On Unix it carries a [`UnixGracefulShutdown`] of one or more [`UnixGracefulPhase`]s.
20/// - On Windows it carries a single [`WindowsGracefulShutdown`] timeout for the only available
21/// graceful signal, `CTRL_BREAK_EVENT`.
22///
23/// SIGKILL on Unix and `TerminateProcess` on Windows are always the implicit final fallback;
24/// they are not configurable phases.
25///
26/// # Choosing the Unix sequence
27///
28/// See [`UnixGracefulShutdown`] for the recommended single-signal sequences and a discussion of
29/// why mixing SIGINT and SIGTERM does not cover children with unknown signal handlers.
30///
31/// # Cross-platform construction
32///
33/// Use [`GracefulShutdown::builder`] to write a single cross-platform construction expression.
34/// The setter for the platform that does not match the current target accepts its argument
35/// without using it, so no cfg gates are needed at the call site:
36///
37/// ```rust
38/// use std::time::Duration;
39/// use tokio_process_tools::GracefulShutdown;
40///
41/// // Common case: single-signal sequences via the convenience shortcuts.
42/// let shutdown = GracefulShutdown::builder()
43/// .unix_sigterm(Duration::from_secs(10))
44/// .windows_ctrl_break(Duration::from_secs(10))
45/// .build();
46/// ```
47///
48/// For the rare multiphase case, use the [`GracefulShutdownBuilder::unix`] setter, accepting a
49/// custom-built `UnixGracefulShutdown`:
50///
51/// ```rust
52/// use std::time::Duration;
53/// use tokio_process_tools::{
54/// GracefulShutdown, UnixGracefulPhase, UnixGracefulShutdown,
55/// };
56///
57/// let shutdown = GracefulShutdown::builder()
58/// .unix(UnixGracefulShutdown::from_phases([
59/// UnixGracefulPhase::interrupt(Duration::from_secs(60)),
60/// UnixGracefulPhase::terminate(Duration::from_secs(5)),
61/// ]))
62/// .windows_ctrl_break(Duration::from_secs(10))
63/// .build();
64/// ```
65///
66/// # Platform availability
67///
68/// This type is only available on Unix and Windows because the underlying graceful-shutdown
69/// signals only exist there. On other Tokio-supported targets the spawn, wait, output-collection,
70/// and [`ProcessHandle::kill`] APIs remain available; only the graceful-termination surface
71/// (`terminate(...)`, `terminate_on_drop(...)`, `wait_for_completion_or_terminate(...)`, the
72/// `send_*_signal(...)` methods, and this type) is gated out.
73///
74/// [`ProcessHandle::terminate`]: super::ProcessHandle::terminate
75/// [`ProcessHandle::kill`]: super::ProcessHandle::kill
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct GracefulShutdown {
78 /// Unix graceful-shutdown phase sequence.
79 #[cfg(unix)]
80 pub unix: UnixGracefulShutdown,
81 /// Windows graceful-shutdown timeout for the single `CTRL_BREAK_EVENT` phase.
82 #[cfg(windows)]
83 pub windows: WindowsGracefulShutdown,
84}
85
86impl GracefulShutdown {
87 /// Start a fluent specification of a `GracefulShutdown` value.
88 ///
89 /// Call [`unix`](GracefulShutdownBuilder::unix), then
90 /// [`windows`](GracefulShutdownBuilder::windows), then
91 /// [`build`](GracefulShutdownBuilder::build). The setter for the platform that does not
92 /// match the current target accepts its argument without using it, which lets cross-platform
93 /// code construct the value without cfg gates.
94 #[must_use]
95 pub fn builder() -> GracefulShutdownBuilder<UnixUnset> {
96 GracefulShutdownBuilder {
97 #[cfg(unix)]
98 unix: None,
99 #[cfg(windows)]
100 windows: WindowsGracefulShutdown {
101 timeout: Duration::ZERO,
102 },
103 _state: PhantomData,
104 }
105 }
106}
107
108/// Typestate marker indicating that the Unix-side sequence has not been provided yet.
109#[doc(hidden)]
110#[derive(Debug, Clone, Copy)]
111pub struct UnixUnset;
112
113/// Typestate marker indicating that the Unix-side sequence has been provided but the
114/// Windows-side budget has not.
115#[doc(hidden)]
116#[derive(Debug, Clone, Copy)]
117pub struct UnixSet;
118
119/// Typestate marker indicating that both the Unix-side sequence and the Windows-side budget have
120/// been provided. A builder in this state can be finished with
121/// [`build`](GracefulShutdownBuilder::build).
122#[doc(hidden)]
123#[derive(Debug, Clone, Copy)]
124pub struct BothSet;
125
126/// Typestate builder for [`GracefulShutdown`]. Created via [`GracefulShutdown::builder`].
127///
128/// Both [`unix`](Self::unix) and [`windows`](Self::windows) must be called (in that order)
129/// before [`build`](Self::build) becomes available. The setter for the platform that does not
130/// match the current target accepts its argument without using it, so cross-platform code can
131/// build a value without cfg gates.
132#[derive(Debug, Clone)]
133pub struct GracefulShutdownBuilder<State> {
134 #[cfg(unix)]
135 unix: Option<UnixGracefulShutdown>,
136 #[cfg(windows)]
137 windows: WindowsGracefulShutdown,
138 _state: PhantomData<fn() -> State>,
139}
140
141impl GracefulShutdownBuilder<UnixUnset> {
142 /// Set the Unix-side sequence to an explicit [`UnixGracefulShutdown`].
143 ///
144 /// Use this for the rare multi-phase case where you control the child and need a
145 /// cooperative protocol that escalates through more than one signal. For the common
146 /// single-signal cases prefer [`Self::unix_sigterm`] or [`Self::unix_sigint`], which
147 /// take a `Duration` directly.
148 ///
149 /// On non-Unix targets the value is accepted but unused.
150 #[must_use]
151 #[cfg_attr(not(unix), allow(clippy::needless_pass_by_value))]
152 pub fn unix(self, sequence: UnixGracefulShutdown) -> GracefulShutdownBuilder<UnixSet> {
153 #[cfg(not(unix))]
154 let _ = sequence;
155 GracefulShutdownBuilder {
156 #[cfg(unix)]
157 unix: Some(sequence),
158 #[cfg(windows)]
159 windows: self.windows,
160 _state: PhantomData,
161 }
162 }
163
164 /// Shorthand for `.unix(UnixGracefulShutdown::terminate_only(timeout))`. The recommended
165 /// default for service-like children. See [`UnixGracefulShutdown`] for the choice between
166 /// `terminate_only`, `interrupt_only`, and `from_phases`.
167 ///
168 /// On non-Unix targets the value is accepted but unused.
169 #[must_use]
170 pub fn unix_sigterm(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
171 #[cfg(not(unix))]
172 let _ = timeout;
173 GracefulShutdownBuilder {
174 #[cfg(unix)]
175 unix: Some(UnixGracefulShutdown::terminate_only(timeout)),
176 #[cfg(windows)]
177 windows: self.windows,
178 _state: PhantomData,
179 }
180 }
181
182 /// Shorthand for `.unix(UnixGracefulShutdown::interrupt_only(timeout))`. Use this when
183 /// forwarding a Ctrl-C / TTY interrupt to a CLI-like child. See [`UnixGracefulShutdown`]
184 /// for the choice between `terminate_only`, `interrupt_only`, and `from_phases`.
185 ///
186 /// On non-Unix targets the value is accepted but unused.
187 #[must_use]
188 pub fn unix_sigint(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
189 #[cfg(not(unix))]
190 let _ = timeout;
191 GracefulShutdownBuilder {
192 #[cfg(unix)]
193 unix: Some(UnixGracefulShutdown::interrupt_only(timeout)),
194 #[cfg(windows)]
195 windows: self.windows,
196 _state: PhantomData,
197 }
198 }
199}
200
201impl GracefulShutdownBuilder<UnixSet> {
202 /// Set the Windows-side sequence to an explicit [`WindowsGracefulShutdown`].
203 ///
204 /// For the common case prefer [`Self::windows_ctrl_break`], which takes a `Duration`
205 /// directly. Windows currently has only one graceful signal (`CTRL_BREAK_EVENT`), so
206 /// the structured form is rarely needed; it is kept for symmetry with the Unix side and
207 /// to leave room for future Windows-specific knobs.
208 ///
209 /// On non-Windows targets the value is accepted but unused.
210 #[must_use]
211 #[cfg_attr(not(windows), allow(clippy::needless_pass_by_value))]
212 pub fn windows(self, sequence: WindowsGracefulShutdown) -> GracefulShutdownBuilder<BothSet> {
213 #[cfg(not(windows))]
214 let _ = sequence;
215 GracefulShutdownBuilder {
216 #[cfg(unix)]
217 unix: self.unix,
218 #[cfg(windows)]
219 windows: sequence,
220 _state: PhantomData,
221 }
222 }
223
224 /// Shorthand for `.windows(WindowsGracefulShutdown::new(timeout))`.
225 ///
226 /// `timeout` bounds the post-`CTRL_BREAK_EVENT` wait before escalating to
227 /// `TerminateProcess`.
228 ///
229 /// On non-Windows targets the value is accepted but unused.
230 #[must_use]
231 pub fn windows_ctrl_break(self, timeout: Duration) -> GracefulShutdownBuilder<BothSet> {
232 #[cfg(not(windows))]
233 let _ = timeout;
234 GracefulShutdownBuilder {
235 #[cfg(unix)]
236 unix: self.unix,
237 #[cfg(windows)]
238 windows: WindowsGracefulShutdown::new(timeout),
239 _state: PhantomData,
240 }
241 }
242}
243
244impl GracefulShutdownBuilder<BothSet> {
245 /// Finish the builder, producing a [`GracefulShutdown`].
246 ///
247 /// # Panics
248 ///
249 /// Should never panic: the builder's typestate requires [`Self::unix`] to have been called
250 /// before this method becomes available.
251 #[must_use]
252 pub fn build(self) -> GracefulShutdown {
253 GracefulShutdown {
254 #[cfg(unix)]
255 unix: self
256 .unix
257 .expect("UnixGracefulShutdown must be set before build()"),
258 #[cfg(windows)]
259 windows: self.windows,
260 }
261 }
262}
263
264/// Graceful-shutdown signal that can be the first or escalation step on Unix before the implicit
265/// SIGKILL fallback.
266///
267/// Marked `#[non_exhaustive]` so additional Unix signals (e.g. `SIGHUP`, `SIGUSR1`) can be added
268/// in future minor releases without forcing a breaking change on `match` users.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270#[non_exhaustive]
271pub enum UnixGracefulSignal {
272 /// `SIGINT`. Default disposition is `Term` (kernel terminates the process when no handler is
273 /// installed). Idiomatic Tokio applications using `tokio::signal::ctrl_c()` install a SIGINT
274 /// handler, but service-style children (systemd, K8s, Docker) typically install only a
275 /// SIGTERM handler, in which case the kernel default disposition kills the child without
276 /// running the user's shutdown logic.
277 Interrupt,
278
279 /// `SIGTERM`. Default disposition is `Term` (kernel terminates the process when no handler is
280 /// installed). The conventional "please shut down" signal in Unix; what `kill <pid>` (no args)
281 /// sends and what systemd, K8s, Docker, runit, and supervisord all use.
282 Terminate,
283}
284
285#[cfg(unix)]
286impl UnixGracefulSignal {
287 pub(crate) fn label(self) -> &'static str {
288 match self {
289 Self::Interrupt => "SIGINT",
290 Self::Terminate => "SIGTERM",
291 }
292 }
293}
294
295/// One step of a Unix graceful-shutdown sequence: a signal to send and a maximum time to wait
296/// for the child to exit before escalating.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub struct UnixGracefulPhase {
299 /// Signal sent at the start of this phase.
300 pub signal: UnixGracefulSignal,
301
302 /// Maximum time to wait for the child to exit after `signal` is sent. When this elapses the
303 /// sequence escalates to the next phase, or to the implicit SIGKILL fallback if this is the
304 /// last phase.
305 pub timeout: Duration,
306}
307
308impl UnixGracefulPhase {
309 /// Convenience constructor for a `SIGINT` phase.
310 #[must_use]
311 pub const fn interrupt(timeout: Duration) -> Self {
312 Self {
313 signal: UnixGracefulSignal::Interrupt,
314 timeout,
315 }
316 }
317
318 /// Convenience constructor for a `SIGTERM` phase.
319 #[must_use]
320 pub const fn terminate(timeout: Duration) -> Self {
321 Self {
322 signal: UnixGracefulSignal::Terminate,
323 timeout,
324 }
325 }
326}
327
328/// One or more graceful-shutdown phases dispatched on Unix before the implicit SIGKILL fallback.
329///
330/// Always followed by `SIGKILL` if every configured phase elapses without the child exiting.
331///
332/// # Choosing a sequence
333///
334/// **Recommended for service-like children (default for most orchestrators):**
335/// [`UnixGracefulShutdown::terminate_only`]. `SIGTERM` is the standard Unix shutdown signal:
336/// `kill <pid>`, systemd, K8s, Docker, runit, and supervisord all send it. Most well-behaved
337/// long-running services install a SIGTERM handler.
338///
339/// **Recommended for CLI-like children:** [`UnixGracefulShutdown::interrupt_only`]. Use when the
340/// orchestrator's user model is "I pressed Ctrl-C" and the child is expected to handle `SIGINT`
341/// (Tokio `tokio::signal::ctrl_c()`, Python `KeyboardInterrupt`, and similar).
342///
343/// **Multi-phase sequences via [`from_phases`](Self::from_phases) are NOT a way to cover children
344/// with unknown signal handlers.** If phase 1 sends a signal whose handler is missing in the
345/// child, the kernel default disposition (`Term`) kills the child during phase 1 and later phases
346/// are never dispatched. A two-phase `SIGINT` -> `SIGTERM` sequence does not "cover both
347/// conventions": a child that handles only `SIGTERM` is killed by the `SIGINT` phase via kernel
348/// default before its `SIGTERM` handler can run, and the symmetric problem occurs for
349/// `SIGTERM` -> `SIGINT` against `SIGINT`-only-handler children.
350///
351/// Use multi-phase only when you control the child and use multiple signals as distinct
352/// cooperative shutdown stages (for example: `SIGINT` "begin drain" followed by `SIGTERM` "abort
353/// drain"). For unknown or heterogeneous children, pick `terminate_only` or `interrupt_only`.
354///
355/// # Construction
356///
357/// `UnixGracefulShutdown` cannot be constructed empty. The implicit `SIGKILL` fallback is not a
358/// phase; a graceful sequence must contain at least one signal to dispatch. Use one of the named
359/// single-signal constructors, or [`from_phases`](Self::from_phases) for a multi-phase
360/// cooperative sequence.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct UnixGracefulShutdown {
363 phases: Vec<UnixGracefulPhase>,
364}
365
366impl UnixGracefulShutdown {
367 /// Single-phase sequence sending `SIGTERM` only. The recommended default for service-like
368 /// children; see the [type-level docs](Self#choosing-a-sequence) for the full discussion.
369 /// The implicit `SIGKILL` fallback runs after `timeout` if the child has not exited.
370 #[must_use]
371 pub fn terminate_only(timeout: Duration) -> Self {
372 Self::single(UnixGracefulPhase::terminate(timeout))
373 }
374
375 /// Single-phase sequence sending `SIGINT` only. Use this when forwarding a Ctrl-C / TTY
376 /// interrupt to a CLI-like child; see the [type-level docs](Self#choosing-a-sequence) for
377 /// the full discussion. The implicit `SIGKILL` fallback runs after `timeout` if the child
378 /// has not exited.
379 #[must_use]
380 pub fn interrupt_only(timeout: Duration) -> Self {
381 Self::single(UnixGracefulPhase::interrupt(timeout))
382 }
383
384 /// Single-phase sequence sending an arbitrary [`UnixGracefulSignal`].
385 #[must_use]
386 pub(crate) fn single(phase: UnixGracefulPhase) -> Self {
387 Self {
388 phases: vec![phase],
389 }
390 }
391
392 /// Multi-phase sequence dispatched in iteration order. Use only for cooperative
393 /// shutdown protocols against a child you control, where each signal in the sequence has
394 /// a distinct handler. For unknown children pick [`Self::terminate_only`] or
395 /// [`Self::interrupt_only`] instead; see the [type-level docs](Self#choosing-a-sequence)
396 /// for why a multi-phase sequence does not "cover both conventions".
397 ///
398 /// # Panics
399 ///
400 /// Panics if `phases` produces no elements. A graceful sequence must contain at least one
401 /// phase; the implicit `SIGKILL` fallback is not a phase.
402 #[must_use]
403 pub fn from_phases(phases: impl IntoIterator<Item = UnixGracefulPhase>) -> Self {
404 let phases: Vec<_> = phases.into_iter().collect();
405 assert!(
406 !phases.is_empty(),
407 "UnixGracefulShutdown must contain at least one phase",
408 );
409 Self { phases }
410 }
411
412 /// Phases in dispatch order.
413 #[must_use]
414 pub fn phases(&self) -> &[UnixGracefulPhase] {
415 &self.phases
416 }
417}
418
419/// Single-phase Windows graceful-shutdown sequence.
420///
421/// `CTRL_BREAK_EVENT` is dispatched to the child's console process group, then `TerminateProcess`
422/// runs as the kill fallback if the child has not exited within `timeout`. Windows has
423/// no second graceful signal: `GenerateConsoleCtrlEvent` accepts only `CTRL_BREAK_EVENT` for
424/// nonzero process groups, so a second graceful phase would just duplicate the first send.
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub struct WindowsGracefulShutdown {
427 /// Maximum time to wait after sending `CTRL_BREAK_EVENT` before escalating to
428 /// `TerminateProcess`.
429 pub timeout: Duration,
430}
431
432impl WindowsGracefulShutdown {
433 /// Construct a Windows graceful sequence with the given timeout for the single
434 /// `CTRL_BREAK_EVENT` phase.
435 #[must_use]
436 pub const fn new(timeout: Duration) -> Self {
437 Self { timeout }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use assertr::prelude::*;
445
446 mod builder {
447 use super::*;
448
449 #[test]
450 fn populates_unix_terminate_only_phase() {
451 let shutdown = GracefulShutdown::builder()
452 .unix(UnixGracefulShutdown::terminate_only(Duration::from_secs(5)))
453 .windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
454 .build();
455
456 #[cfg(unix)]
457 {
458 let phases = shutdown.unix.phases();
459 assert_that!(phases.len()).is_equal_to(1);
460 assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(5));
461 }
462 #[cfg(windows)]
463 {
464 assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
465 }
466 }
467
468 #[test]
469 fn uses_interrupt_signal_for_unix_interrupt_only() {
470 let shutdown = GracefulShutdown::builder()
471 .unix(UnixGracefulShutdown::interrupt_only(Duration::from_secs(3)))
472 .windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
473 .build();
474
475 #[cfg(unix)]
476 {
477 let phases = shutdown.unix.phases();
478 assert_that!(phases.len()).is_equal_to(1);
479 assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(3));
480 }
481 #[cfg(windows)]
482 {
483 assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
484 }
485 }
486 }
487
488 #[cfg(unix)]
489 mod unix_sequence {
490 use super::*;
491 use crate::{UnixGracefulPhase, UnixGracefulSignal};
492
493 #[test]
494 fn from_phases_preserves_iteration_order() {
495 let sequence = UnixGracefulShutdown::from_phases([
496 UnixGracefulPhase::interrupt(Duration::from_secs(1)),
497 UnixGracefulPhase::terminate(Duration::from_secs(2)),
498 ]);
499
500 let phases = sequence.phases();
501 assert_that!(phases.len()).is_equal_to(2);
502 assert_that!(phases[0].signal).is_equal_to(UnixGracefulSignal::Interrupt);
503 assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(1));
504 assert_that!(phases[1].signal).is_equal_to(UnixGracefulSignal::Terminate);
505 assert_that!(phases[1].timeout).is_equal_to(Duration::from_secs(2));
506 }
507
508 #[test]
509 #[should_panic(expected = "UnixGracefulShutdown must contain at least one phase")]
510 fn from_phases_panics_on_empty_input() {
511 let _ = UnixGracefulShutdown::from_phases(std::iter::empty());
512 }
513 }
514}