Skip to main content

scope_cli/cli/
prompt.rs

1//! # Prompt input
2//!
3//! Reads the answer to an interactive prompt (the setup wizard, a token
4//! selection) from stdin, so that a cancel can stop the read.
5//!
6//! In the TUI, a suspend-mode command reads its prompts on the plain
7//! terminal. Ctrl-C there must cancel the command and return to the TUI. A
8//! blocking `read_line` cannot be stopped, and a read on another thread
9//! would stay behind and take the TUI's next keys. So the reader waits for
10//! input in short steps and checks `CANCEL` between them. It reads one
11//! byte at a time, so it never takes bytes after the newline.
12//!
13//! On Windows the console edits the line itself and hands it over whole,
14//! so the reader calls `ReadConsoleW` and waits for the line. Ctrl-C makes
15//! that read return empty with `ERROR_OPERATION_ABORTED` (std retries
16//! then, so a `read_line` would keep waiting); the reader takes it as the
17//! cancel.
18//!
19//! Outside the TUI nothing sets `CANCEL`, and the reader acts as
20//! `read_line` does.
21//!
22//! Every prompt goes through `ask` (or `pick`, for a numbered list).
23//! It pauses the progress bars, so a spinner never draws over a question.
24//! A question, a list to pick from and the text around a prompt are not
25//! data: they go to the diagnostic channel of the [`Output`]. So piped
26//! data stays clean, and a TUI run keeps only data for Ctrl-Y and `save`.
27
28use crate::cli::output::Output;
29use scope::error::ScopeError;
30use std::io::{self, BufRead};
31use std::sync::atomic::AtomicBool;
32#[cfg(any(unix, test))]
33use std::sync::atomic::Ordering;
34
35/// Set when a signal cancels the suspend-mode command. A prompt read that
36/// sees it stops with [`io::ErrorKind::Interrupted`]. The TUI clears it
37/// when a suspend-mode command starts.
38pub(crate) static CANCEL: AtomicBool = AtomicBool::new(false);
39
40/// How long the reader waits for input before it checks the cancel flag
41/// again. It is the longest delay between a cancel and the read's return.
42#[cfg(unix)]
43const POLL_INTERVAL_MS: libc::c_int = 100;
44
45/// The error a cancelled read returns.
46#[cfg(any(unix, windows))]
47fn cancelled() -> io::Error {
48    io::Error::new(io::ErrorKind::Interrupted, "cancelled")
49}
50
51/// Writes `question` to the diagnostic channel of `out` and reads one
52/// line of answer from `reader`. Every progress bar is paused from before
53/// the question is written until the answer is read, so no bar draws over
54/// it. The question may span lines. The channel is stderr on the CLI,
55/// which Rust does not buffer, so the question shows before the read.
56///
57/// # Errors
58///
59/// Returns the write or read error.
60pub(crate) fn ask(reader: &mut impl BufRead, out: &Output, question: &str) -> io::Result<String> {
61    crate::cli::progress::paused(|| {
62        crate::err!(out, "{question}")?;
63        let mut answer = String::new();
64        reader.read_line(&mut answer)?;
65        Ok(answer)
66    })
67}
68
69/// `ask` on stdin (cancellable in the TUI). The `scope` binary's own
70/// prompts use it.
71///
72/// # Errors
73///
74/// Returns the write or read error.
75pub fn ask_stdin(out: &Output, question: &str) -> io::Result<String> {
76    ask(&mut stdin(), out, question)
77}
78
79/// `ask` on stdin that waits at most `timeout` for the answer. Returns
80/// `None` when no answer came in time.
81///
82/// The read runs on a thread of its own, which stays behind, blocked on
83/// stdin, when the time runs out. Use it only as the process's last
84/// question (the exit question about the Ghola sidecar).
85///
86/// # Errors
87///
88/// Returns the write or read error.
89pub fn ask_timeout(
90    out: &Output,
91    question: &str,
92    timeout: std::time::Duration,
93) -> io::Result<Option<String>> {
94    crate::cli::progress::paused(|| {
95        crate::err!(out, "{question}")?;
96        let (tx, rx) = std::sync::mpsc::channel();
97        std::thread::spawn(move || {
98            let mut answer = String::new();
99            let read = stdin().read_line(&mut answer).map(|_| answer);
100            let _ = tx.send(read);
101        });
102        match rx.recv_timeout(timeout) {
103            Ok(read) => read.map(Some),
104            Err(_) => Ok(None),
105        }
106    })
107}
108
109/// Shows `list` (it ends with a newline), asks `Select <what> (1-<count>): `
110/// and returns the zero-based index of the answer.
111///
112/// # Errors
113///
114/// The write or read error; `Api` when the answer is not a number
115/// from 1 to `count`.
116pub(crate) fn pick(
117    reader: &mut impl BufRead,
118    out: &Output,
119    list: &str,
120    what: &str,
121    count: usize,
122) -> scope::error::Result<usize> {
123    let question = format!("{list}\nSelect {what} (1-{count}): ");
124    let answer = ask(reader, out, &question)?;
125    let n: usize = answer
126        .trim()
127        .parse()
128        .map_err(|_| ScopeError::Api("Invalid selection".to_string()))?;
129    if !(1..=count).contains(&n) {
130        return Err(ScopeError::Api(format!(
131            "Selection must be between 1 and {count}"
132        )));
133    }
134    Ok(n - 1)
135}
136
137/// The input for a prompt: stdin, cancellable with [`CANCEL`].
138#[cfg(unix)]
139pub(crate) fn stdin() -> PromptInput<'static> {
140    PromptInput::new(libc::STDIN_FILENO, &CANCEL)
141}
142
143/// The input for a prompt: stdin, cancellable by Ctrl-C at the console.
144#[cfg(windows)]
145pub(crate) fn stdin() -> console::ConsoleInput<'static> {
146    console::ConsoleInput::new(&CANCEL)
147}
148
149/// The input for a prompt: stdin. There is no cancel off unix and Windows.
150#[cfg(not(any(unix, windows)))]
151pub(crate) fn stdin() -> io::StdinLock<'static> {
152    io::stdin().lock()
153}
154
155#[cfg(windows)]
156mod console {
157    use super::cancelled;
158    use std::io::{self, BufRead, Read};
159    use std::sync::atomic::{AtomicBool, Ordering};
160    use windows_sys::Win32::Foundation::{ERROR_OPERATION_ABORTED, GetLastError, HANDLE};
161    use windows_sys::Win32::System::Console::{
162        GetConsoleMode, GetStdHandle, ReadConsoleW, STD_INPUT_HANDLE,
163    };
164
165    /// Prompt input on Windows. A console stdin is read with
166    /// `ReadConsoleW`, so Ctrl-C ends the read; any other stdin (a pipe,
167    /// a file) is read as std reads it.
168    pub(crate) struct ConsoleInput<'a> {
169        /// The console input handle; `None` when stdin is not a console.
170        console: Option<HANDLE>,
171        std: io::StdinLock<'static>,
172        cancel: &'a AtomicBool,
173        /// Bytes of a console read that the caller has not taken yet.
174        pending: Vec<u8>,
175    }
176
177    impl<'a> ConsoleInput<'a> {
178        pub(crate) fn new(cancel: &'a AtomicBool) -> Self {
179            // SAFETY: GetStdHandle takes no pointers; GetConsoleMode
180            // writes one u32 and fails on a handle that is not a console.
181            let console = unsafe {
182                let handle = GetStdHandle(STD_INPUT_HANDLE);
183                let mut mode = 0u32;
184                (GetConsoleMode(handle, &mut mode) != 0).then_some(handle)
185            };
186            Self {
187                console,
188                std: io::stdin().lock(),
189                cancel,
190                pending: Vec::new(),
191            }
192        }
193
194        /// Reads console input up to and including the end of a line into
195        /// `pending`. Leaves it empty at end of input (Ctrl-Z).
196        ///
197        /// # Errors
198        ///
199        /// `Interrupted` when Ctrl-C aborts the read or the cancel flag is
200        /// set; `InvalidData` when the input is not valid UTF-16; or the
201        /// read error.
202        fn read_console_line(&mut self, handle: HANDLE) -> io::Result<()> {
203            let mut line: Vec<u16> = Vec::new();
204            let mut buf = [0u16; 1024];
205            loop {
206                if self.cancel.load(Ordering::SeqCst) {
207                    return Err(cancelled());
208                }
209                let mut read = 0u32;
210                // SAFETY: the buffer is valid for `buf.len()` u16s, and
211                // ReadConsoleW writes at most that many and the count.
212                let ok = unsafe {
213                    ReadConsoleW(
214                        handle,
215                        buf.as_mut_ptr().cast(),
216                        buf.len() as u32,
217                        &mut read,
218                        std::ptr::null(),
219                    )
220                };
221                if ok == 0 {
222                    return Err(io::Error::last_os_error());
223                }
224                if read == 0 {
225                    // SAFETY: reads the calling thread's last error.
226                    if unsafe { GetLastError() } == ERROR_OPERATION_ABORTED {
227                        // Ctrl-C. The TUI's handler sets the flag too, but
228                        // maybe not yet, and run_suspended checks it.
229                        self.cancel.store(true, Ordering::SeqCst);
230                        return Err(cancelled());
231                    }
232                    break;
233                }
234                let got = &buf[..read as usize];
235                line.extend_from_slice(got);
236                // Ctrl-Z at the start of a line is end of input, as std
237                // reads it.
238                if line.first() == Some(&0x1a) {
239                    line.clear();
240                    break;
241                }
242                if got.last() == Some(&u16::from(b'\n')) {
243                    break;
244                }
245            }
246            let text = String::from_utf16(&line).map_err(|_| {
247                io::Error::new(
248                    io::ErrorKind::InvalidData,
249                    "console input was not valid UTF-16",
250                )
251            })?;
252            self.pending = text.into_bytes();
253            Ok(())
254        }
255    }
256
257    impl Read for ConsoleInput<'_> {
258        fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
259            let available = self.fill_buf()?;
260            let n = available.len().min(out.len());
261            out[..n].copy_from_slice(&available[..n]);
262            self.consume(n);
263            Ok(n)
264        }
265    }
266
267    impl BufRead for ConsoleInput<'_> {
268        /// A cancel returns `Other`, not `Interrupted`: the std helpers
269        /// over `fill_buf` retry on `Interrupted`, so they would never
270        /// stop.
271        fn fill_buf(&mut self) -> io::Result<&[u8]> {
272            let Some(handle) = self.console else {
273                return self.std.fill_buf();
274            };
275            if self.pending.is_empty() {
276                match self.read_console_line(handle) {
277                    Err(e) if e.kind() == io::ErrorKind::Interrupted => {
278                        return Err(io::Error::other(e));
279                    }
280                    other => other?,
281                }
282            }
283            Ok(&self.pending)
284        }
285
286        fn consume(&mut self, amt: usize) {
287            if self.console.is_none() {
288                self.std.consume(amt);
289            } else {
290                self.pending.drain(..amt.min(self.pending.len()));
291            }
292        }
293
294        /// Reads one line, as `BufRead::read_line` does, and appends it
295        /// to `buf`. Returns the number of bytes read; 0 at end of input.
296        ///
297        /// # Errors
298        ///
299        /// `Interrupted` when Ctrl-C cancels the read; or the read error.
300        fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
301            let Some(handle) = self.console else {
302                return self.std.read_line(buf);
303            };
304            if self.pending.is_empty() {
305                self.read_console_line(handle)?;
306            }
307            let end = self
308                .pending
309                .iter()
310                .position(|&b| b == b'\n')
311                .map_or(self.pending.len(), |i| i + 1);
312            let line: Vec<u8> = self.pending.drain(..end).collect();
313            let text = String::from_utf8(line).expect("decoded from UTF-16");
314            buf.push_str(&text);
315            Ok(text.len())
316        }
317    }
318}
319
320/// A line reader over a file descriptor that stops when `cancel` is set.
321#[cfg(unix)]
322pub(crate) struct PromptInput<'a> {
323    fd: std::os::fd::RawFd,
324    cancel: &'a AtomicBool,
325    /// The byte `fill_buf` read and `consume` did not take yet.
326    byte: [u8; 1],
327    has_byte: bool,
328}
329
330#[cfg(unix)]
331impl<'a> PromptInput<'a> {
332    pub(crate) fn new(fd: std::os::fd::RawFd, cancel: &'a AtomicBool) -> Self {
333        Self {
334            fd,
335            cancel,
336            byte: [0],
337            has_byte: false,
338        }
339    }
340
341    /// Reads one byte. `None` at end of input. Waits in steps of
342    /// [`POLL_INTERVAL_MS`] and stops with `Interrupted` when `cancel` is
343    /// set.
344    fn read_byte(&mut self) -> io::Result<Option<u8>> {
345        if std::mem::take(&mut self.has_byte) {
346            return Ok(Some(self.byte[0]));
347        }
348        loop {
349            if self.cancel.load(Ordering::SeqCst) {
350                return Err(cancelled());
351            }
352            let mut pfd = libc::pollfd {
353                fd: self.fd,
354                events: libc::POLLIN,
355                revents: 0,
356            };
357            // SAFETY: one valid pollfd, and poll writes only its revents.
358            let ready = unsafe { libc::poll(&mut pfd, 1, POLL_INTERVAL_MS) };
359            if ready < 0 {
360                let e = io::Error::last_os_error();
361                if e.kind() == io::ErrorKind::Interrupted {
362                    continue;
363                }
364                return Err(e);
365            }
366            if ready == 0 {
367                continue;
368            }
369            // Input, end of input or an error: the read tells which.
370            let mut b = 0u8;
371            // SAFETY: reads at most one byte into a valid one-byte buffer.
372            let n = unsafe { libc::read(self.fd, (&mut b as *mut u8).cast(), 1) };
373            match n {
374                1 => return Ok(Some(b)),
375                0 => return Ok(None),
376                _ => {
377                    let e = io::Error::last_os_error();
378                    if matches!(
379                        e.kind(),
380                        io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
381                    ) {
382                        continue;
383                    }
384                    return Err(e);
385                }
386            }
387        }
388    }
389}
390
391#[cfg(unix)]
392impl io::Read for PromptInput<'_> {
393    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
394        let Some(first) = buf.first_mut() else {
395            return Ok(0);
396        };
397        Ok(match self.read_byte()? {
398            Some(b) => {
399                *first = b;
400                1
401            }
402            None => 0,
403        })
404    }
405}
406
407#[cfg(unix)]
408impl BufRead for PromptInput<'_> {
409    /// The prompts use `read_line`; this exists because `BufRead` needs
410    /// it. A cancel returns `Other`, not `Interrupted`: the std helpers
411    /// over `fill_buf` retry on `Interrupted`, so they would never stop.
412    fn fill_buf(&mut self) -> io::Result<&[u8]> {
413        if !self.has_byte {
414            match self.read_byte() {
415                Ok(Some(b)) => {
416                    self.byte[0] = b;
417                    self.has_byte = true;
418                }
419                Ok(None) => {}
420                Err(e) if e.kind() == io::ErrorKind::Interrupted => {
421                    return Err(io::Error::other(e));
422                }
423                Err(e) => return Err(e),
424            }
425        }
426        Ok(&self.byte[..usize::from(self.has_byte)])
427    }
428
429    fn consume(&mut self, amt: usize) {
430        if amt > 0 {
431            self.has_byte = false;
432        }
433    }
434
435    /// Reads up to and including the newline, as `BufRead::read_line`
436    /// does, and appends it to `buf`. Returns the number of bytes read; 0
437    /// at end of input.
438    ///
439    /// # Errors
440    ///
441    /// `Interrupted` when the cancel flag is set; `InvalidData` when the
442    /// line is not UTF-8 (`buf` is then unchanged); or the read error.
443    fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
444        let mut line = Vec::new();
445        while let Some(b) = self.read_byte()? {
446            line.push(b);
447            if b == b'\n' {
448                break;
449            }
450        }
451        let text = String::from_utf8(line).map_err(|_| {
452            io::Error::new(
453                io::ErrorKind::InvalidData,
454                "stream did not contain valid UTF-8",
455            )
456        })?;
457        buf.push_str(&text);
458        Ok(text.len())
459    }
460}
461
462#[cfg(test)]
463mod ask_tests {
464    use super::*;
465    use crate::cli::progress::testing::{REGISTRY_LOCK, Screen, drawing_spinner};
466    use std::io::Write;
467    use std::sync::Arc;
468    use std::sync::atomic::AtomicUsize;
469    use std::time::Duration;
470
471    /// An answer that takes a while to type. It notes how many draws the
472    /// screen had when the answer came.
473    struct SlowAnswer<'a> {
474        screen: &'a Screen,
475        draws_at_answer: Option<usize>,
476    }
477
478    impl io::Read for SlowAnswer<'_> {
479        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
480            if self.draws_at_answer.is_some() {
481                return Ok(0);
482            }
483            std::thread::sleep(Duration::from_millis(40));
484            self.draws_at_answer = Some(self.screen.ops().len());
485            buf[..2].copy_from_slice(b"y\n");
486            Ok(2)
487        }
488    }
489
490    /// A diagnostic channel that notes how many draws the screen had when
491    /// the question was written.
492    struct Question {
493        screen: Screen,
494        draws_at_question: Arc<AtomicUsize>,
495    }
496
497    impl Write for Question {
498        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
499            self.draws_at_question
500                .store(self.screen.ops().len(), Ordering::SeqCst);
501            Ok(buf.len())
502        }
503        fn flush(&mut self) -> io::Result<()> {
504            Ok(())
505        }
506    }
507
508    #[test]
509    fn test_ask_keeps_a_ticking_spinner_off_the_question() {
510        // crawl by symbol: the "Searching..." spinner drew over "Save this
511        // token for future use? [y/N]" until the answer came.
512        let _lock = REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner());
513        let screen = Screen::default();
514        let sp = drawing_spinner("Searching...", &Output::capture().0, &screen);
515        std::thread::sleep(Duration::from_millis(30));
516        let mut answer = SlowAnswer {
517            screen: &screen,
518            draws_at_answer: None,
519        };
520        let mut reader = io::BufReader::new(&mut answer);
521        let draws_at_question = Arc::new(AtomicUsize::new(0));
522        let out = Output::from_writers(
523            Vec::new(),
524            Question {
525                screen: screen.clone(),
526                draws_at_question: draws_at_question.clone(),
527            },
528        );
529        let got = ask(&mut reader, &out, "Save? [y/N]: ").unwrap();
530        drop(reader);
531        assert_eq!(got, "y\n");
532        assert_eq!(
533            answer.draws_at_answer,
534            Some(draws_at_question.load(Ordering::SeqCst)),
535            "the spinner drew between the question and the answer"
536        );
537        sp.finish_and_clear();
538    }
539
540    #[test]
541    fn test_a_prompt_never_reaches_the_data_channel() {
542        // `scope crawl USDC > out.json`: the question must show on the
543        // terminal and stay out of the file.
544        let (out, cap) = Output::capture();
545        let got = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "Save? [y/N]: ").unwrap();
546        assert_eq!(got, "y\n");
547        assert_eq!(cap.out(), "");
548        assert_eq!(cap.err(), "Save? [y/N]: ");
549    }
550
551    #[test]
552    fn test_ask_returns_the_write_error() {
553        let out = Output::from_writers(Vec::new(), crate::cli::output::tests::ClosedPipe);
554        let e = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "q? ").unwrap_err();
555        assert_eq!(e.kind(), io::ErrorKind::BrokenPipe);
556    }
557
558    #[test]
559    fn test_pick_returns_the_index_and_rejects_bad_answers() {
560        let pick_with = |answer: &str| {
561            let (out, cap) = Output::capture();
562            let got = pick(
563                &mut io::Cursor::new(answer.as_bytes().to_vec()),
564                &out,
565                "1 a\n2 b\n",
566                "token",
567                2,
568            );
569            assert_eq!(cap.err(), "1 a\n2 b\n\nSelect token (1-2): ");
570            assert_eq!(cap.out(), "", "the list is not data");
571            got.map_err(|e| e.to_string())
572        };
573        assert_eq!(pick_with("2\n"), Ok(1));
574        assert!(pick_with("x\n").unwrap_err().contains("Invalid selection"));
575        assert!(
576            pick_with("3\n")
577                .unwrap_err()
578                .contains("Selection must be between 1 and 2")
579        );
580        assert!(
581            pick_with("0\n")
582                .unwrap_err()
583                .contains("Selection must be between 1 and 2")
584        );
585    }
586}
587
588#[cfg(all(test, unix))]
589mod tests {
590    use super::*;
591    use std::io::Read;
592    use std::os::fd::RawFd;
593    use std::time::{Duration, Instant};
594
595    /// A pipe: (read end, write end). Closed on drop.
596    struct Pipe(RawFd, RawFd);
597
598    impl Pipe {
599        fn new() -> Self {
600            let mut fds = [0; 2];
601            // SAFETY: pipe writes two fds into a valid two-element array.
602            assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
603            Pipe(fds[0], fds[1])
604        }
605
606        fn write(&self, bytes: &[u8]) {
607            // SAFETY: writes from a valid buffer of the given length.
608            let n = unsafe { libc::write(self.1, bytes.as_ptr().cast(), bytes.len()) };
609            assert_eq!(n, bytes.len() as isize);
610        }
611
612        fn close_write(&mut self) {
613            // SAFETY: closes the write end once; -1 marks it closed.
614            unsafe { libc::close(self.1) };
615            self.1 = -1;
616        }
617
618        /// What is left in the pipe, read without the prompt reader.
619        fn rest(&mut self) -> Vec<u8> {
620            self.close_write();
621            let mut out = Vec::new();
622            let mut b = [0u8; 64];
623            loop {
624                // SAFETY: reads into a valid buffer of the given length.
625                let n = unsafe { libc::read(self.0, b.as_mut_ptr().cast(), b.len()) };
626                if n <= 0 {
627                    return out;
628                }
629                out.extend_from_slice(&b[..n as usize]);
630            }
631        }
632    }
633
634    impl Drop for Pipe {
635        fn drop(&mut self) {
636            for fd in [self.0, self.1] {
637                if fd >= 0 {
638                    // SAFETY: each open fd is closed once.
639                    unsafe { libc::close(fd) };
640                }
641            }
642        }
643    }
644
645    #[test]
646    fn test_reads_one_line_and_leaves_the_rest() {
647        // The next prompt reads the next line: the reader must not take
648        // bytes after the newline.
649        let flag = AtomicBool::new(false);
650        let mut pipe = Pipe::new();
651        pipe.write(b"yes\nnext\n");
652        let mut input = PromptInput::new(pipe.0, &flag);
653        let mut line = String::from("old:");
654        assert_eq!(input.read_line(&mut line).unwrap(), 4);
655        assert_eq!(line, "old:yes\n", "appends, as read_line does");
656        assert_eq!(pipe.rest(), b"next\n");
657    }
658
659    #[test]
660    fn test_end_of_input_returns_the_partial_line_then_zero() {
661        // A closed stdin must end the prompt as read_line does, not hang.
662        let flag = AtomicBool::new(false);
663        let mut pipe = Pipe::new();
664        pipe.write(b"abc");
665        pipe.close_write();
666        let mut input = PromptInput::new(pipe.0, &flag);
667        let mut line = String::new();
668        assert_eq!(input.read_line(&mut line).unwrap(), 3);
669        assert_eq!(line, "abc");
670        assert_eq!(input.read_line(&mut line).unwrap(), 0);
671    }
672
673    #[test]
674    fn test_invalid_utf8_is_an_error_and_leaves_the_buffer() {
675        let flag = AtomicBool::new(false);
676        let pipe = Pipe::new();
677        pipe.write(b"\xff\n");
678        let mut input = PromptInput::new(pipe.0, &flag);
679        let mut line = String::from("kept");
680        let e = input.read_line(&mut line).unwrap_err();
681        assert_eq!(e.kind(), io::ErrorKind::InvalidData);
682        assert_eq!(line, "kept");
683    }
684
685    #[test]
686    fn test_cancel_stops_a_waiting_read_and_takes_no_later_input() {
687        // Ctrl-C at a prompt in the TUI: the read must stop soon, and no
688        // reader may stay behind to take the TUI's next keys.
689        let flag = AtomicBool::new(false);
690        let mut pipe = Pipe::new();
691        let started = Instant::now();
692        let e = std::thread::scope(|s| {
693            s.spawn(|| {
694                std::thread::sleep(Duration::from_millis(150));
695                flag.store(true, Ordering::SeqCst);
696            });
697            let mut line = String::new();
698            PromptInput::new(pipe.0, &flag)
699                .read_line(&mut line)
700                .unwrap_err()
701        });
702        assert_eq!(e.kind(), io::ErrorKind::Interrupted);
703        assert!(started.elapsed() < Duration::from_secs(1), "stopped late");
704        pipe.write(b"k");
705        std::thread::sleep(Duration::from_millis(2 * POLL_INTERVAL_MS as u64));
706        assert_eq!(pipe.rest(), b"k", "the key after the cancel is still there");
707    }
708
709    #[test]
710    fn test_a_set_flag_stops_the_read_before_it_waits() {
711        let flag = AtomicBool::new(true);
712        let mut pipe = Pipe::new();
713        pipe.write(b"y\n");
714        let mut line = String::new();
715        let e = PromptInput::new(pipe.0, &flag)
716            .read_line(&mut line)
717            .unwrap_err();
718        assert_eq!(e.kind(), io::ErrorKind::Interrupted);
719        assert_eq!(pipe.rest(), b"y\n");
720    }
721
722    #[test]
723    fn test_read_and_fill_buf_take_one_byte_at_a_time() {
724        let flag = AtomicBool::new(false);
725        let mut pipe = Pipe::new();
726        pipe.write(b"ab");
727        let mut input = PromptInput::new(pipe.0, &flag);
728        assert_eq!(input.read(&mut []).unwrap(), 0);
729        assert_eq!(input.fill_buf().unwrap(), b"a");
730        assert_eq!(input.fill_buf().unwrap(), b"a", "not consumed yet");
731        let mut buf = [0u8; 4];
732        assert_eq!(input.read(&mut buf).unwrap(), 1, "the buffered byte");
733        assert_eq!(buf[0], b'a');
734        input.consume(0);
735        assert_eq!(input.fill_buf().unwrap(), b"b");
736        input.consume(1);
737        assert_eq!(pipe.rest(), b"");
738        assert_eq!(input.fill_buf().unwrap(), b"", "end of input");
739        assert_eq!(input.read(&mut buf).unwrap(), 0);
740    }
741
742    #[test]
743    fn test_fill_buf_cancel_is_not_interrupted() {
744        // std's read_until retries Interrupted forever; fill_buf must not
745        // return it.
746        let flag = AtomicBool::new(true);
747        let pipe = Pipe::new();
748        let mut input = PromptInput::new(pipe.0, &flag);
749        let e = input.fill_buf().unwrap_err();
750        assert_eq!(e.kind(), io::ErrorKind::Other);
751        let mut v = Vec::new();
752        assert!(input.read_until(b'\n', &mut v).is_err(), "no endless retry");
753    }
754
755    #[test]
756    fn test_a_read_error_is_returned() {
757        // A directory polls as ready, and reading it fails (EISDIR).
758        let flag = AtomicBool::new(false);
759        // SAFETY: a valid NUL-terminated path; the fd is closed below.
760        let fd = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY) };
761        assert!(fd >= 0);
762        let mut line = String::new();
763        let r = PromptInput::new(fd, &flag).read_line(&mut line);
764        // SAFETY: closes the fd opened above, once.
765        unsafe { libc::close(fd) };
766        assert!(r.is_err());
767        assert_ne!(r.unwrap_err().kind(), io::ErrorKind::Interrupted);
768    }
769
770    #[test]
771    fn test_stdin_reads_from_the_process_cancel_flag() {
772        let input = stdin();
773        assert_eq!(input.fd, libc::STDIN_FILENO);
774        assert!(std::ptr::eq(input.cancel, &CANCEL));
775    }
776    #[test]
777    fn test_fill_buf_returns_a_read_error_as_it_is() {
778        // Only a cancel becomes `Other`; EISDIR comes back unchanged.
779        let flag = AtomicBool::new(false);
780        // SAFETY: a valid NUL-terminated path; the fd is closed below.
781        let fd = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY) };
782        assert!(fd >= 0);
783        let r = PromptInput::new(fd, &flag).fill_buf().map(<[u8]>::to_vec);
784        // SAFETY: closes the fd opened above, once.
785        unsafe { libc::close(fd) };
786        assert_eq!(r.unwrap_err().raw_os_error(), Some(libc::EISDIR));
787    }
788
789    extern "C" fn ignore_signal(_: libc::c_int) {}
790
791    #[test]
792    fn test_a_signal_during_the_wait_does_not_end_the_read() {
793        // A signal (SIGWINCH from a resize, say) makes poll fail with
794        // EINTR. The reader must keep waiting, not give up the prompt.
795        // SAFETY: installs a handler that does nothing, without
796        // SA_RESTART, so a blocked poll returns EINTR.
797        unsafe {
798            let mut action: libc::sigaction = std::mem::zeroed();
799            action.sa_sigaction = ignore_signal as extern "C" fn(libc::c_int) as usize;
800            libc::sigemptyset(&mut action.sa_mask);
801            assert_eq!(
802                libc::sigaction(libc::SIGUSR1, &action, std::ptr::null_mut()),
803                0
804            );
805        }
806        let flag = AtomicBool::new(false);
807        let pipe = Pipe::new();
808        let fd = pipe.0;
809        let (tx, rx) = std::sync::mpsc::channel();
810        let line = std::thread::scope(|s| {
811            let reader = s.spawn(|| {
812                // SAFETY: pthread_self has no preconditions.
813                tx.send(unsafe { libc::pthread_self() }).unwrap();
814                let mut line = String::new();
815                PromptInput::new(fd, &flag)
816                    .read_line(&mut line)
817                    .map(|_| line)
818            });
819            let thread = rx.recv().unwrap();
820            for _ in 0..10 {
821                std::thread::sleep(Duration::from_millis(20));
822                // SAFETY: the reader thread is alive until it reads the
823                // line, which is written only after the last signal.
824                assert_eq!(unsafe { libc::pthread_kill(thread, libc::SIGUSR1) }, 0);
825            }
826            pipe.write(b"still here\n");
827            reader.join().unwrap()
828        });
829        assert_eq!(line.unwrap(), "still here\n");
830    }
831
832    /// Puts a pipe on fd 0 for the test and puts the real stdin back on
833    /// drop. Holds `DIAG_LOCK`, so no other test reads stdin meanwhile.
834    struct PipedStdin {
835        pipe: Pipe,
836        saved: RawFd,
837        _lock: tokio::sync::MutexGuard<'static, ()>,
838    }
839
840    impl PipedStdin {
841        fn new() -> Self {
842            let lock = crate::cli::tui::DIAG_LOCK.blocking_lock();
843            let pipe = Pipe::new();
844            // SAFETY: dup and dup2 on open fds; `saved` is restored and
845            // closed on drop.
846            let saved = unsafe { libc::dup(libc::STDIN_FILENO) };
847            assert!(saved >= 0);
848            assert_eq!(unsafe { libc::dup2(pipe.0, libc::STDIN_FILENO) }, 0);
849            Self {
850                pipe,
851                saved,
852                _lock: lock,
853            }
854        }
855
856        /// Bytes still waiting in the pipe.
857        fn unread(&self) -> usize {
858            let mut n: libc::c_int = 0;
859            // SAFETY: FIONREAD writes one c_int.
860            assert_eq!(
861                unsafe { libc::ioctl(self.pipe.0, libc::FIONREAD, &mut n) },
862                0
863            );
864            n as usize
865        }
866    }
867
868    impl Drop for PipedStdin {
869        fn drop(&mut self) {
870            // SAFETY: restores the stdin saved in `new` and closes the copy.
871            unsafe {
872                libc::dup2(self.saved, libc::STDIN_FILENO);
873                libc::close(self.saved);
874            }
875        }
876    }
877
878    #[test]
879    fn test_ask_stdin_asks_on_the_diagnostic_channel_and_reads_stdin() {
880        let stdin = PipedStdin::new();
881        stdin.pipe.write(b"yes\nnext\n");
882        let (out, cap) = Output::capture();
883        assert_eq!(ask_stdin(&out, "Continue? ").unwrap(), "yes\n");
884        assert_eq!(cap.err(), "Continue? ");
885        assert_eq!(cap.out(), "");
886        assert_eq!(
887            stdin.unread(),
888            5,
889            "the next line is left for the next prompt"
890        );
891    }
892
893    #[test]
894    fn test_ask_timeout_returns_the_answer_or_none_when_time_runs_out() {
895        let stdin = PipedStdin::new();
896        let (out, cap) = Output::capture();
897
898        stdin.pipe.write(b"n\n");
899        let got = ask_timeout(&out, "Stop Ghola? ", Duration::from_secs(5)).unwrap();
900        assert_eq!(got.as_deref(), Some("n\n"));
901        assert_eq!(cap.err(), "Stop Ghola? ");
902
903        let started = Instant::now();
904        let got = ask_timeout(&out, "Again? ", Duration::from_millis(50)).unwrap();
905        assert_eq!(got, None);
906        assert!(
907            started.elapsed() < Duration::from_secs(2),
908            "waited too long"
909        );
910        assert_eq!(cap.err(), "Stop Ghola? Again? ");
911
912        // The read left behind takes this line; wait for it before the
913        // real stdin goes back on fd 0.
914        stdin.pipe.write(b"late\n");
915        let deadline = Instant::now() + Duration::from_secs(5);
916        while stdin.unread() > 0 && Instant::now() < deadline {
917            std::thread::sleep(Duration::from_millis(10));
918        }
919        assert_eq!(stdin.unread(), 0);
920    }
921}