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}