char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
12.1 KiB330 linesraw
1use std::io;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::sync::{Arc, Mutex, OnceLock, Weak};
4use std::time::Duration;
5
6use tokio::runtime::Handle;
7use tokio_util::sync::CancellationToken;
8
9pub struct Tree {
10    processes: Mutex<Vec<Weak<super::Process>>>,
11    sync_interrupt: AtomicU64,
12    installed: AtomicBool,
13    /// Whether the install owns SIGINT (run mode). Serve and the REPL
14    /// claim SIGINT through the tree instead; listener feeds consult
15    /// holds() to tell a supervisor-owned death from one they must
16    /// emulate themselves.
17    interrupt: AtomicBool,
18    /// Claimed by the subsystem that owns SIGINT shutdown or interrupt
19    /// handling (serve's CLI listener, the REPL). Read by the supervisor
20    /// loop when it holds SIGINT; otherwise served by claim_interrupt's
21    /// dedicated feed. Either way the owner shares one chaining registry
22    /// with listener feeds, so install order never matters.
23    #[cfg_attr(windows, allow(dead_code))]
24    interrupt_owner: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
25    /// Guards claim_interrupt's dedicated feed against double installs.
26    #[cfg(unix)]
27    interrupt_claimed: AtomicBool,
28    /// Blocks new spawns once the process is dying, so respawn loops cannot
29    /// relaunch children during the shutdown grace period.
30    closed: AtomicBool,
31    /// Claimed by the subsystem that owns SIGTERM shutdown (serve's CLI
32    /// listener). The tree is the sole TERM listener, so claiming and
33    /// dispatch serialize on this slot: a delivery either runs the owner or
34    /// predates it, never both and never neither.
35    #[cfg_attr(windows, allow(dead_code))]
36    term_owner: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
37    pub(super) stopped: CancellationToken,
38}
39impl Tree {
40    pub fn global() -> Arc<Self> {
41        static TREE: OnceLock<Arc<Tree>> = OnceLock::new();
42        TREE.get_or_init(|| {
43            Arc::new(Self {
44                processes: Mutex::new(Vec::new()),
45                sync_interrupt: AtomicU64::new(0),
46                installed: AtomicBool::new(false),
47                interrupt: AtomicBool::new(false),
48                interrupt_owner: Mutex::new(None),
49                #[cfg(unix)]
50                interrupt_claimed: AtomicBool::new(false),
51                closed: AtomicBool::new(false),
52                term_owner: Mutex::new(None),
53                stopped: CancellationToken::new(),
54            })
55        })
56        .clone()
57    }
58    pub(super) fn register(&self, process: &Arc<super::Process>) {
59        let mut processes = self.processes.lock().unwrap();
60        processes.retain(|process| process.strong_count() != 0);
61        processes.push(Arc::downgrade(process));
62    }
63    pub fn force_live_processes(&self) {
64        self.closed.store(true, Ordering::Release);
65        for process in self.live() {
66            process.request("terminated", None, true);
67        }
68    }
69    pub(super) fn sync_generation(&self) -> u64 {
70        self.sync_interrupt.load(Ordering::SeqCst)
71    }
72    #[cfg(unix)]
73    pub fn claim_term(
74        self: &Arc<Self>,
75        runtime: &Handle,
76        owner: Arc<dyn Fn() + Send + Sync>,
77    ) -> io::Result<()> {
78        *self.term_owner.lock().unwrap() = Some(owner);
79        self.install(runtime, false)
80    }
81    /// Hands SIGINT deliveries to a subsystem owner through the shared
82    /// chaining registry, alongside any listener feeds. Modes whose
83    /// supervisor loop does not hold SIGINT get a dedicated feed; run
84    /// mode's loop routes to the owner itself.
85    #[cfg(unix)]
86    pub fn claim_interrupt(
87        self: &Arc<Self>,
88        runtime: &Handle,
89        owner: Arc<dyn Fn() + Send + Sync>,
90    ) -> io::Result<()> {
91        *self.interrupt_owner.lock().unwrap() = Some(owner.clone());
92        if self.interrupt.load(Ordering::Acquire)
93            || self.interrupt_claimed.swap(true, Ordering::AcqRel)
94        {
95            return Ok(());
96        }
97        let _enter = runtime.enter();
98        let mut stream =
99            match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()) {
100                Ok(stream) => stream,
101                Err(error) => {
102                    *self.interrupt_owner.lock().unwrap() = None;
103                    self.interrupt_claimed.store(false, Ordering::Release);
104                    return Err(error);
105                }
106            };
107        runtime.spawn(async move {
108            while stream.recv().await.is_some() {
109                owner();
110            }
111        });
112        Ok(())
113    }
114    pub(super) fn is_closed(&self) -> bool {
115        self.closed.load(Ordering::Acquire)
116    }
117    /// Whether the supervisor itself holds this signal's handler, and so
118    /// owns its default fate. Listener feeds emulate the prior disposition
119    /// only for signals nobody holds.
120    #[cfg(unix)]
121    pub(crate) fn holds(&self, signal: libc::c_int) -> bool {
122        if !self.installed.load(Ordering::Acquire) {
123            return false;
124        }
125        signal == libc::SIGTERM
126            || (signal == libc::SIGINT
127                && (self.interrupt.load(Ordering::Acquire)
128                    || self.interrupt_owner.lock().unwrap().is_some()))
129    }
130    /// Asks every live process to shut down with its configured signal and
131    /// grace period. One shot: already-finished processes ignore the request,
132    /// and later spawns start fresh.
133    pub(crate) fn stop_live_processes(&self) {
134        for process in self.live() {
135            process.request("terminated", None, false);
136        }
137    }
138    /// Stops only processes blocked in a synchronous wait, leaving other
139    /// session work alone. The REPL uses this so Ctrl-C interrupts runSync
140    /// without cancelling background operations.
141    pub fn stop_sync_processes(&self) {
142        // Pair the generation with runSync's waiter publication. SeqCst
143        // ensures an interrupt racing spawn sees the waiter, or the waiter
144        // sees the interrupt, rather than both missing each other.
145        self.sync_interrupt.fetch_add(1, Ordering::SeqCst);
146        for process in self.live() {
147            if !process.worker && process.sync_wait.load(Ordering::SeqCst) {
148                process.request("terminated", None, false);
149            }
150        }
151    }
152    fn live(&self) -> Vec<Arc<super::Process>> {
153        self.processes
154            .lock()
155            .unwrap()
156            .iter()
157            .filter_map(Weak::upgrade)
158            .collect()
159    }
160    pub(super) fn install(
161        self: &Arc<Self>,
162        runtime: &Handle,
163        handle_interrupt: bool,
164    ) -> io::Result<()> {
165        if self.installed.swap(true, Ordering::AcqRel) {
166            return Ok(());
167        }
168        self.interrupt.store(handle_interrupt, Ordering::Release);
169        let _enter = runtime.enter();
170        let mut signals = match Signals::listen(handle_interrupt) {
171            Ok(Some(signals)) => signals,
172            Ok(None) => return Ok(()),
173            Err(error) => {
174                self.installed.store(false, Ordering::Release);
175                return Err(error);
176            }
177        };
178        let tree = self.clone();
179        runtime.spawn(async move {
180            loop {
181                let delivery = signals.recv().await;
182                // A claimed interrupt belongs to its owner (serve
183                // shutdown, REPL eval interrupt); listeners still fire
184                // through their own feeds.
185                #[cfg(unix)]
186                if matches!(delivery, Delivery::Interrupt) {
187                    let owner = tree.interrupt_owner.lock().unwrap().clone();
188                    if let Some(owner) = owner {
189                        owner();
190                        continue;
191                    }
192                }
193                // Claimed TERM belongs to its owner, including repeats.
194                // In-flight children keep running during serve's drain.
195                #[cfg(unix)]
196                if matches!(delivery, Delivery::Terminate) {
197                    let owner = tree.term_owner.lock().unwrap().clone();
198                    if let Some(owner) = owner {
199                        owner();
200                        continue;
201                    }
202                }
203                // Listened signals belong to their JS listeners: skip the
204                // shutdown and death below, so delivery does not kill the
205                // process while listeners exist. Serve's claimed TERM still
206                // runs its owner above.
207                if super::super::signals::listener_count(delivery.name()) > 0 {
208                    continue;
209                }
210                tree.closed.store(true, Ordering::Release);
211                tree.stop_live_processes();
212                // Bounded shutdown, abortable by an impatient repeat.
213                let live = tree.live();
214                let pending =
215                    futures::future::join_all(live.iter().map(|process| process.wait(true, None)));
216                tokio::select! {
217                    _ = tokio::time::timeout(Duration::from_secs(2), pending) => {},
218                    _ = signals.recv() => {},
219                }
220                // Grace-resisting descendants cannot outlive the owner.
221                for process in tree.live() {
222                    process.kill_scope();
223                }
224                tree.stopped.cancel();
225                delivery.die();
226                return;
227            }
228        });
229        Ok(())
230    }
231}
232
233#[derive(Clone, Copy)]
234enum Delivery {
235    Interrupt,
236    #[cfg_attr(windows, allow(dead_code))]
237    Terminate,
238}
239
240impl Delivery {
241    fn name(self) -> &'static str {
242        match self {
243            Self::Interrupt => "SIGINT",
244            Self::Terminate => "SIGTERM",
245        }
246    }
247}
248
249struct Signals {
250    interrupt: Option<Stream>,
251    terminate: Option<Stream>,
252}
253impl Signals {
254    async fn recv(&mut self) -> Delivery {
255        async fn next(stream: &mut Option<Stream>) {
256            if let Some(stream) = stream
257                && stream.recv().await.is_some()
258            {
259                return;
260            }
261            std::future::pending().await
262        }
263        tokio::select! {
264            _ = next(&mut self.interrupt) => Delivery::Interrupt,
265            _ = next(&mut self.terminate) => Delivery::Terminate,
266        }
267    }
268}
269
270#[cfg(unix)]
271type Stream = tokio::signal::unix::Signal;
272#[cfg(unix)]
273impl Signals {
274    fn listen(handle_interrupt: bool) -> io::Result<Option<Self>> {
275        use tokio::signal::unix::{SignalKind, signal};
276        // An inherited SIG_IGN stays ignored: taking over the signal would
277        // change the fate of a process that was launched to survive it.
278        // Consult the startup snapshot, never the live disposition, which
279        // earlier listener feeds may already have taken over.
280        let interrupt =
281            if handle_interrupt && !avril_subprocess::signals::originally_ignored(libc::SIGINT) {
282                Some(signal(SignalKind::interrupt())?)
283            } else {
284                None
285            };
286        let terminate = if avril_subprocess::signals::originally_ignored(libc::SIGTERM) {
287            None
288        } else {
289            Some(signal(SignalKind::terminate())?)
290        };
291        Ok((interrupt.is_some() || terminate.is_some()).then_some(Self { interrupt, terminate }))
292    }
293}
294#[cfg(unix)]
295impl Delivery {
296    fn die(self) {
297        let number = match self {
298            Self::Interrupt => libc::SIGINT,
299            Self::Terminate => libc::SIGTERM,
300        };
301        unsafe {
302            libc::signal(number, libc::SIG_DFL);
303            libc::raise(number);
304        }
305    }
306}
307
308// Windows has no termination signal to supervise: console close, logoff and
309// shutdown end the process, and kill-on-close jobs take the children along.
310#[cfg(windows)]
311type Stream = tokio::signal::windows::CtrlC;
312#[cfg(windows)]
313impl Signals {
314    fn listen(handle_interrupt: bool) -> io::Result<Option<Self>> {
315        if !handle_interrupt {
316            return Ok(None);
317        }
318        Ok(Some(Self {
319            interrupt: Some(tokio::signal::windows::ctrl_c()?),
320            terminate: None,
321        }))
322    }
323}
324#[cfg(windows)]
325impl Delivery {
326    fn die(self) {
327        // The default console control handler exits with STATUS_CONTROL_C_EXIT.
328        std::process::exit(0xC000013Au32 as i32);
329    }
330}