nextest_runner/
signal.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Support for handling signals in nextest.
5
6use crate::errors::SignalHandlerSetupError;
7
8/// The kind of signal handling to set up for a test run.
9///
10/// A `SignalHandlerKind` can be passed into
11/// [`TestRunnerBuilder::build`](crate::runner::TestRunnerBuilder::build).
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
13pub enum SignalHandlerKind {
14    /// The standard signal handler. Capture interrupt and termination signals depending on the
15    /// platform.
16    Standard,
17
18    /// Debugger mode signal handler. Only handles termination signals (SIGTERM,
19    /// SIGHUP) to allow graceful cleanup. Other signals are ignored by nextest
20    /// and are expected to be handled by the debugger.
21    DebuggerMode,
22
23    /// A no-op signal handler. Useful for tests.
24    Noop,
25}
26
27impl SignalHandlerKind {
28    pub(crate) fn build(self) -> Result<SignalHandler, SignalHandlerSetupError> {
29        match self {
30            Self::Standard => SignalHandler::new(),
31            Self::DebuggerMode => SignalHandler::debugger_mode(),
32            Self::Noop => Ok(SignalHandler::noop()),
33        }
34    }
35}
36
37/// The signal handler implementation.
38#[derive(Debug)]
39pub(crate) struct SignalHandler {
40    signals: Option<imp::Signals>,
41}
42
43impl SignalHandler {
44    /// Creates a new `SignalHandler` that handles Ctrl-C and other signals.
45    #[cfg(any(unix, windows))]
46    pub(crate) fn new() -> Result<Self, SignalHandlerSetupError> {
47        let signals = imp::Signals::new()?;
48        Ok(Self {
49            signals: Some(signals),
50        })
51    }
52
53    /// Creates a new `SignalHandler` for debugger mode that only handles termination signals.
54    #[cfg(any(unix, windows))]
55    pub(crate) fn debugger_mode() -> Result<Self, SignalHandlerSetupError> {
56        let signals = imp::Signals::debugger_mode()?;
57        Ok(Self {
58            signals: Some(signals),
59        })
60    }
61
62    /// Creates a new `SignalReceiver` that does nothing.
63    pub(crate) fn noop() -> Self {
64        Self { signals: None }
65    }
66
67    pub(crate) async fn recv(&mut self) -> Option<SignalEvent> {
68        match &mut self.signals {
69            Some(signals) => signals.recv().await,
70            None => None,
71        }
72    }
73}
74
75#[cfg(unix)]
76mod imp {
77    use super::*;
78    use std::io;
79    use tokio::signal::unix::{SignalKind, signal};
80    use tokio_stream::{StreamExt, StreamMap, wrappers::SignalStream};
81
82    #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
83    enum SignalId {
84        Int,
85        Hup,
86        Term,
87        Quit,
88        Tstp,
89        Cont,
90        Info,
91        Usr1,
92    }
93
94    /// Signals for SIGINT, SIGTERM and SIGHUP on Unix.
95    #[derive(Debug)]
96    pub(super) struct Signals {
97        // The number of streams is quite small, so a StreamMap (backed by a
98        // Vec) is a good option to store the list of streams to poll.
99        map: StreamMap<SignalId, SignalStream>,
100        sigquit_as_info: bool,
101    }
102
103    impl Signals {
104        pub(super) fn new() -> io::Result<Self> {
105            let mut map = StreamMap::new();
106
107            // Set up basic signals.
108            map.extend([
109                (SignalId::Int, signal_stream(SignalKind::interrupt())?),
110                (SignalId::Hup, signal_stream(SignalKind::hangup())?),
111                (SignalId::Term, signal_stream(SignalKind::terminate())?),
112                (SignalId::Quit, signal_stream(SignalKind::quit())?),
113                (SignalId::Tstp, signal_stream(tstp_kind())?),
114                (SignalId::Cont, signal_stream(cont_kind())?),
115                (SignalId::Usr1, signal_stream(SignalKind::user_defined1())?),
116            ]);
117
118            if let Some(info_kind) = info_kind() {
119                map.insert(SignalId::Info, signal_stream(info_kind)?);
120            }
121
122            // This is a debug-only environment variable to let ctrl-\ (SIGQUIT)
123            // behave like SIGINFO. Useful for testing signal-based info queries
124            // on Linux.
125            let sigquit_as_info =
126                std::env::var("__NEXTEST_SIGQUIT_AS_INFO").is_ok_and(|v| v == "1");
127
128            Ok(Self {
129                map,
130                sigquit_as_info,
131            })
132        }
133
134        /// Creates a signal handler for debugger mode.
135        ///
136        /// SIGINT and SIGQUIT are set to SIG_IGN so nextest ignores them. The
137        /// debugger will also receive these signals and will handle them as
138        /// appropriate.
139        ///
140        /// SIGTSTP and SIGCONT are handled for internal bookkeeping (pausing/
141        /// resuming timers) but are not propagated to child processes.
142        pub(super) fn debugger_mode() -> io::Result<Self> {
143            use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
144
145            // Set SIGINT and SIGQUIT to SIG_IGN so nextest ignores them
146            // and they only affect the debugger process.
147            let ignore_action =
148                SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty());
149
150            unsafe {
151                let _ = sigaction(Signal::SIGINT, &ignore_action);
152                let _ = sigaction(Signal::SIGQUIT, &ignore_action);
153            }
154
155            let mut map = StreamMap::new();
156
157            // Set up termination signals and job control signals.
158            // Job control signals are handled for internal bookkeeping but not
159            // propagated to children.
160            map.extend([
161                (SignalId::Hup, signal_stream(SignalKind::hangup())?),
162                (SignalId::Term, signal_stream(SignalKind::terminate())?),
163                (SignalId::Tstp, signal_stream(tstp_kind())?),
164                (SignalId::Cont, signal_stream(cont_kind())?),
165            ]);
166
167            Ok(Self {
168                map,
169                sigquit_as_info: false,
170            })
171        }
172
173        pub(super) async fn recv(&mut self) -> Option<SignalEvent> {
174            self.map.next().await.map(|(id, _)| match id {
175                SignalId::Int => {
176                    SignalEvent::Shutdown(ShutdownEvent::Signal(ShutdownSignalEvent::Interrupt))
177                }
178                SignalId::Hup => {
179                    SignalEvent::Shutdown(ShutdownEvent::Signal(ShutdownSignalEvent::Hangup))
180                }
181                SignalId::Term => {
182                    SignalEvent::Shutdown(ShutdownEvent::Signal(ShutdownSignalEvent::Term))
183                }
184                SignalId::Quit => {
185                    if self.sigquit_as_info {
186                        SignalEvent::Info(SignalInfoEvent::Info)
187                    } else {
188                        SignalEvent::Shutdown(ShutdownEvent::Signal(ShutdownSignalEvent::Quit))
189                    }
190                }
191                SignalId::Tstp => SignalEvent::JobControl(JobControlEvent::Stop),
192                SignalId::Cont => SignalEvent::JobControl(JobControlEvent::Continue),
193                SignalId::Info => SignalEvent::Info(SignalInfoEvent::Info),
194                SignalId::Usr1 => SignalEvent::Info(SignalInfoEvent::Usr1),
195            })
196        }
197    }
198
199    fn signal_stream(kind: SignalKind) -> io::Result<SignalStream> {
200        Ok(SignalStream::new(signal(kind)?))
201    }
202
203    fn tstp_kind() -> SignalKind {
204        SignalKind::from_raw(libc::SIGTSTP)
205    }
206
207    fn cont_kind() -> SignalKind {
208        SignalKind::from_raw(libc::SIGCONT)
209    }
210
211    // The SIGINFO signal is available on many Unix platforms, but not all of
212    // them.
213    cfg_if::cfg_if! {
214        if #[cfg(any(
215            target_os = "dragonfly",
216            target_os = "freebsd",
217            target_os = "macos",
218            target_os = "netbsd",
219            target_os = "openbsd",
220            target_os = "illumos",
221        ))] {
222            fn info_kind() -> Option<SignalKind> {
223                Some(SignalKind::info())
224            }
225        } else {
226            fn info_kind() -> Option<SignalKind> {
227                None
228            }
229        }
230    }
231}
232
233#[cfg(windows)]
234mod imp {
235    use super::*;
236    use tokio::signal::windows::{CtrlC, ctrl_c};
237
238    #[derive(Debug)]
239    pub(super) struct Signals {
240        ctrl_c: CtrlC,
241        ctrl_c_done: bool,
242    }
243
244    impl Signals {
245        pub(super) fn new() -> std::io::Result<Self> {
246            let ctrl_c = ctrl_c()?;
247            Ok(Self {
248                ctrl_c,
249                ctrl_c_done: false,
250            })
251        }
252
253        /// Creates a signal handler for debugger mode.
254        /// On Windows, we don't handle Ctrl-C in debugger mode, allowing the debugger to handle it.
255        pub(super) fn debugger_mode() -> std::io::Result<Self> {
256            // Create a ctrl_c handler but mark it as done immediately,
257            // so recv() will always return None
258            let ctrl_c = ctrl_c()?;
259            Ok(Self {
260                ctrl_c,
261                ctrl_c_done: true,
262            })
263        }
264
265        pub(super) async fn recv(&mut self) -> Option<SignalEvent> {
266            if self.ctrl_c_done {
267                return None;
268            }
269
270            match self.ctrl_c.recv().await {
271                Some(()) => Some(SignalEvent::Shutdown(ShutdownEvent::Signal(
272                    ShutdownSignalEvent::Interrupt,
273                ))),
274                None => {
275                    self.ctrl_c_done = true;
276                    None
277                }
278            }
279        }
280    }
281}
282
283#[derive(Copy, Clone, Debug, Eq, PartialEq)]
284pub(crate) enum SignalEvent {
285    #[cfg(unix)]
286    JobControl(JobControlEvent),
287    Shutdown(ShutdownEvent),
288    #[cfg_attr(not(unix), expect(dead_code))]
289    Info(SignalInfoEvent),
290}
291
292// A job-control related signal event.
293#[derive(Copy, Clone, Debug, Eq, PartialEq)]
294pub(crate) enum JobControlEvent {
295    #[cfg(unix)]
296    Stop,
297    #[cfg(unix)]
298    Continue,
299}
300
301// A signal event that should cause a shutdown to happen.
302#[derive(Copy, Clone, Debug, Eq, PartialEq)]
303pub(crate) enum ShutdownSignalEvent {
304    #[cfg(unix)]
305    Hangup,
306    #[cfg(unix)]
307    Term,
308    #[cfg(unix)]
309    Quit,
310    Interrupt,
311}
312
313impl ShutdownSignalEvent {
314    #[cfg(test)]
315    pub(crate) const ALL_VARIANTS: &'static [Self] = &[
316        #[cfg(unix)]
317        Self::Hangup,
318        #[cfg(unix)]
319        Self::Term,
320        #[cfg(unix)]
321        Self::Quit,
322        Self::Interrupt,
323    ];
324}
325
326// An event that should cause a shutdown to happen.
327#[derive(Copy, Clone, Debug, Eq, PartialEq)]
328pub(crate) enum ShutdownEvent {
329    /// A signal was received from the OS.
330    Signal(ShutdownSignalEvent),
331    /// A test failure occurred with immediate termination mode.
332    TestFailureImmediate,
333}
334
335impl ShutdownEvent {
336    // On Unix, send SIGTERM for termination (global timeout, test failure with immediate mode).
337    #[cfg(unix)]
338    pub(crate) const TERMINATE: Self = Self::Signal(ShutdownSignalEvent::Term);
339
340    // On Windows, the best we can do is to interrupt the process.
341    #[cfg(not(unix))]
342    pub(crate) const TERMINATE: Self = Self::Signal(ShutdownSignalEvent::Interrupt);
343}
344
345// A signal event to query information about tests.
346#[derive(Copy, Clone, Debug, Eq, PartialEq)]
347pub(crate) enum SignalInfoEvent {
348    /// SIGUSR1
349    #[cfg(unix)]
350    Usr1,
351
352    /// SIGINFO
353    #[cfg(unix)]
354    Info,
355}