tokio_process_tools/terminate_on_drop.rs
1use crate::OutputStream;
2use crate::process_handle::ProcessHandle;
3use std::ops::{Deref, DerefMut};
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> DerefMut for TerminateOnDrop<O> {
46 fn deref_mut(&mut self) -> &mut Self::Target {
47 &mut self.process_handle
48 }
49}
50
51impl<O: OutputStream> Drop for TerminateOnDrop<O> {
52 fn drop(&mut self) {
53 // 1. We are in a Drop implementation which is synchronous - it can't be async.
54 // But we need to execute an async operation (the `terminate` call).
55 //
56 // 2. `Block_on` is needed because it takes an async operation and runs it to completion
57 // synchronously - it's how we can execute our async terminate call within the synchronous
58 // drop.
59 //
60 // 3. However, block_on by itself isn't safe to call from within an async context
61 // (which we are in since we're inside the Tokio runtime).
62 // This is because it could lead to deadlocks - imagine if the current thread is needed to
63 // process some task that our blocked async operation is waiting on.
64 //
65 // 4. This is where block_in_place comes in - it tells Tokio:
66 // "hey, I'm about to block this thread, please make sure other threads are available to
67 // still process tasks". It essentially moves the blocking work to a dedicated thread pool
68 // so that the async runtime can continue functioning.
69 //
70 // 5. Note that `block_in_place` requires a multithreaded tokio runtime to be active!
71 // So use `#[tokio::test(flavor = "multi_thread")]` in tokio-enabled tests.
72 //
73 // 6. Also note that `block_in_place` enforces that the given closure runs to completion,
74 // even when the async executor is terminated - this might be because our program ended
75 // or because we crashed due to a panic.
76 tokio::task::block_in_place(|| {
77 tokio::runtime::Handle::current().block_on(async {
78 if !self.process_handle.is_running().as_bool() {
79 tracing::debug!(
80 process = %self.process_handle.name,
81 "Process already terminated"
82 );
83 return;
84 }
85
86 tracing::debug!(process = %self.process_handle.name, "Terminating process");
87 match self
88 .process_handle
89 .terminate(self.interrupt_timeout, self.terminate_timeout)
90 .await
91 {
92 Ok(exit_status) => {
93 tracing::debug!(
94 process = %self.process_handle.name,
95 ?exit_status,
96 "Successfully terminated process"
97 )
98 }
99 Err(err) => {
100 panic!(
101 "Failed to terminate process '{}': {}",
102 self.process_handle.name, err
103 );
104 }
105 };
106 });
107 });
108 }
109}