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}