Skip to main content

proc_cli/ui/
out.rs

1//! Every byte proc writes to stdout or stderr goes through this module.
2//!
3//! `println!` and friends panic when the write fails, and Rust ignores SIGPIPE,
4//! so a reader that stops early (`proc list | head -n1`) used to end proc with
5//! a panic and exit 101, even when the command had just killed something. The
6//! policy lives here, in one place:
7//!
8//! - **A broken pipe** means the reader is gone. Writing stops, quietly, on
9//!   that stream, and the command goes on to the end and exits with the code
10//!   its own result calls for: a `kill` that worked exits 0, a not-found exits
11//!   2. A broken pipe on stderr changes nothing either.
12//! - **Any other write error** (a full disk, a file size limit, an I/O error)
13//!   is a failure: nothing more is written to that stream, and [`finish`] turns
14//!   an exit 0 into exit 1 and says why on stderr, if stderr still works. A
15//!   command that already failed keeps its own, more specific exit code.
16//! - Nothing here panics, and nothing writes about a broken pipe.
17//!
18//! The commands print through [`out_println!`](crate::out_println!),
19//! [`out_print!`](crate::out_print!), [`err_println!`](crate::err_println!) and
20//! [`err_print!`](crate::err_print!), or through the writers [`stdout`] and
21//! [`stderr`]. The failure is recorded on the stream when it happens, so a
22//! deep helper does not need to pass an error up through every caller; `main`
23//! reads it back once, in [`finish`], and unwinds normally (destructors and
24//! terminal restore still run). `clippy.toml` bans the std print macros and
25//! `std::io::stdout`/`stderr` everywhere else, so a new direct print fails
26//! `cargo clippy -- -D warnings`.
27
28// This module is the one place that may touch the std streams.
29#![allow(clippy::disallowed_macros, clippy::disallowed_methods)]
30
31use std::cell::Cell;
32use std::fmt;
33use std::io::{self, ErrorKind, IsTerminal, Write};
34use std::sync::Mutex;
35use std::time::Duration;
36
37/// What has gone wrong on one stream so far
38#[derive(Default)]
39struct State {
40    /// The reader is gone (`EPIPE`)
41    broken: bool,
42    /// The first other write error: its kind and message
43    failure: Option<(ErrorKind, String)>,
44}
45
46/// One stream's health. Once a write has failed, later writes fail at once
47/// without touching the stream, so a command that keeps printing costs nothing.
48struct Channel {
49    state: Mutex<State>,
50}
51
52impl Channel {
53    const fn new() -> Self {
54        Channel {
55            state: Mutex::new(State {
56                broken: false,
57                failure: None,
58            }),
59        }
60    }
61
62    fn lock(&self) -> std::sync::MutexGuard<'_, State> {
63        // A poisoned lock only means another thread panicked while holding it;
64        // the state is plain data and still right
65        self.state
66            .lock()
67            .unwrap_or_else(std::sync::PoisonError::into_inner)
68    }
69
70    /// Run a write on the stream, unless an earlier one already failed, and
71    /// remember how it went
72    fn run<T>(&self, write: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
73        {
74            let state = self.lock();
75            if state.broken {
76                return Err(ErrorKind::BrokenPipe.into());
77            }
78            if let Some((kind, message)) = &state.failure {
79                return Err(io::Error::new(*kind, message.clone()));
80            }
81        }
82        let result = write();
83        if let Err(err) = &result {
84            let mut state = self.lock();
85            if err.kind() == ErrorKind::BrokenPipe {
86                state.broken = true;
87            } else if state.failure.is_none() {
88                state.failure = Some((err.kind(), err.to_string()));
89            }
90        }
91        result
92    }
93
94    /// Whether nothing has gone wrong on the stream so far: the reader is
95    /// still there and no write has failed
96    fn usable(&self) -> bool {
97        let state = self.lock();
98        !state.broken && state.failure.is_none()
99    }
100
101    /// The message of the first write error that was not a broken pipe
102    fn failure(&self) -> Option<String> {
103        self.lock()
104            .failure
105            .as_ref()
106            .map(|(_, message)| message.clone())
107    }
108}
109
110static STDOUT: Channel = Channel::new();
111static STDERR: Channel = Channel::new();
112
113/// Which stream a [`Out`] writes to
114#[derive(Clone, Copy)]
115enum Stream {
116    Stdout,
117    Stderr,
118}
119
120/// A writer for stdout or stderr that applies the module's policy. Cheap to
121/// make; make one wherever a `Write` is wanted.
122#[derive(Clone, Copy)]
123pub struct Out {
124    stream: Stream,
125}
126
127impl Out {
128    fn channel(self) -> &'static Channel {
129        match self.stream {
130            Stream::Stdout => &STDOUT,
131            Stream::Stderr => &STDERR,
132        }
133    }
134}
135
136impl Write for Out {
137    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
138        self.write_all(buf).map(|()| buf.len())
139    }
140
141    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
142        self.channel().run(|| match self.stream {
143            Stream::Stdout => io::stdout().write_all(buf),
144            Stream::Stderr => io::stderr().write_all(buf),
145        })
146    }
147
148    fn flush(&mut self) -> io::Result<()> {
149        self.channel().run(|| match self.stream {
150            Stream::Stdout => io::stdout().flush(),
151            Stream::Stderr => io::stderr().flush(),
152        })
153    }
154}
155
156thread_local! {
157    /// Set while [`on_stderr`] runs: what this thread prints to stdout goes to stderr
158    static STDOUT_ON_STDERR: Cell<bool> = const { Cell::new(false) };
159}
160
161/// The writer for stdout (or for stderr, while [`on_stderr`] runs)
162pub fn stdout() -> Out {
163    Out {
164        stream: if STDOUT_ON_STDERR.get() {
165            Stream::Stderr
166        } else {
167            Stream::Stdout
168        },
169    }
170}
171
172/// Run `show` with everything it prints to stdout going to stderr instead.
173///
174/// For a list that goes with a question: the question is asked on stderr, so
175/// when stdout is a file or a pipe the list must go beside it, or the person
176/// answers without seeing what they approve.
177pub fn on_stderr<T>(show: impl FnOnce() -> T) -> T {
178    /// Puts the routing back even if `show` panics
179    struct Reset(bool);
180    impl Drop for Reset {
181        fn drop(&mut self) {
182            STDOUT_ON_STDERR.set(self.0);
183        }
184    }
185    let _reset = Reset(STDOUT_ON_STDERR.replace(true));
186    show()
187}
188
189/// Whether stdout is still good: its reader is there and no write has failed
190pub fn stdout_usable() -> bool {
191    STDOUT.usable()
192}
193
194/// Whether stderr is still good: its reader is there and no write has failed
195pub fn stderr_usable() -> bool {
196    STDERR.usable()
197}
198
199/// A writer that goes straight to the stream, whatever has failed before.
200///
201/// For putting a terminal back: the escape codes must be tried even after an
202/// earlier write failed (the terminal may well still be there), and nothing
203/// about them is worth recording.
204pub struct Unrecorded {
205    to_stderr: bool,
206}
207
208impl Write for Unrecorded {
209    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
210        self.write_all(buf).map(|()| buf.len())
211    }
212
213    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
214        if self.to_stderr {
215            io::stderr().write_all(buf)
216        } else {
217            io::stdout().write_all(buf)
218        }
219    }
220
221    fn flush(&mut self) -> io::Result<()> {
222        if self.to_stderr {
223            io::stderr().flush()
224        } else {
225            io::stdout().flush()
226        }
227    }
228}
229
230/// The writer for stdout that ignores earlier failures (see [`Unrecorded`])
231pub fn stdout_unrecorded() -> Unrecorded {
232    Unrecorded { to_stderr: false }
233}
234
235/// The writer for stderr that ignores earlier failures (see [`Unrecorded`])
236pub fn stderr_unrecorded() -> Unrecorded {
237    Unrecorded { to_stderr: true }
238}
239
240/// The writer for stderr
241pub fn stderr() -> Out {
242    Out {
243        stream: Stream::Stderr,
244    }
245}
246
247/// Whether stdout is a terminal
248pub fn stdout_is_terminal() -> bool {
249    io::stdout().is_terminal()
250}
251
252/// Whether stderr is a terminal
253pub fn stderr_is_terminal() -> bool {
254    io::stderr().is_terminal()
255}
256
257/// Wait for `span`, or until nobody reads stdout any more; true in that case.
258///
259/// For a command that prints on a timer (`watch --json`): the broken pipe is
260/// only noticed by a write, and the next one is a whole interval away, so a
261/// reader that left would be waited for that long. On Unix the wait itself
262/// watches the pipe and ends the moment the reader closes it. Windows has no
263/// such wait without more system calls than this is worth: there the reader's
264/// leaving is noticed by the next write, as for any other command.
265pub fn reader_gone_within(span: Duration) -> bool {
266    !STDOUT.usable() || hung_up_within(span)
267}
268
269#[cfg(unix)]
270fn hung_up_within(span: Duration) -> bool {
271    poll_hangup(libc::STDOUT_FILENO, span)
272}
273
274#[cfg(not(unix))]
275fn hung_up_within(span: Duration) -> bool {
276    std::thread::sleep(span);
277    false
278}
279
280/// Wait up to `span` for the other end of `fd` to be closed (`POLLERR` on a
281/// pipe whose reader is gone, `POLLHUP` on a terminal that hung up)
282#[cfg(unix)]
283fn poll_hangup(fd: i32, span: Duration) -> bool {
284    let deadline = std::time::Instant::now() + span;
285    // A wait that a signal interrupts is started again, for what is left
286    let outcome = std::iter::repeat_with(|| poll_once(fd, deadline)).find_map(|done| done);
287    match outcome {
288        Some((0, _, _)) => false,
289        Some((ready, events, _)) if ready > 0 && events & (libc::POLLERR | libc::POLLHUP) != 0 => {
290            true
291        }
292        // Something else (the descriptor is not open, or poll failed): there is
293        // nothing to learn from it, so just sit out the rest of the wait
294        other => {
295            std::thread::sleep(other.map_or(Duration::ZERO, |(_, _, left)| left));
296            false
297        }
298    }
299}
300
301/// One `poll` of `fd` until `deadline`: what it returned, the events it
302/// reported and the time that was left when it started. `None` if a signal
303/// interrupted it.
304#[cfg(unix)]
305fn poll_once(fd: i32, deadline: std::time::Instant) -> Option<(i32, i16, Duration)> {
306    let left = deadline.saturating_duration_since(std::time::Instant::now());
307    // Round up, so a wait of under a millisecond does not become a spin
308    let millis = left.as_micros().div_ceil(1000).min(i32::MAX as u128) as i32;
309    // Linux reports a pipe whose reader left (POLLERR) whatever is asked
310    // for; macOS reports nothing unless POLLHUP is asked for
311    let mut watched = libc::pollfd {
312        fd,
313        events: libc::POLLHUP,
314        revents: 0,
315    };
316    // SAFETY: one valid pollfd, and a count of one
317    let ready = unsafe { libc::poll(&mut watched, 1, millis) };
318    let interrupted = ready < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted;
319    (!interrupted).then_some((ready, watched.revents, left))
320}
321
322/// Settle the result of a write whose failure is already recorded on its
323/// stream (see the module docs): there is nothing left to do with it here.
324pub fn recorded(result: io::Result<()>) {
325    drop(result);
326}
327
328/// Write formatted text to stdout, and a newline after it when `newline` is
329/// set. This is what [`out_print!`](crate::out_print!) and [`out_println!`](crate::out_println!) call.
330pub fn print_stdout(args: fmt::Arguments<'_>, newline: bool) {
331    print_to(&mut stdout(), args, newline);
332}
333
334/// Write formatted text to stderr, and a newline after it when `newline` is
335/// set. This is what [`err_print!`](crate::err_print!) and [`err_println!`](crate::err_println!) call.
336pub fn print_stderr(args: fmt::Arguments<'_>, newline: bool) {
337    print_to(&mut stderr(), args, newline);
338}
339
340fn print_to(out: &mut impl Write, args: fmt::Arguments<'_>, newline: bool) {
341    let written = out.write_fmt(args);
342    recorded(written);
343    if newline {
344        recorded(out.write_all(b"\n"));
345    }
346}
347
348/// Run something that prints to stdout by itself (clap's help and version,
349/// which choose their own colours) under the same policy: a failed write is
350/// recorded, and nothing is written once stdout has failed.
351pub fn through_stdout(print: impl FnOnce() -> io::Result<()>) {
352    recorded(STDOUT.run(print));
353}
354
355/// Write bytes to stdout in one go (a generated man page or completion script)
356pub fn write_stdout(bytes: &[u8]) {
357    recorded(stdout().write_all(bytes));
358}
359
360/// `print!` for stdout: a failed write is recorded, never a panic
361#[macro_export]
362macro_rules! out_print {
363    ($($arg:tt)*) => {
364        $crate::ui::out::print_stdout(::std::format_args!($($arg)*), false)
365    };
366}
367
368/// `println!` for stdout: a failed write is recorded, never a panic
369#[macro_export]
370macro_rules! out_println {
371    () => {
372        $crate::ui::out::print_stdout(::std::format_args!(""), true)
373    };
374    ($($arg:tt)*) => {
375        $crate::ui::out::print_stdout(::std::format_args!($($arg)*), true)
376    };
377}
378
379/// `eprint!` for stderr: a failed write is recorded, never a panic
380#[macro_export]
381macro_rules! err_print {
382    ($($arg:tt)*) => {
383        $crate::ui::out::print_stderr(::std::format_args!($($arg)*), false)
384    };
385}
386
387/// `eprintln!` for stderr: a failed write is recorded, never a panic
388#[macro_export]
389macro_rules! err_println {
390    () => {
391        $crate::ui::out::print_stderr(::std::format_args!(""), true)
392    };
393    ($($arg:tt)*) => {
394        $crate::ui::out::print_stderr(::std::format_args!($($arg)*), true)
395    };
396}
397
398/// The exit code once the command is over: `code`, unless a write failed.
399///
400/// Flushes stdout (so a failure of the last, unterminated line is seen too).
401/// A broken pipe leaves `code` alone. Any other write error on either stream
402/// makes an exit 0 an exit 1, and one on stdout is also reported on stderr.
403pub fn finish(code: u8) -> u8 {
404    recorded(stdout().flush());
405    resolve(code, &STDOUT, &STDERR, &mut stderr())
406}
407
408fn resolve(code: u8, stdout: &Channel, stderr: &Channel, report: &mut impl Write) -> u8 {
409    let stdout_failure = stdout.failure();
410    if let Some(message) = &stdout_failure {
411        recorded(writeln!(report, "proc: cannot write to stdout: {message}"));
412    }
413    if code == 0 && (stdout_failure.is_some() || stderr.failure().is_some()) {
414        1
415    } else {
416        code
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    fn broken() -> io::Result<()> {
425        Err(ErrorKind::BrokenPipe.into())
426    }
427
428    fn full() -> io::Result<()> {
429        Err(io::Error::other("no space left on device"))
430    }
431
432    #[test]
433    fn a_broken_pipe_stops_the_stream_without_a_failure() {
434        let channel = Channel::new();
435        assert!(channel.run(|| Ok(())).is_ok());
436        assert_eq!(
437            channel.run(broken).unwrap_err().kind(),
438            ErrorKind::BrokenPipe
439        );
440        // later writes fail at once, without running
441        let err = channel
442            .run(|| -> io::Result<()> { panic!("must not write after a broken pipe") })
443            .unwrap_err();
444        assert_eq!(err.kind(), ErrorKind::BrokenPipe);
445        assert_eq!(channel.failure(), None);
446    }
447
448    #[test]
449    fn another_write_error_is_a_failure_that_sticks() {
450        let channel = Channel::new();
451        assert_eq!(
452            channel.run(full).unwrap_err().to_string(),
453            "no space left on device"
454        );
455        let err = channel
456            .run(|| -> io::Result<()> { panic!("must not write after a failure") })
457            .unwrap_err();
458        assert_eq!(err.to_string(), "no space left on device");
459        assert_eq!(
460            channel.failure().as_deref(),
461            Some("no space left on device")
462        );
463    }
464
465    #[test]
466    fn the_first_failure_is_the_one_kept() {
467        let channel = Channel::new();
468        let _ = channel.run(full);
469        // a broken pipe after a failure does not replace it
470        let _ = channel.run(|| Err::<(), _>(io::Error::new(ErrorKind::BrokenPipe, "late")));
471        assert_eq!(
472            channel.failure().as_deref(),
473            Some("no space left on device")
474        );
475    }
476
477    #[test]
478    fn a_poisoned_lock_is_still_usable() {
479        let channel = Channel::new();
480        std::thread::scope(|scope| {
481            let _ = scope
482                .spawn(|| {
483                    let _held = channel.state.lock().unwrap();
484                    panic!("poison the lock");
485                })
486                .join();
487        });
488        assert!(channel.state.is_poisoned());
489        assert!(channel.run(|| Ok(())).is_ok());
490    }
491
492    #[test]
493    fn the_exit_code_follows_the_command_unless_a_write_failed() {
494        let (stdout, stderr) = (Channel::new(), Channel::new());
495        let mut report = Vec::new();
496        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
497        assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
498        assert!(report.is_empty());
499
500        // a closed reader on either stream changes nothing
501        let _ = stdout.run(broken);
502        let _ = stderr.run(broken);
503        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
504        assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
505        assert!(report.is_empty());
506    }
507
508    #[test]
509    fn a_failed_stdout_is_exit_1_and_is_reported() {
510        let (stdout, stderr) = (Channel::new(), Channel::new());
511        let _ = stdout.run(full);
512        let mut report = Vec::new();
513        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
514        assert_eq!(
515            String::from_utf8(report).unwrap(),
516            "proc: cannot write to stdout: no space left on device\n"
517        );
518        // a command that failed on its own keeps its code
519        assert_eq!(resolve(2, &stdout, &stderr, &mut Vec::new()), 2);
520    }
521
522    #[test]
523    fn a_failed_stderr_is_exit_1_and_says_nothing() {
524        let (stdout, stderr) = (Channel::new(), Channel::new());
525        let _ = stderr.run(full);
526        let mut report = Vec::new();
527        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
528        assert!(report.is_empty());
529        assert_eq!(resolve(3, &stdout, &stderr, &mut report), 3);
530    }
531
532    #[test]
533    fn the_message_is_lost_quietly_when_the_report_fails_too() {
534        let (stdout, stderr) = (Channel::new(), Channel::new());
535        let _ = stdout.run(full);
536        let mut gone = crate::test_support::FailsAfter {
537            left: 0,
538            written: Vec::new(),
539        };
540        assert_eq!(resolve(0, &stdout, &stderr, &mut gone), 1);
541    }
542
543    #[test]
544    fn the_real_streams_take_an_empty_write() {
545        for mut out in [stdout(), stderr()] {
546            assert_eq!(out.write(b"").unwrap(), 0);
547            out.flush().unwrap();
548        }
549    }
550
551    #[test]
552    fn a_stream_is_usable_until_something_goes_wrong_on_it() {
553        let channel = Channel::new();
554        assert!(channel.usable());
555        let _ = channel.run(broken);
556        assert!(!channel.usable());
557
558        let channel = Channel::new();
559        let _ = channel.run(full);
560        assert!(!channel.usable());
561    }
562
563    #[test]
564    fn on_stderr_sends_stdout_to_stderr_and_puts_it_back() {
565        let on = || matches!(stdout().stream, Stream::Stderr);
566        assert!(!on());
567        assert!(on_stderr(on));
568        // inside another on_stderr, it stays on when the inner one ends
569        assert!(on_stderr(|| {
570            on_stderr(|| ());
571            on()
572        }));
573        assert!(!on());
574        // and it is put back when the list panics
575        let caught = std::panic::catch_unwind(|| on_stderr(|| panic!("a list that panics")));
576        assert!(caught.is_err());
577        assert!(!on());
578    }
579
580    #[test]
581    fn unrecorded_writers_take_an_empty_write() {
582        for mut out in [stdout_unrecorded(), stderr_unrecorded()] {
583            assert_eq!(out.write(b"").unwrap(), 0);
584            out.flush().unwrap();
585        }
586    }
587
588    #[test]
589    fn a_wait_with_a_reader_still_there_lasts_the_whole_time() {
590        let started = std::time::Instant::now();
591        assert!(!reader_gone_within(Duration::from_millis(40)));
592        assert!(started.elapsed() >= Duration::from_millis(40));
593    }
594
595    #[cfg(unix)]
596    mod hangup {
597        use super::super::*;
598        use std::os::fd::{AsRawFd, IntoRawFd};
599        use std::time::Instant;
600
601        #[test]
602        fn a_pipe_whose_reader_left_ends_the_wait_at_once() {
603            let (reader, writer) = std::io::pipe().unwrap();
604            let fd = writer.as_raw_fd();
605
606            // reader still there: the wait runs its course
607            let started = Instant::now();
608            assert!(!poll_hangup(fd, Duration::from_millis(40)));
609            assert!(started.elapsed() >= Duration::from_millis(40));
610
611            drop(reader);
612            let started = Instant::now();
613            assert!(poll_hangup(fd, Duration::from_secs(30)));
614            assert!(started.elapsed() < Duration::from_secs(5));
615        }
616
617        #[test]
618        fn a_file_has_no_reader_to_lose() {
619            let file = std::fs::File::open("/dev/null").unwrap();
620            let started = Instant::now();
621            assert!(!poll_hangup(file.as_raw_fd(), Duration::from_millis(40)));
622            assert!(started.elapsed() >= Duration::from_millis(40));
623        }
624
625        #[test]
626        fn a_descriptor_that_is_not_open_just_waits() {
627            let (reader, _writer) = std::io::pipe().unwrap();
628            let closed = reader.into_raw_fd();
629            // SAFETY: closing the descriptor taken from `reader` just above
630            unsafe { libc::close(closed) };
631            let started = Instant::now();
632            assert!(!poll_hangup(closed, Duration::from_millis(40)));
633            assert!(started.elapsed() >= Duration::from_millis(40));
634        }
635    }
636
637    #[test]
638    fn print_to_writes_the_text_and_the_newline() {
639        let mut out = Vec::new();
640        print_to(&mut out, format_args!("{} {}", "a", 1), true);
641        print_to(&mut out, format_args!("b"), false);
642        assert_eq!(out, b"a 1\nb");
643    }
644
645    #[test]
646    fn print_to_survives_a_failing_writer() {
647        let mut gone = crate::test_support::FailsAfter {
648            left: 1,
649            written: Vec::new(),
650        };
651        print_to(&mut gone, format_args!("abc"), true);
652        assert_eq!(gone.written, b"a");
653    }
654}