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}