tokio_process_tools/terminate_on_drop.rs
1use crate::process_handle::ProcessHandle;
2use crate::OutputStream;
3use std::ops::Deref;
4use std::time::Duration;
5
6/// A wrapper that automatically terminates a process when dropped.
7///
8/// # Safety Requirements
9///
10/// **WARNING**: This type requires a multithreaded tokio runtime to function correctly!
11///
12/// # Usage Guidelines
13///
14/// This type should only be used when:
15/// - Your code is running in a multithreaded tokio runtime.
16/// - Automatic process cleanup on drop is absolutely necessary.
17///
18/// # Recommended Alternatives
19///
20/// Instead of relying on automatic termination, prefer these safer approaches:
21/// 1. Manual process termination using [`ProcessHandle::terminate`]
22/// 2. Awaiting process completion using [`ProcessHandle::wait_for_completion`]
23/// 3. Awaiting process completion or performing an explicit termination using
24/// [`ProcessHandle::wait_for_completion_or_terminate`]
25///
26/// # Implementation Details
27///
28/// The drop implementation tries to terminate the process if it was neither awaited nor
29/// terminated before being dropped. If termination fails, a panic is raised.
30#[derive(Debug)]
31pub struct TerminateOnDrop<O: OutputStream> {
32 pub(crate) process_handle: ProcessHandle<O>,
33 pub(crate) interrupt_timeout: Duration,
34 pub(crate) terminate_timeout: Duration,
35}
36
37impl<O: OutputStream> Deref for TerminateOnDrop<O> {
38 type Target = ProcessHandle<O>;
39
40 fn deref(&self) -> &Self::Target {
41 &self.process_handle
42 }
43}
44
45impl<O: OutputStream> Drop for TerminateOnDrop<O> {
46 fn drop(&mut self) {
47 // 1. We are in a Drop implementation which is synchronous - it can't be async.
48 // But we need to execute an async operation (the `terminate` call).
49 //
50 // 2. `Block_on` is needed because it takes an async operation and runs it to completion
51 // synchronously - it's how we can execute our async terminate call within the synchronous
52 // drop.
53 //
54 // 3. However, block_on by itself isn't safe to call from within an async context
55 // (which we are in since we're inside the Tokio runtime).
56 // This is because it could lead to deadlocks - imagine if the current thread is needed to
57 // process some task that our blocked async operation is waiting on.
58 //
59 // 4. This is where block_in_place comes in - it tells Tokio:
60 // "hey, I'm about to block this thread, please make sure other threads are available to
61 // still process tasks". It essentially moves the blocking work to a dedicated thread pool
62 // so that the async runtime can continue functioning.
63 //
64 // 5. Note that `block_in_place` requires a multithreaded tokio runtime to be active!
65 // So use `#[tokio::test(flavor = "multi_thread")]` in tokio-enabled tests.
66 //
67 // 6. Also note that `block_in_place` enforces that the given closure runs to completion,
68 // even when the async executor is terminated - this might be because our program ended
69 // or because we crashed due to a panic.
70 tokio::task::block_in_place(|| {
71 tokio::runtime::Handle::current().block_on(async {
72 if !self.process_handle.is_running().as_bool() {
73 tracing::debug!(
74 process = %self.process_handle.name,
75 "Process already terminated"
76 );
77 return;
78 }
79
80 tracing::debug!(process = %self.process_handle.name, "Terminating process");
81 match self
82 .process_handle
83 .terminate(self.interrupt_timeout, self.terminate_timeout)
84 .await
85 {
86 Ok(exit_status) => {
87 tracing::debug!(
88 process = %self.process_handle.name,
89 ?exit_status,
90 "Successfully terminated process"
91 )
92 }
93 Err(err) => {
94 panic!(
95 "Failed to terminate process '{}': {}",
96 self.process_handle.name, err
97 );
98 }
99 };
100 });
101 });
102 }
103}