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}