Skip to main content

turbopack_trace_utils/
exit.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    sync::{Arc, Mutex, OnceLock},
5};
6
7use anyhow::Result;
8use tokio::{select, sync::mpsc, task::JoinSet};
9
10/// A guard for the exit handler. When dropped, the exit guard will be dropped.
11/// It might also be dropped on Ctrl-C.
12pub struct ExitGuard<T>(Arc<Mutex<Option<T>>>);
13
14impl<T> Drop for ExitGuard<T> {
15    fn drop(&mut self) {
16        drop(self.0.lock().unwrap().take())
17    }
18}
19
20impl<T: Send + 'static> ExitGuard<T> {
21    /// Drop a guard when Ctrl-C is pressed or the [ExitGuard] is dropped.
22    ///
23    /// On wasm targets there is no Ctrl-C to listen for (`tokio::signal` does not exist on wasi),
24    /// so the guard is only dropped when the [ExitGuard] itself is dropped.
25    pub fn new(guard: T) -> Result<Self> {
26        let guard = Arc::new(Mutex::new(Some(guard)));
27        #[cfg(not(target_family = "wasm"))]
28        {
29            let guard = guard.clone();
30            tokio::spawn(async move {
31                tokio::signal::ctrl_c().await.unwrap();
32                drop(guard.lock().unwrap().take());
33                std::process::exit(0);
34            });
35        }
36        Ok(ExitGuard(guard))
37    }
38}
39
40type BoxExitFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
41
42/// The singular global ExitHandler. This is primarily used to ensure `ExitHandler::listen` is only
43/// called once.
44///
45/// The global handler is intentionally not exposed, so that APIs that depend on exit behavior are
46/// required to take the `ExitHandler`. This ensures that the `ExitHandler` is configured before
47/// these APIs are run, and that these consumers can be used with a callback (e.g. a mock) instead.
48static GLOBAL_EXIT_HANDLER: OnceLock<Arc<ExitHandler>> = OnceLock::new();
49
50pub struct ExitHandler {
51    tx: mpsc::UnboundedSender<BoxExitFuture>,
52}
53
54impl ExitHandler {
55    /// Waits for `SIGINT` using [`tokio::signal::ctrl_c`], and exits the process with exit code `0`
56    /// after running any futures scheduled with [`ExitHandler::on_exit`].
57    ///
58    /// As this uses global process signals, this must only be called once, and will panic if called
59    /// multiple times. Use this when you own the process (e.g. `turbopack-cli`).
60    ///
61    /// If you don't own the process (e.g. you're called as a library, such as in `next-swc`), use
62    /// [`ExitHandler::new_receiver`] instead.
63    ///
64    /// This may listen for other signals, like `SIGTERM` or `SIGPIPE` in the future.
65    ///
66    /// On wasm targets there are no process signals, so nothing is ever run: the returned handler
67    /// only fires through [`ExitReceiver::run_exit_handler`], which this function has no way to
68    /// reach. wasm callers should use [`ExitHandler::new_receiver`].
69    pub fn listen() -> &'static Arc<ExitHandler> {
70        let (handler, receiver) = Self::new_receiver();
71        if GLOBAL_EXIT_HANDLER.set(handler).is_err() {
72            panic!("ExitHandler::listen must only be called once");
73        }
74        #[cfg(not(target_family = "wasm"))]
75        tokio::spawn(async move {
76            tokio::signal::ctrl_c()
77                .await
78                .expect("failed to set ctrl_c handler");
79            receiver.run_exit_handler().await;
80            std::process::exit(0);
81        });
82        #[cfg(target_family = "wasm")]
83        drop(receiver);
84        GLOBAL_EXIT_HANDLER.get().expect("value is set")
85    }
86
87    /// Creates an [`ExitHandler`] that can be manually controlled with an [`ExitReceiver`].
88    ///
89    /// This does not actually exit the process or listen for any signals. If you'd like that
90    /// behavior, use [`ExitHandler::listen`].
91    ///
92    /// Because this API has no global side-effects and can be called many times within the same
93    /// process, it is possible to use it to provide a mock [`ExitHandler`] inside unit tests.
94    pub fn new_receiver() -> (Arc<ExitHandler>, ExitReceiver) {
95        let (tx, rx) = mpsc::unbounded_channel();
96        (Arc::new(ExitHandler { tx }), ExitReceiver { rx })
97    }
98
99    /// Register this given [`Future`] to run upon process exit.
100    ///
101    /// As there are many ways for a process be killed that are outside of a process's own control
102    /// (e.g. `SIGKILL` or `SIGSEGV`), this API is provided on a best-effort basis.
103    pub fn on_exit(&self, fut: impl Future<Output = ()> + Send + 'static) {
104        // realistically, this error case can only happen with the `new_receiver` API.
105        self.tx
106            .send(Box::pin(fut))
107            .expect("cannot send future after process exit");
108    }
109}
110
111/// Provides a way to run futures scheduled with an [`ExitHandler`].
112pub struct ExitReceiver {
113    rx: mpsc::UnboundedReceiver<BoxExitFuture>,
114}
115
116impl ExitReceiver {
117    /// Call this when the process exits to run the futures scheduled via [`ExitHandler::on_exit`].
118    ///
119    /// As this is intended to be used in a library context, this does not exit the process. It is
120    /// expected that the process will not exit until this async method finishes executing.
121    ///
122    /// Additional work can be scheduled using [`ExitHandler::on_exit`] even while this is running,
123    /// and it will execute before this function finishes. Work attempted to be scheduled after this
124    /// finishes will panic.
125    pub async fn run_exit_handler(mut self) {
126        let mut set = JoinSet::new();
127        while let Ok(fut) = self.rx.try_recv() {
128            set.spawn(fut);
129        }
130        loop {
131            select! {
132                biased;
133                Some(fut) = self.rx.recv() => {
134                    set.spawn(fut);
135                },
136                val = set.join_next() => {
137                    match val {
138                        Some(Ok(())) => {}
139                        Some(Err(_)) => panic!("ExitHandler future panicked!"),
140                        None => return,
141                    }
142                },
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    #![allow(clippy::needless_return)] // tokio macro-generated code doesn't respect this
151    use std::{
152        future::Future,
153        pin::Pin,
154        sync::{
155            Arc,
156            atomic::{AtomicBool, AtomicU32, Ordering},
157        },
158    };
159
160    use super::ExitHandler;
161
162    #[tokio::test]
163    async fn test_on_exit() {
164        let (handler, receiver) = ExitHandler::new_receiver();
165
166        let called = Arc::new(AtomicBool::new(false));
167        handler.on_exit({
168            let called = Arc::clone(&called);
169            async move {
170                tokio::task::yield_now().await;
171                called.store(true, Ordering::SeqCst);
172            }
173        });
174
175        receiver.run_exit_handler().await;
176        assert!(called.load(Ordering::SeqCst));
177    }
178
179    #[tokio::test]
180    async fn test_queue_while_exiting() {
181        let (handler, receiver) = ExitHandler::new_receiver();
182        let call_count = Arc::new(AtomicU32::new(0));
183
184        type BoxExitFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
185
186        // this struct is needed to construct the recursive closure type
187        #[derive(Clone)]
188        struct GetFut {
189            handler: Arc<ExitHandler>,
190            call_count: Arc<AtomicU32>,
191        }
192
193        impl GetFut {
194            fn get(self) -> BoxExitFuture {
195                Box::pin(async move {
196                    tokio::task::yield_now().await;
197                    if self.call_count.fetch_add(1, Ordering::SeqCst) < 99 {
198                        // queue more work while the exit handler is running
199                        Arc::clone(&self.handler).on_exit(self.get())
200                    }
201                })
202            }
203        }
204
205        handler.on_exit(
206            GetFut {
207                handler: Arc::clone(&handler),
208                call_count: Arc::clone(&call_count),
209            }
210            .get(),
211        );
212        receiver.run_exit_handler().await;
213        assert_eq!(call_count.load(Ordering::SeqCst), 100);
214    }
215}