nextest_runner/
input.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Input handling for nextest.
5//!
6//! Similar to signal handling, input handling is read by the runner and used to control
7//! non-signal-related aspects of the test run. For example, "i" to print information about which
8//! tests are currently running.
9
10use crate::errors::DisplayErrorChain;
11use crossterm::event::{Event, EventStream, KeyCode};
12use futures::StreamExt;
13use std::sync::{Arc, Mutex};
14use thiserror::Error;
15use tracing::{debug, warn};
16
17/// The kind of input handling to set up for a test run.
18///
19/// An `InputHandlerKind` can be passed into
20/// [`TestRunnerBuilder::build`](crate::runner::TestRunnerBuilder::build).
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum InputHandlerKind {
23    /// The standard input handler, which reads from standard input.
24    Standard,
25
26    /// A no-op input handler. Useful for tests.
27    Noop,
28}
29
30impl InputHandlerKind {
31    pub(crate) fn build(self) -> InputHandler {
32        match self {
33            Self::Standard => InputHandler::new(),
34            Self::Noop => InputHandler::noop(),
35        }
36    }
37}
38
39/// The input handler implementation.
40#[derive(Debug)]
41pub(crate) struct InputHandler {
42    // A scope guard that ensures non-canonical mode is disabled when this is
43    // dropped, along with a stream to read events from.
44    imp: Option<(InputHandlerImpl, EventStream)>,
45}
46
47impl InputHandler {
48    const INFO_CHAR: char = 't';
49
50    /// Creates a new `InputHandler` that reads from standard input.
51    pub(crate) fn new() -> Self {
52        if imp::is_foreground_process() {
53            // Try enabling non-canonical mode.
54            match InputHandlerImpl::new() {
55                Ok(handler) => {
56                    let stream = EventStream::new();
57                    debug!("enabled terminal non-canonical mode, reading input events");
58                    Self {
59                        imp: Some((handler, stream)),
60                    }
61                }
62                Err(error) => {
63                    warn!(
64                        "failed to enable terminal non-canonical mode, \
65                         cannot read input events: {}",
66                        error,
67                    );
68                    Self::noop()
69                }
70            }
71        } else {
72            debug!(
73                "not reading input because nextest is not \
74                 a foreground process in a terminal"
75            );
76            Self::noop()
77        }
78    }
79
80    /// Creates a new `InputHandler` that does nothing.
81    pub(crate) fn noop() -> Self {
82        Self { imp: None }
83    }
84
85    pub(crate) fn status(&self) -> InputHandlerStatus {
86        if self.imp.is_some() {
87            InputHandlerStatus::Enabled {
88                info_char: Self::INFO_CHAR,
89            }
90        } else {
91            InputHandlerStatus::Disabled
92        }
93    }
94
95    /// Receives an event from the input, or None if the input is closed and there are no more
96    /// events.
97    ///
98    /// This is a cancel-safe operation.
99    pub(crate) async fn recv(&mut self) -> Option<InputEvent> {
100        let (_, stream) = self.imp.as_mut()?;
101        loop {
102            let next = stream.next().await?;
103            // Everything after here must be cancel-safe: ideally no await
104            // points at all, but okay with discarding `next` if there are any
105            // await points.
106            match next {
107                Ok(Event::Key(key)) => {
108                    if key.code == KeyCode::Char(Self::INFO_CHAR) && key.modifiers.is_empty() {
109                        return Some(InputEvent::Info);
110                    }
111                    if key.code == KeyCode::Enter {
112                        return Some(InputEvent::Enter);
113                    }
114                }
115                Ok(event) => {
116                    debug!("unhandled event: {:?}", event);
117                }
118                Err(error) => {
119                    warn!("failed to read input event: {}", error);
120                }
121            }
122        }
123    }
124
125    /// Suspends the input handler temporarily, restoring the original terminal
126    /// state.
127    ///
128    /// Used by the stop signal handler.
129    #[cfg(unix)]
130    pub(crate) fn suspend(&mut self) {
131        let Some((handler, _)) = self.imp.as_mut() else {
132            return;
133        };
134
135        if let Err(error) = handler.restore() {
136            warn!("failed to suspend terminal non-canonical mode: {}", error);
137            // Don't set imp to None -- we want to try to reinit() on resume.
138        }
139    }
140
141    /// Resumes the input handler after a suspension.
142    ///
143    /// Used by the continue signal handler.
144    #[cfg(unix)]
145    pub(crate) fn resume(&mut self) {
146        let Some((handler, _)) = self.imp.as_mut() else {
147            // None means that the input handler is disabled, so there is
148            // nothing to resume.
149            return;
150        };
151
152        if let Err(error) = handler.reinit() {
153            warn!(
154                "failed to resume terminal non-canonical mode, \
155                 cannot read input events: {}",
156                error
157            );
158            // Do set self.imp to None in this case -- we want to indicate to
159            // callers (e.g. via status()) that the input handler is disabled.
160            self.imp = None;
161        }
162    }
163}
164
165/// The status of the input handler, returned by
166/// [`TestRunner::input_handler_status`](crate::runner::TestRunner::input_handler_status).
167pub enum InputHandlerStatus {
168    /// The input handler is enabled.
169    Enabled {
170        /// The character that triggers the "info" event.
171        info_char: char,
172    },
173
174    /// The input handler is disabled.
175    Disabled,
176}
177
178#[derive(Clone, Debug)]
179struct InputHandlerImpl {
180    // `Arc<Mutex<_>>` for coordination between the drop handler and the panic
181    // hook.
182    guard: Arc<Mutex<imp::InputGuard>>,
183}
184
185impl InputHandlerImpl {
186    fn new() -> Result<Self, InputHandlerCreateError> {
187        let guard = imp::InputGuard::new().map_err(InputHandlerCreateError::EnableNonCanonical)?;
188
189        // At this point, the new terminal state is committed. Install a
190        // panic hook to restore the original state.
191        let ret = Self {
192            guard: Arc::new(Mutex::new(guard)),
193        };
194
195        let ret2 = ret.clone();
196        let panic_hook = std::panic::take_hook();
197        std::panic::set_hook(Box::new(move |info| {
198            // Ignore errors to avoid double-panicking.
199            if let Err(error) = ret2.restore() {
200                eprintln!(
201                    "failed to restore terminal state: {}",
202                    DisplayErrorChain::new(error)
203                );
204            }
205            panic_hook(info);
206        }));
207
208        Ok(ret)
209    }
210
211    #[cfg(unix)]
212    fn reinit(&self) -> Result<(), InputHandlerCreateError> {
213        // Make a new input guard and replace the old one. Don't set a new panic
214        // hook.
215        //
216        // The mutex is shared by the panic hook and self/the drop handler, so
217        // the change below will also be visible to the panic hook. But we
218        // acquire the mutex first to avoid a potential race where multiple
219        // calls to reinit() can happen concurrently.
220        //
221        // Also note that if this fails, the old InputGuard will be visible to
222        // the panic hook, which is fine -- since we called restore() first, the
223        // terminal state is already restored and guard is None.
224        let mut locked = self
225            .guard
226            .lock()
227            .map_err(|_| InputHandlerCreateError::Poisoned)?;
228        let guard = imp::InputGuard::new().map_err(InputHandlerCreateError::EnableNonCanonical)?;
229        *locked = guard;
230        Ok(())
231    }
232
233    fn restore(&self) -> Result<(), InputHandlerFinishError> {
234        // Do not panic here, in case a panic happened while the thread was
235        // locked. Instead, ignore the error.
236        let mut locked = self
237            .guard
238            .lock()
239            .map_err(|_| InputHandlerFinishError::Poisoned)?;
240        locked.restore().map_err(InputHandlerFinishError::Restore)
241    }
242}
243
244// Defense in depth -- use both the Drop impl (for regular drops and
245// panic=unwind) and a panic hook (for panic=abort).
246impl Drop for InputHandlerImpl {
247    fn drop(&mut self) {
248        if let Err(error) = self.restore() {
249            eprintln!(
250                "failed to restore terminal state: {}",
251                DisplayErrorChain::new(error)
252            );
253        }
254    }
255}
256
257#[derive(Debug, Error)]
258enum InputHandlerCreateError {
259    #[error("failed to enable terminal non-canonical mode")]
260    EnableNonCanonical(#[source] imp::Error),
261
262    #[cfg(unix)]
263    #[error("mutex was poisoned while reinitializing terminal state")]
264    Poisoned,
265}
266
267#[derive(Debug, Error)]
268enum InputHandlerFinishError {
269    #[error("mutex was poisoned while restoring terminal state")]
270    Poisoned,
271
272    #[error("failed to restore terminal state")]
273    Restore(#[source] imp::Error),
274}
275
276#[cfg(unix)]
277mod imp {
278    use libc::{ECHO, ICANON, TCSAFLUSH, TCSANOW, VMIN, VTIME, tcgetattr, tcsetattr};
279    use std::{
280        ffi::c_int,
281        io::{self, IsTerminal},
282        mem,
283        os::fd::AsRawFd,
284    };
285    use tracing::debug;
286
287    pub(super) type Error = io::Error;
288
289    pub(super) fn is_foreground_process() -> bool {
290        if !std::io::stdin().is_terminal() {
291            debug!("stdin is not a terminal => is_foreground_process() is false");
292            return false;
293        }
294
295        // Also check that tcgetpgrp is the same. If tcgetpgrp fails, it'll
296        // return -1 and this check will fail.
297        //
298        // See https://stackoverflow.com/a/2428429.
299        let pgrp = unsafe { libc::getpgrp() };
300        let tc_pgrp = unsafe { libc::tcgetpgrp(std::io::stdin().as_raw_fd()) };
301        if tc_pgrp == -1 {
302            debug!(
303                "stdin is a terminal, and pgrp = {pgrp}, but tcgetpgrp failed with error {} => \
304                 is_foreground_process() is false",
305                io::Error::last_os_error()
306            );
307            return false;
308        }
309        if pgrp != tc_pgrp {
310            debug!(
311                "stdin is a terminal, but pgrp {} != tcgetpgrp {} => is_foreground_process() is false",
312                pgrp, tc_pgrp
313            );
314            return false;
315        }
316
317        debug!(
318            "stdin is a terminal, and pgrp {pgrp} == tcgetpgrp {tc_pgrp} => \
319             is_foreground_process() is true"
320        );
321        true
322    }
323
324    /// A scope guard to enable non-canonical input mode on Unix platforms.
325    ///
326    /// Importantly, this does not enable the full raw mode that crossterm
327    /// provides -- that disables things like signal processing via the terminal
328    /// driver, which is unnecessary for our purposes. Here we only disable
329    /// options relevant to the input: echoing and canonical mode.
330    #[derive(Clone, Debug)]
331    pub(super) struct InputGuard {
332        // None indicates that the original state has been restored -- only one
333        // entity should do this.
334        //
335        // Note: originally, this used nix's termios support, but that was found
336        // to be buggy on illumos (lock up the terminal) -- apparently, not all
337        // bitflags were modeled. Using libc directly is more reliable.
338        original: Option<libc::termios>,
339    }
340
341    impl InputGuard {
342        pub(super) fn new() -> io::Result<Self> {
343            let TermiosPair { original, updated } = compute_termios()?;
344            stdin_tcsetattr(TCSAFLUSH, &updated)?;
345
346            Ok(Self {
347                original: Some(original),
348            })
349        }
350
351        pub(super) fn restore(&mut self) -> io::Result<()> {
352            if let Some(original) = self.original.take() {
353                stdin_tcsetattr(TCSANOW, &original)
354            } else {
355                Ok(())
356            }
357        }
358    }
359
360    fn compute_termios() -> io::Result<TermiosPair> {
361        let mut termios = mem::MaybeUninit::uninit();
362        let res = unsafe { tcgetattr(std::io::stdin().as_raw_fd(), termios.as_mut_ptr()) };
363        if res == -1 {
364            return Err(io::Error::last_os_error());
365        }
366
367        // SAFETY: if res is 0, then termios has been initialized.
368        let original = unsafe { termios.assume_init() };
369
370        let mut updated = original;
371
372        // Disable echoing inputs and canonical mode. We don't disable things like ISIG -- we
373        // handle that via the signal handler.
374        updated.c_lflag &= !(ECHO | ICANON);
375        // VMIN is 1 and VTIME is 0: this enables blocking reads of 1 byte
376        // at a time with no timeout. See
377        // https://linux.die.net/man/3/tcgetattr's "Canonical and
378        // noncanonical mode" section.
379        updated.c_cc[VMIN] = 1;
380        updated.c_cc[VTIME] = 0;
381
382        Ok(TermiosPair { original, updated })
383    }
384
385    #[derive(Clone, Debug)]
386    struct TermiosPair {
387        original: libc::termios,
388        updated: libc::termios,
389    }
390
391    fn stdin_tcsetattr(optional_actions: c_int, updated: &libc::termios) -> io::Result<()> {
392        let res = unsafe { tcsetattr(std::io::stdin().as_raw_fd(), optional_actions, updated) };
393        if res == -1 {
394            Err(io::Error::last_os_error())
395        } else {
396            Ok(())
397        }
398    }
399}
400
401#[cfg(windows)]
402mod imp {
403    use std::{
404        io::{self, IsTerminal},
405        os::windows::io::AsRawHandle,
406    };
407    use tracing::debug;
408    use windows_sys::Win32::System::Console::{
409        CONSOLE_MODE, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, GetConsoleMode, SetConsoleMode,
410    };
411
412    pub(super) type Error = io::Error;
413
414    pub(super) fn is_foreground_process() -> bool {
415        // Windows doesn't have a notion of foreground and background process
416        // groups: https://github.com/microsoft/terminal/issues/680. So simply
417        // checking that stdin is a terminal is enough.
418        //
419        // This function is written slightly non-idiomatically, because it
420        // follows the same structure as the more complex Unix function above.
421        if !std::io::stdin().is_terminal() {
422            debug!("stdin is not a terminal => is_foreground_process() is false");
423            return false;
424        }
425
426        debug!("stdin is a terminal => is_foreground_process() is true");
427        true
428    }
429
430    /// A scope guard to enable raw input mode on Windows.
431    ///
432    /// Importantly, this does not mask out `ENABLE_PROCESSED_INPUT` like
433    /// crossterm does -- that disables things like signal processing via the
434    /// terminal driver, which is unnecessary for our purposes. Here we only
435    /// disable options relevant to the input: `ENABLE_LINE_INPUT` and
436    /// `ENABLE_ECHO_INPUT`.
437    #[derive(Clone, Debug)]
438    pub(super) struct InputGuard {
439        original: Option<CONSOLE_MODE>,
440    }
441
442    impl InputGuard {
443        pub(super) fn new() -> io::Result<Self> {
444            let handle = std::io::stdin().as_raw_handle();
445
446            // Read the original console mode.
447            let mut original: CONSOLE_MODE = 0;
448            let res = unsafe { GetConsoleMode(handle, &mut original) };
449            if res == 0 {
450                return Err(io::Error::last_os_error());
451            }
452
453            // Mask out ENABLE_LINE_INPUT and ENABLE_ECHO_INPUT.
454            let updated = original & !(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
455
456            // Set the new console mode.
457            let res = unsafe { SetConsoleMode(handle, updated) };
458            if res == 0 {
459                return Err(io::Error::last_os_error());
460            }
461
462            Ok(Self {
463                original: Some(original),
464            })
465        }
466
467        pub(super) fn restore(&mut self) -> io::Result<()> {
468            if let Some(original) = self.original.take() {
469                let handle = std::io::stdin().as_raw_handle();
470                let res = unsafe { SetConsoleMode(handle, original) };
471                if res == 0 {
472                    Err(io::Error::last_os_error())
473                } else {
474                    Ok(())
475                }
476            } else {
477                Ok(())
478            }
479        }
480    }
481}
482
483#[derive(Copy, Clone, Debug, Eq, PartialEq)]
484pub(crate) enum InputEvent {
485    Info,
486    Enter,
487}