Skip to main content

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    /// Upper bound on wall-clock time spent in graceful phases before the implicit forceful kill.
108    ///
109    /// On Unix this is the sum of every [`UnixGracefulPhase`]'s timeout in the configured sequence.
110    /// On Windows it is the single [`WindowsGracefulShutdown::timeout`].
111    #[must_use]
112    pub fn total_timeout(&self) -> Duration {
113        #[cfg(unix)]
114        {
115            self.unix.total_timeout()
116        }
117        #[cfg(windows)]
118        {
119            self.windows.timeout
120        }
121    }
122}
123
124/// Typestate marker indicating that the Unix-side sequence has not been provided yet.
125#[doc(hidden)]
126#[derive(Debug, Clone, Copy)]
127pub struct UnixUnset;
128
129/// Typestate marker indicating that the Unix-side sequence has been provided but the
130/// Windows-side budget has not.
131#[doc(hidden)]
132#[derive(Debug, Clone, Copy)]
133pub struct UnixSet;
134
135/// Typestate marker indicating that both the Unix-side sequence and the Windows-side budget have
136/// been provided. A builder in this state can be finished with
137/// [`build`](GracefulShutdownBuilder::build).
138#[doc(hidden)]
139#[derive(Debug, Clone, Copy)]
140pub struct BothSet;
141
142/// Typestate builder for [`GracefulShutdown`]. Created via [`GracefulShutdown::builder`].
143///
144/// Both [`unix`](Self::unix) and [`windows`](Self::windows) must be called (in that order)
145/// before [`build`](Self::build) becomes available. The setter for the platform that does not
146/// match the current target accepts its argument without using it, so cross-platform code can
147/// build a value without cfg gates.
148#[derive(Debug, Clone)]
149pub struct GracefulShutdownBuilder<State> {
150    #[cfg(unix)]
151    unix: Option<UnixGracefulShutdown>,
152    #[cfg(windows)]
153    windows: WindowsGracefulShutdown,
154    _state: PhantomData<fn() -> State>,
155}
156
157impl GracefulShutdownBuilder<UnixUnset> {
158    /// Set the Unix-side sequence to an explicit [`UnixGracefulShutdown`].
159    ///
160    /// Use this for the rare multi-phase case where you control the child and need a
161    /// cooperative protocol that escalates through more than one signal. For the common
162    /// single-signal cases prefer [`Self::unix_sigterm`] or [`Self::unix_sigint`], which
163    /// take a `Duration` directly.
164    ///
165    /// On non-Unix targets the value is accepted but unused.
166    #[must_use]
167    #[cfg_attr(not(unix), allow(clippy::needless_pass_by_value))]
168    pub fn unix(self, sequence: UnixGracefulShutdown) -> GracefulShutdownBuilder<UnixSet> {
169        #[cfg(not(unix))]
170        let _ = sequence;
171        GracefulShutdownBuilder {
172            #[cfg(unix)]
173            unix: Some(sequence),
174            #[cfg(windows)]
175            windows: self.windows,
176            _state: PhantomData,
177        }
178    }
179
180    /// Shorthand for `.unix(UnixGracefulShutdown::terminate_only(timeout))`. The recommended
181    /// default for service-like children. See [`UnixGracefulShutdown`] for the choice between
182    /// `terminate_only`, `interrupt_only`, and `from_phases`.
183    ///
184    /// On non-Unix targets the value is accepted but unused.
185    #[must_use]
186    pub fn unix_sigterm(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
187        #[cfg(not(unix))]
188        let _ = timeout;
189        GracefulShutdownBuilder {
190            #[cfg(unix)]
191            unix: Some(UnixGracefulShutdown::terminate_only(timeout)),
192            #[cfg(windows)]
193            windows: self.windows,
194            _state: PhantomData,
195        }
196    }
197
198    /// Shorthand for `.unix(UnixGracefulShutdown::interrupt_only(timeout))`. Use this when
199    /// forwarding a Ctrl-C / TTY interrupt to a CLI-like child. See [`UnixGracefulShutdown`]
200    /// for the choice between `terminate_only`, `interrupt_only`, and `from_phases`.
201    ///
202    /// On non-Unix targets the value is accepted but unused.
203    #[must_use]
204    pub fn unix_sigint(self, timeout: Duration) -> GracefulShutdownBuilder<UnixSet> {
205        #[cfg(not(unix))]
206        let _ = timeout;
207        GracefulShutdownBuilder {
208            #[cfg(unix)]
209            unix: Some(UnixGracefulShutdown::interrupt_only(timeout)),
210            #[cfg(windows)]
211            windows: self.windows,
212            _state: PhantomData,
213        }
214    }
215}
216
217impl GracefulShutdownBuilder<UnixSet> {
218    /// Set the Windows-side sequence to an explicit [`WindowsGracefulShutdown`].
219    ///
220    /// For the common case prefer [`Self::windows_ctrl_break`], which takes a `Duration`
221    /// directly. Windows currently has only one graceful signal (`CTRL_BREAK_EVENT`), so
222    /// the structured form is rarely needed; it is kept for symmetry with the Unix side and
223    /// to leave room for future Windows-specific knobs.
224    ///
225    /// On non-Windows targets the value is accepted but unused.
226    #[must_use]
227    #[cfg_attr(not(windows), allow(clippy::needless_pass_by_value))]
228    pub fn windows(self, sequence: WindowsGracefulShutdown) -> GracefulShutdownBuilder<BothSet> {
229        #[cfg(not(windows))]
230        let _ = sequence;
231        GracefulShutdownBuilder {
232            #[cfg(unix)]
233            unix: self.unix,
234            #[cfg(windows)]
235            windows: sequence,
236            _state: PhantomData,
237        }
238    }
239
240    /// Shorthand for `.windows(WindowsGracefulShutdown::new(timeout))`.
241    ///
242    /// `timeout` bounds the post-`CTRL_BREAK_EVENT` wait before escalating to
243    /// `TerminateProcess`.
244    ///
245    /// On non-Windows targets the value is accepted but unused.
246    #[must_use]
247    pub fn windows_ctrl_break(self, timeout: Duration) -> GracefulShutdownBuilder<BothSet> {
248        #[cfg(not(windows))]
249        let _ = timeout;
250        GracefulShutdownBuilder {
251            #[cfg(unix)]
252            unix: self.unix,
253            #[cfg(windows)]
254            windows: WindowsGracefulShutdown::new(timeout),
255            _state: PhantomData,
256        }
257    }
258}
259
260impl GracefulShutdownBuilder<BothSet> {
261    /// Finish the builder, producing a [`GracefulShutdown`].
262    ///
263    /// # Panics
264    ///
265    /// Should never panic: the builder's typestate requires [`Self::unix`] to have been called
266    /// before this method becomes available.
267    #[must_use]
268    pub fn build(self) -> GracefulShutdown {
269        GracefulShutdown {
270            #[cfg(unix)]
271            unix: self
272                .unix
273                .expect("UnixGracefulShutdown must be set before build()"),
274            #[cfg(windows)]
275            windows: self.windows,
276        }
277    }
278}
279
280/// Graceful-shutdown signal that can be the first or escalation step on Unix before the implicit
281/// SIGKILL fallback.
282///
283/// Marked `#[non_exhaustive]` so additional Unix signals (e.g. `SIGHUP`, `SIGUSR1`) can be added
284/// in future minor releases without forcing a breaking change on `match` users.
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286#[non_exhaustive]
287pub enum UnixGracefulSignal {
288    /// `SIGINT`. Default disposition is `Term` (kernel terminates the process when no handler is
289    /// installed). Idiomatic Tokio applications using `tokio::signal::ctrl_c()` install a SIGINT
290    /// handler, but service-style children (systemd, K8s, Docker) typically install only a
291    /// SIGTERM handler, in which case the kernel default disposition kills the child without
292    /// running the user's shutdown logic.
293    Interrupt,
294
295    /// `SIGTERM`. Default disposition is `Term` (kernel terminates the process when no handler is
296    /// installed). The conventional "please shut down" signal in Unix; what `kill <pid>` (no args)
297    /// sends and what systemd, K8s, Docker, runit, and supervisord all use.
298    Terminate,
299}
300
301#[cfg(unix)]
302impl UnixGracefulSignal {
303    pub(crate) fn label(self) -> &'static str {
304        match self {
305            Self::Interrupt => "SIGINT",
306            Self::Terminate => "SIGTERM",
307        }
308    }
309}
310
311/// One step of a Unix graceful-shutdown sequence: a signal to send and a maximum time to wait
312/// for the child to exit before escalating.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub struct UnixGracefulPhase {
315    /// Signal sent at the start of this phase.
316    pub signal: UnixGracefulSignal,
317
318    /// Maximum time to wait for the child to exit after `signal` is sent. When this elapses the
319    /// sequence escalates to the next phase, or to the implicit SIGKILL fallback if this is the
320    /// last phase.
321    pub timeout: Duration,
322}
323
324impl UnixGracefulPhase {
325    /// Convenience constructor for a `SIGINT` phase.
326    #[must_use]
327    pub const fn interrupt(timeout: Duration) -> Self {
328        Self {
329            signal: UnixGracefulSignal::Interrupt,
330            timeout,
331        }
332    }
333
334    /// Convenience constructor for a `SIGTERM` phase.
335    #[must_use]
336    pub const fn terminate(timeout: Duration) -> Self {
337        Self {
338            signal: UnixGracefulSignal::Terminate,
339            timeout,
340        }
341    }
342}
343
344/// One or more graceful-shutdown phases dispatched on Unix before the implicit SIGKILL fallback.
345///
346/// Always followed by `SIGKILL` if every configured phase elapses without the child exiting.
347///
348/// # Choosing a sequence
349///
350/// **Recommended for service-like children (default for most orchestrators):**
351/// [`UnixGracefulShutdown::terminate_only`]. `SIGTERM` is the standard Unix shutdown signal:
352/// `kill <pid>`, systemd, K8s, Docker, runit, and supervisord all send it. Most well-behaved
353/// long-running services install a SIGTERM handler.
354///
355/// **Recommended for CLI-like children:** [`UnixGracefulShutdown::interrupt_only`]. Use when the
356/// orchestrator's user model is "I pressed Ctrl-C" and the child is expected to handle `SIGINT`
357/// (Tokio `tokio::signal::ctrl_c()`, Python `KeyboardInterrupt`, and similar).
358///
359/// **Multi-phase sequences via [`from_phases`](Self::from_phases) are NOT a way to cover children
360/// with unknown signal handlers.** If phase 1 sends a signal whose handler is missing in the
361/// child, the kernel default disposition (`Term`) kills the child during phase 1 and later phases
362/// are never dispatched. A two-phase `SIGINT` -> `SIGTERM` sequence does not "cover both
363/// conventions": a child that handles only `SIGTERM` is killed by the `SIGINT` phase via kernel
364/// default before its `SIGTERM` handler can run, and the symmetric problem occurs for
365/// `SIGTERM` -> `SIGINT` against `SIGINT`-only-handler children.
366///
367/// Use multi-phase only when you control the child and use multiple signals as distinct
368/// cooperative shutdown stages (for example: `SIGINT` "begin drain" followed by `SIGTERM` "abort
369/// drain"). For unknown or heterogeneous children, pick `terminate_only` or `interrupt_only`.
370///
371/// # Construction
372///
373/// `UnixGracefulShutdown` cannot be constructed empty. The implicit `SIGKILL` fallback is not a
374/// phase; a graceful sequence must contain at least one signal to dispatch. Use one of the named
375/// single-signal constructors, or [`from_phases`](Self::from_phases) for a multi-phase
376/// cooperative sequence.
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct UnixGracefulShutdown {
379    phases: Vec<UnixGracefulPhase>,
380}
381
382impl UnixGracefulShutdown {
383    /// Single-phase sequence sending `SIGTERM` only. The recommended default for service-like
384    /// children; see the [type-level docs](Self#choosing-a-sequence) for the full discussion.
385    /// The implicit `SIGKILL` fallback runs after `timeout` if the child has not exited.
386    #[must_use]
387    pub fn terminate_only(timeout: Duration) -> Self {
388        Self::single(UnixGracefulPhase::terminate(timeout))
389    }
390
391    /// Single-phase sequence sending `SIGINT` only. Use this when forwarding a Ctrl-C / TTY
392    /// interrupt to a CLI-like child; see the [type-level docs](Self#choosing-a-sequence) for
393    /// the full discussion. The implicit `SIGKILL` fallback runs after `timeout` if the child
394    /// has not exited.
395    #[must_use]
396    pub fn interrupt_only(timeout: Duration) -> Self {
397        Self::single(UnixGracefulPhase::interrupt(timeout))
398    }
399
400    /// Single-phase sequence sending an arbitrary [`UnixGracefulSignal`].
401    #[must_use]
402    pub(crate) fn single(phase: UnixGracefulPhase) -> Self {
403        Self {
404            phases: vec![phase],
405        }
406    }
407
408    /// Multi-phase sequence dispatched in iteration order. Use only for cooperative
409    /// shutdown protocols against a child you control, where each signal in the sequence has
410    /// a distinct handler. For unknown children pick [`Self::terminate_only`] or
411    /// [`Self::interrupt_only`] instead; see the [type-level docs](Self#choosing-a-sequence)
412    /// for why a multi-phase sequence does not "cover both conventions".
413    ///
414    /// # Panics
415    ///
416    /// Panics if `phases` produces no elements. A graceful sequence must contain at least one
417    /// phase; the implicit `SIGKILL` fallback is not a phase.
418    #[must_use]
419    pub fn from_phases(phases: impl IntoIterator<Item = UnixGracefulPhase>) -> Self {
420        let phases: Vec<_> = phases.into_iter().collect();
421        assert!(
422            !phases.is_empty(),
423            "UnixGracefulShutdown must contain at least one phase",
424        );
425        Self { phases }
426    }
427
428    /// Phases in dispatch order.
429    #[must_use]
430    pub fn phases(&self) -> &[UnixGracefulPhase] {
431        &self.phases
432    }
433
434    /// Sum of every phase's timeout: the upper bound on wall-clock time spent in graceful phases
435    /// before the implicit kill.
436    #[must_use]
437    pub fn total_timeout(&self) -> Duration {
438        self.phases.iter().map(|p| p.timeout).sum()
439    }
440}
441
442/// Single-phase Windows graceful-shutdown sequence.
443///
444/// `CTRL_BREAK_EVENT` is dispatched to the child's console process group, then `TerminateProcess`
445/// runs as the kill fallback if the child has not exited within `timeout`. Windows has
446/// no second graceful signal: `GenerateConsoleCtrlEvent` accepts only `CTRL_BREAK_EVENT` for
447/// nonzero process groups, so a second graceful phase would just duplicate the first send.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub struct WindowsGracefulShutdown {
450    /// Maximum time to wait after sending `CTRL_BREAK_EVENT` before escalating to
451    /// `TerminateProcess`.
452    pub timeout: Duration,
453}
454
455impl WindowsGracefulShutdown {
456    /// Construct a Windows graceful sequence with the given timeout for the single
457    /// `CTRL_BREAK_EVENT` phase.
458    #[must_use]
459    pub const fn new(timeout: Duration) -> Self {
460        Self { timeout }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use assertr::prelude::*;
468
469    mod builder {
470        use super::*;
471
472        #[test]
473        fn populates_unix_terminate_only_phase() {
474            let shutdown = GracefulShutdown::builder()
475                .unix(UnixGracefulShutdown::terminate_only(Duration::from_secs(5)))
476                .windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
477                .build();
478
479            #[cfg(unix)]
480            {
481                let phases = shutdown.unix.phases();
482                assert_that!(phases.len()).is_equal_to(1);
483                assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(5));
484            }
485            #[cfg(windows)]
486            {
487                assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
488            }
489        }
490
491        #[test]
492        fn uses_interrupt_signal_for_unix_interrupt_only() {
493            let shutdown = GracefulShutdown::builder()
494                .unix(UnixGracefulShutdown::interrupt_only(Duration::from_secs(3)))
495                .windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
496                .build();
497
498            #[cfg(unix)]
499            {
500                let phases = shutdown.unix.phases();
501                assert_that!(phases.len()).is_equal_to(1);
502                assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(3));
503            }
504            #[cfg(windows)]
505            {
506                assert_that!(shutdown.windows.timeout).is_equal_to(Duration::from_secs(7));
507            }
508        }
509    }
510
511    #[cfg(unix)]
512    mod unix_sequence {
513        use super::*;
514        use crate::{UnixGracefulPhase, UnixGracefulSignal};
515
516        #[test]
517        fn from_phases_preserves_iteration_order() {
518            let sequence = UnixGracefulShutdown::from_phases([
519                UnixGracefulPhase::interrupt(Duration::from_secs(1)),
520                UnixGracefulPhase::terminate(Duration::from_secs(2)),
521            ]);
522
523            let phases = sequence.phases();
524            assert_that!(phases.len()).is_equal_to(2);
525            assert_that!(phases[0].signal).is_equal_to(UnixGracefulSignal::Interrupt);
526            assert_that!(phases[0].timeout).is_equal_to(Duration::from_secs(1));
527            assert_that!(phases[1].signal).is_equal_to(UnixGracefulSignal::Terminate);
528            assert_that!(phases[1].timeout).is_equal_to(Duration::from_secs(2));
529        }
530
531        #[test]
532        #[should_panic(expected = "UnixGracefulShutdown must contain at least one phase")]
533        fn from_phases_panics_on_empty_input() {
534            let _ = UnixGracefulShutdown::from_phases(std::iter::empty());
535        }
536
537        #[test]
538        fn total_timeout_sums_every_phase() {
539            let sequence = UnixGracefulShutdown::from_phases([
540                UnixGracefulPhase::interrupt(Duration::from_secs(1)),
541                UnixGracefulPhase::terminate(Duration::from_secs(2)),
542                UnixGracefulPhase::terminate(Duration::from_millis(500)),
543            ]);
544
545            assert_that!(sequence.total_timeout()).is_equal_to(Duration::from_millis(3_500));
546        }
547
548        #[test]
549        fn total_timeout_of_single_phase_equals_that_phase() {
550            let sequence = UnixGracefulShutdown::terminate_only(Duration::from_secs(7));
551
552            assert_that!(sequence.total_timeout()).is_equal_to(Duration::from_secs(7));
553        }
554    }
555
556    mod total_timeout {
557        use super::*;
558
559        #[test]
560        fn reflects_active_platform_budget() {
561            let shutdown = GracefulShutdown::builder()
562                .unix(UnixGracefulShutdown::from_phases([
563                    UnixGracefulPhase::interrupt(Duration::from_secs(1)),
564                    UnixGracefulPhase::terminate(Duration::from_secs(4)),
565                ]))
566                .windows(WindowsGracefulShutdown::new(Duration::from_secs(7)))
567                .build();
568
569            #[cfg(unix)]
570            assert_that!(shutdown.total_timeout()).is_equal_to(Duration::from_secs(5));
571            #[cfg(windows)]
572            assert_that!(shutdown.total_timeout()).is_equal_to(Duration::from_secs(7));
573        }
574    }
575}