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
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}