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    /// A no-op signal handler. Useful for tests.
19    Noop,
20}
21
22impl SignalHandlerKind {
23    pub(crate) fn build(self) -> Result<SignalHandler, SignalHandlerSetupError> {
24        match self {
25            Self::Standard => SignalHandler::new(),
26            Self::Noop => Ok(SignalHandler::noop()),
27        }
28    }
29}
30
31/// The signal handler implementation.
32#[derive(Debug)]
33pub(crate) struct SignalHandler {
34    signals: Option<imp::Signals>,
35}
36
37impl SignalHandler {
38    /// Creates a new `SignalHandler` that handles Ctrl-C and other signals.
39    #[cfg(any(unix, windows))]
40    pub(crate) fn new() -> Result<Self, SignalHandlerSetupError> {
41        let signals = imp::Signals::new()?;
42        Ok(Self {
43            signals: Some(signals),
44        })
45    }
46
47    /// Creates a new `SignalReceiver` that does nothing.
48    pub(crate) fn noop() -> Self {
49        Self { signals: None }
50    }
51
52    pub(crate) async fn recv(&mut self) -> Option<SignalEvent> {
53        match &mut self.signals {
54            Some(signals) => signals.recv().await,
55            None => None,
56        }
57    }
58}
59
60#[cfg(unix)]
61mod imp {
62    use super::*;
63    use std::io;
64    use tokio::signal::unix::{SignalKind, signal};
65    use tokio_stream::{StreamExt, StreamMap, wrappers::SignalStream};
66
67    #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
68    enum SignalId {
69        Int,
70        Hup,
71        Term,
72        Quit,
73        Tstp,
74        Cont,
75        Info,
76        Usr1,
77    }
78
79    /// Signals for SIGINT, SIGTERM and SIGHUP on Unix.
80    #[derive(Debug)]
81    pub(super) struct Signals {
82        // The number of streams is quite small, so a StreamMap (backed by a
83        // Vec) is a good option to store the list of streams to poll.
84        map: StreamMap<SignalId, SignalStream>,
85        sigquit_as_info: bool,
86    }
87
88    impl Signals {
89        pub(super) fn new() -> io::Result<Self> {
90            let mut map = StreamMap::new();
91
92            // Set up basic signals.
93            map.extend([
94                (SignalId::Int, signal_stream(SignalKind::interrupt())?),
95                (SignalId::Hup, signal_stream(SignalKind::hangup())?),
96                (SignalId::Term, signal_stream(SignalKind::terminate())?),
97                (SignalId::Quit, signal_stream(SignalKind::quit())?),
98                (SignalId::Tstp, signal_stream(tstp_kind())?),
99                (SignalId::Cont, signal_stream(cont_kind())?),
100                (SignalId::Usr1, signal_stream(SignalKind::user_defined1())?),
101            ]);
102
103            if let Some(info_kind) = info_kind() {
104                map.insert(SignalId::Info, signal_stream(info_kind)?);
105            }
106
107            // This is a debug-only environment variable to let ctrl-\ (SIGQUIT)
108            // behave like SIGINFO. Useful for testing signal-based info queries
109            // on Linux.
110            let sigquit_as_info =
111                std::env::var("__NEXTEST_SIGQUIT_AS_INFO").is_ok_and(|v| v == "1");
112
113            Ok(Self {
114                map,
115                sigquit_as_info,
116            })
117        }
118
119        pub(super) async fn recv(&mut self) -> Option<SignalEvent> {
120            self.map.next().await.map(|(id, _)| match id {
121                SignalId::Int => SignalEvent::Shutdown(ShutdownEvent::Interrupt),
122                SignalId::Hup => SignalEvent::Shutdown(ShutdownEvent::Hangup),
123                SignalId::Term => SignalEvent::Shutdown(ShutdownEvent::Term),
124                SignalId::Quit => {
125                    if self.sigquit_as_info {
126                        SignalEvent::Info(SignalInfoEvent::Info)
127                    } else {
128                        SignalEvent::Shutdown(ShutdownEvent::Quit)
129                    }
130                }
131                SignalId::Tstp => SignalEvent::JobControl(JobControlEvent::Stop),
132                SignalId::Cont => SignalEvent::JobControl(JobControlEvent::Continue),
133                SignalId::Info => SignalEvent::Info(SignalInfoEvent::Info),
134                SignalId::Usr1 => SignalEvent::Info(SignalInfoEvent::Usr1),
135            })
136        }
137    }
138
139    fn signal_stream(kind: SignalKind) -> io::Result<SignalStream> {
140        Ok(SignalStream::new(signal(kind)?))
141    }
142
143    fn tstp_kind() -> SignalKind {
144        SignalKind::from_raw(libc::SIGTSTP)
145    }
146
147    fn cont_kind() -> SignalKind {
148        SignalKind::from_raw(libc::SIGCONT)
149    }
150
151    // The SIGINFO signal is available on many Unix platforms, but not all of
152    // them.
153    cfg_if::cfg_if! {
154        if #[cfg(any(
155            target_os = "dragonfly",
156            target_os = "freebsd",
157            target_os = "macos",
158            target_os = "netbsd",
159            target_os = "openbsd",
160            target_os = "illumos",
161        ))] {
162            fn info_kind() -> Option<SignalKind> {
163                Some(SignalKind::info())
164            }
165        } else {
166            fn info_kind() -> Option<SignalKind> {
167                None
168            }
169        }
170    }
171}
172
173#[cfg(windows)]
174mod imp {
175    use super::*;
176    use tokio::signal::windows::{CtrlC, ctrl_c};
177
178    #[derive(Debug)]
179    pub(super) struct Signals {
180        ctrl_c: CtrlC,
181        ctrl_c_done: bool,
182    }
183
184    impl Signals {
185        pub(super) fn new() -> std::io::Result<Self> {
186            let ctrl_c = ctrl_c()?;
187            Ok(Self {
188                ctrl_c,
189                ctrl_c_done: false,
190            })
191        }
192
193        pub(super) async fn recv(&mut self) -> Option<SignalEvent> {
194            if self.ctrl_c_done {
195                return None;
196            }
197
198            match self.ctrl_c.recv().await {
199                Some(()) => Some(SignalEvent::Shutdown(ShutdownEvent::Interrupt)),
200                None => {
201                    self.ctrl_c_done = true;
202                    None
203                }
204            }
205        }
206    }
207}
208
209#[derive(Copy, Clone, Debug, Eq, PartialEq)]
210pub(crate) enum SignalEvent {
211    #[cfg(unix)]
212    JobControl(JobControlEvent),
213    Shutdown(ShutdownEvent),
214    #[cfg_attr(not(unix), expect(dead_code))]
215    Info(SignalInfoEvent),
216}
217
218// A job-control related signal event.
219#[derive(Copy, Clone, Debug, Eq, PartialEq)]
220pub(crate) enum JobControlEvent {
221    #[cfg(unix)]
222    Stop,
223    #[cfg(unix)]
224    Continue,
225}
226
227// A signal event that should cause a shutdown to happen.
228#[derive(Copy, Clone, Debug, Eq, PartialEq)]
229pub(crate) enum ShutdownEvent {
230    #[cfg(unix)]
231    Hangup,
232    #[cfg(unix)]
233    Term,
234    #[cfg(unix)]
235    Quit,
236    Interrupt,
237}
238
239impl ShutdownEvent {
240    #[cfg(test)]
241    pub(crate) const ALL_VARIANTS: &'static [Self] = &[
242        #[cfg(unix)]
243        Self::Hangup,
244        #[cfg(unix)]
245        Self::Term,
246        #[cfg(unix)]
247        Self::Quit,
248        Self::Interrupt,
249    ];
250}
251
252// A signal event to query information about tests.
253#[derive(Copy, Clone, Debug, Eq, PartialEq)]
254pub(crate) enum SignalInfoEvent {
255    /// SIGUSR1
256    #[cfg(unix)]
257    Usr1,
258
259    /// SIGINFO
260    #[cfg(unix)]
261    Info,
262}