Skip to main content

tokio_process_tools/process_handle/
drop_guard.rs

1use super::ProcessHandle;
2#[cfg(any(unix, windows))]
3use super::termination::GracefulShutdown;
4use crate::output_stream::OutputStream;
5use crate::panic_on_drop::PanicOnDrop;
6#[cfg(any(unix, windows))]
7use crate::terminate_on_drop::TerminateOnDrop;
8
9/// Drop-time behavior selected by the lifecycle methods on [`ProcessHandle`].
10///
11/// The state machine has two reachable states because every public lifecycle entry point either
12/// keeps both safeguards on (`Armed`) or turns both off (`Disarmed`). There is no "panic only,
13/// no cleanup" or "cleanup only, no panic" combination: the panic guard makes sense only when
14/// paired with the kill that signals the misuse.
15#[derive(Debug)]
16pub(crate) enum DropMode {
17    /// Cleanup is attempted on drop and the panic guard fires when it does.
18    Armed { panic: PanicOnDrop },
19
20    /// Both cleanup and the panic guard are off. Drop is a no-op for this handle's lifecycle.
21    Disarmed,
22}
23
24impl<Stdout, Stderr> Drop for ProcessHandle<Stdout, Stderr>
25where
26    Stdout: OutputStream,
27    Stderr: OutputStream,
28{
29    fn drop(&mut self) {
30        match &self.drop_mode {
31            DropMode::Armed { .. } => {
32                // We want users to explicitly await or terminate spawned processes.
33                // If not done so, kill the process group/job now to have some sort of
34                // last-resort cleanup. The panic guard will additionally raise a panic when this
35                // method returns, signaling the misuse loudly. Targeting the group/job (rather
36                // than the child's PID alone) catches any grandchildren the child has spawned,
37                // which is the same invariant the explicit `kill()` path upholds.
38                if let Err(err) = self.send_kill_signal() {
39                    tracing::warn!(
40                        process = %self.name,
41                        error = %err,
42                        "Failed to kill process while dropping an armed ProcessHandle"
43                    );
44                }
45            }
46            DropMode::Disarmed => {}
47        }
48    }
49}
50
51impl<Stdout, Stderr> ProcessHandle<Stdout, Stderr>
52where
53    Stdout: OutputStream,
54    Stderr: OutputStream,
55{
56    pub(super) fn new_armed_drop_mode() -> DropMode {
57        DropMode::Armed {
58            panic: armed_panic_guard(),
59        }
60    }
61
62    /// Sets a panic-on-drop mechanism for this `ProcessHandle`.
63    ///
64    /// This method enables a safeguard that ensures that the process represented by this
65    /// `ProcessHandle` is properly terminated or awaited before being dropped.
66    /// If `must_be_terminated` is set and the `ProcessHandle` is
67    /// dropped without successfully terminating, killing, waiting for, or explicitly detaching the
68    /// process, an intentional panic will occur to prevent silent failure-states, ensuring that
69    /// system resources are handled correctly.
70    ///
71    /// You typically do not need to call this, as every `ProcessHandle` is marked by default.
72    /// Call `must_not_be_terminated` to clear this safeguard to explicitly allow dropping the
73    /// process without terminating it.
74    /// Calling this method while the safeguard is already enabled is safe and has no effect beyond
75    /// keeping the handle armed.
76    ///
77    /// # Panic
78    ///
79    /// If the `ProcessHandle` is dropped without being awaited or terminated successfully
80    /// after calling this method, a panic will occur with a descriptive message
81    /// to inform about the incorrect usage.
82    pub fn must_be_terminated(&mut self) {
83        match &mut self.drop_mode {
84            DropMode::Armed { panic } if panic.is_armed() => {
85                // Already armed; nothing to do.
86            }
87            _ => {
88                self.drop_mode = DropMode::Armed {
89                    panic: armed_panic_guard(),
90                };
91            }
92        }
93    }
94
95    /// Disables the kill/panic-on-drop safeguards for this handle.
96    ///
97    /// Dropping the handle after calling this method will no longer signal, kill, or panic.
98    /// However, this does **not** keep the library-owned stdio pipes alive. If the child still
99    /// depends on stdin, stdout, or stderr being open, dropping the handle may still affect it.
100    ///
101    /// Use plain [`tokio::process::Command`] directly when you need a child process that can
102    /// outlive the original handle without depending on captured stdio pipes.
103    ///
104    /// Also, the right opt-out after [`terminate`](Self::terminate) returns an unrecoverable error
105    /// and the caller chooses to accept the failure instead of retrying or escalating to
106    /// [`kill`](Self::kill).
107    pub fn must_not_be_terminated(&mut self) {
108        // Defuse the panic guard before swapping the variant so the dropped `PanicOnDrop` does
109        // not fire when the old `Armed` value is dropped by the assignment.
110        if let DropMode::Armed { panic } = &mut self.drop_mode {
111            panic.defuse();
112        }
113        self.drop_mode = DropMode::Disarmed;
114    }
115
116    /// Test-only inspector: whether the drop guard is currently armed.
117    #[doc(hidden)]
118    pub fn is_drop_armed(&self) -> bool {
119        matches!(&self.drop_mode, DropMode::Armed { panic } if panic.is_armed())
120    }
121
122    /// Test-only inspector: whether the drop guard has been disarmed.
123    #[doc(hidden)]
124    pub fn is_drop_disarmed(&self) -> bool {
125        matches!(self.drop_mode, DropMode::Disarmed)
126    }
127
128    /// Wrap this process handle in a `TerminateOnDrop` instance, terminating the controlled process
129    /// automatically when this handle is dropped.
130    ///
131    /// `shutdown` carries the same per-platform graceful policy as [`Self::terminate`]; see
132    /// [`GracefulShutdown`] for how to construct it.
133    ///
134    /// **SAFETY: This only works when your code is running in a multithreaded tokio runtime!**
135    ///
136    /// Prefer manual termination of the process or awaiting it and relying on the (automatically
137    /// configured) `must_be_terminated` logic, raising a panic when a process was neither awaited
138    /// nor terminated before being dropped.
139    #[cfg(any(unix, windows))]
140    pub fn terminate_on_drop(self, shutdown: GracefulShutdown) -> TerminateOnDrop<Stdout, Stderr> {
141        TerminateOnDrop {
142            process_handle: self,
143            shutdown,
144        }
145    }
146}
147
148fn armed_panic_guard() -> PanicOnDrop {
149    PanicOnDrop::new(
150        "tokio_process_tools::ProcessHandle",
151        "The process was not terminated.",
152        "Successfully call `wait_for_completion`, `terminate`, or `kill`, or call `must_not_be_terminated` before the type is dropped!",
153    )
154}