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}