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/// The longest one `poll` waits. NetBSD's poll doesn't wake when a pipe's
302/// reader closes during the wait: a reader that closed 1 s into a 5 s poll
303/// left it waiting the whole 5 s, while one already closed is reported at
304/// once. So there it looks again every 100 ms; elsewhere one poll waits it all
305#[cfg(target_os = "netbsd")]
306const POLL_SLICE: Duration = Duration::from_millis(100);
307#[cfg(all(unix, not(target_os = "netbsd")))]
308const POLL_SLICE: Duration = Duration::MAX;
309
310/// One `poll` of `fd` until `deadline` (at most [`POLL_SLICE`]): what it
311/// returned, the events it reported and the time that was left when it
312/// started. `None` if a signal interrupted it, or the slice ended before the
313/// deadline with nothing to report: look again.
314#[cfg(unix)]
315fn poll_once(fd: i32, deadline: std::time::Instant) -> Option<(i32, i16, Duration)> {
316    let left = deadline.saturating_duration_since(std::time::Instant::now());
317    // Round up, so a wait of under a millisecond does not become a spin
318    let millis = left
319        .min(POLL_SLICE)
320        .as_micros()
321        .div_ceil(1000)
322        .min(i32::MAX as u128) as i32;
323    // Linux reports a pipe whose reader left (POLLERR) whatever is asked
324    // for; macOS reports nothing unless POLLHUP is asked for
325    let mut watched = libc::pollfd {
326        fd,
327        events: libc::POLLHUP,
328        revents: 0,
329    };
330    // SAFETY: one valid pollfd, and a count of one
331    let ready = unsafe { libc::poll(&mut watched, 1, millis) };
332    let interrupted = ready < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted;
333    let slice_ended = ready == 0 && std::time::Instant::now() < deadline;
334    (!interrupted && !slice_ended).then_some((ready, watched.revents, left))
335}
336
337/// Settle the result of a write whose failure is already recorded on its
338/// stream (see the module docs): there is nothing left to do with it here.
339pub fn recorded(result: io::Result<()>) {
340    drop(result);
341}
342
343/// Write formatted text to stdout, and a newline after it when `newline` is
344/// set. This is what [`out_print!`](crate::out_print!) and [`out_println!`](crate::out_println!) call.
345pub fn print_stdout(args: fmt::Arguments<'_>, newline: bool) {
346    print_to(&mut stdout(), args, newline);
347}
348
349/// Write formatted text to stderr, and a newline after it when `newline` is
350/// set. This is what [`err_print!`](crate::err_print!) and [`err_println!`](crate::err_println!) call.
351pub fn print_stderr(args: fmt::Arguments<'_>, newline: bool) {
352    print_to(&mut stderr(), args, newline);
353}
354
355fn print_to(out: &mut impl Write, args: fmt::Arguments<'_>, newline: bool) {
356    let written = out.write_fmt(args);
357    recorded(written);
358    if newline {
359        recorded(out.write_all(b"\n"));
360    }
361}
362
363/// Run something that prints to stdout by itself (clap's help and version,
364/// which choose their own colours) under the same policy: a failed write is
365/// recorded, and nothing is written once stdout has failed.
366pub fn through_stdout(print: impl FnOnce() -> io::Result<()>) {
367    recorded(STDOUT.run(print));
368}
369
370/// Write bytes to stdout in one go (a generated man page or completion script)
371pub fn write_stdout(bytes: &[u8]) {
372    recorded(stdout().write_all(bytes));
373}
374
375/// `print!` for stdout: a failed write is recorded, never a panic
376#[macro_export]
377macro_rules! out_print {
378    ($($arg:tt)*) => {
379        $crate::ui::out::print_stdout(::std::format_args!($($arg)*), false)
380    };
381}
382
383/// `println!` for stdout: a failed write is recorded, never a panic
384#[macro_export]
385macro_rules! out_println {
386    () => {
387        $crate::ui::out::print_stdout(::std::format_args!(""), true)
388    };
389    ($($arg:tt)*) => {
390        $crate::ui::out::print_stdout(::std::format_args!($($arg)*), true)
391    };
392}
393
394/// `eprint!` for stderr: a failed write is recorded, never a panic
395#[macro_export]
396macro_rules! err_print {
397    ($($arg:tt)*) => {
398        $crate::ui::out::print_stderr(::std::format_args!($($arg)*), false)
399    };
400}
401
402/// `eprintln!` for stderr: a failed write is recorded, never a panic
403#[macro_export]
404macro_rules! err_println {
405    () => {
406        $crate::ui::out::print_stderr(::std::format_args!(""), true)
407    };
408    ($($arg:tt)*) => {
409        $crate::ui::out::print_stderr(::std::format_args!($($arg)*), true)
410    };
411}
412
413/// The exit code once the command is over: `code`, unless a write failed.
414///
415/// Flushes stdout (so a failure of the last, unterminated line is seen too).
416/// A broken pipe leaves `code` alone. Any other write error on either stream
417/// makes an exit 0 an exit 1, and one on stdout is also reported on stderr.
418pub fn finish(code: u8) -> u8 {
419    recorded(stdout().flush());
420    resolve(code, &STDOUT, &STDERR, &mut stderr())
421}
422
423fn resolve(code: u8, stdout: &Channel, stderr: &Channel, report: &mut impl Write) -> u8 {
424    let stdout_failure = stdout.failure();
425    if let Some(message) = &stdout_failure {
426        recorded(writeln!(report, "proc: cannot write to stdout: {message}"));
427    }
428    if code == 0 && (stdout_failure.is_some() || stderr.failure().is_some()) {
429        1
430    } else {
431        code
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    fn broken() -> io::Result<()> {
440        Err(ErrorKind::BrokenPipe.into())
441    }
442
443    fn full() -> io::Result<()> {
444        Err(io::Error::other("no space left on device"))
445    }
446
447    #[test]
448    fn a_broken_pipe_stops_the_stream_without_a_failure() {
449        let channel = Channel::new();
450        assert!(channel.run(|| Ok(())).is_ok());
451        assert_eq!(
452            channel.run(broken).unwrap_err().kind(),
453            ErrorKind::BrokenPipe
454        );
455        // later writes fail at once, without running
456        let err = channel
457            .run(|| -> io::Result<()> { panic!("must not write after a broken pipe") })
458            .unwrap_err();
459        assert_eq!(err.kind(), ErrorKind::BrokenPipe);
460        assert_eq!(channel.failure(), None);
461    }
462
463    #[test]
464    fn another_write_error_is_a_failure_that_sticks() {
465        let channel = Channel::new();
466        assert_eq!(
467            channel.run(full).unwrap_err().to_string(),
468            "no space left on device"
469        );
470        let err = channel
471            .run(|| -> io::Result<()> { panic!("must not write after a failure") })
472            .unwrap_err();
473        assert_eq!(err.to_string(), "no space left on device");
474        assert_eq!(
475            channel.failure().as_deref(),
476            Some("no space left on device")
477        );
478    }
479
480    #[test]
481    fn the_first_failure_is_the_one_kept() {
482        let channel = Channel::new();
483        let _ = channel.run(full);
484        // a broken pipe after a failure does not replace it
485        let _ = channel.run(|| Err::<(), _>(io::Error::new(ErrorKind::BrokenPipe, "late")));
486        assert_eq!(
487            channel.failure().as_deref(),
488            Some("no space left on device")
489        );
490    }
491
492    #[test]
493    fn a_poisoned_lock_is_still_usable() {
494        let channel = Channel::new();
495        std::thread::scope(|scope| {
496            let _ = scope
497                .spawn(|| {
498                    let _held = channel.state.lock().unwrap();
499                    panic!("poison the lock");
500                })
501                .join();
502        });
503        assert!(channel.state.is_poisoned());
504        assert!(channel.run(|| Ok(())).is_ok());
505    }
506
507    #[test]
508    fn the_exit_code_follows_the_command_unless_a_write_failed() {
509        let (stdout, stderr) = (Channel::new(), Channel::new());
510        let mut report = Vec::new();
511        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
512        assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
513        assert!(report.is_empty());
514
515        // a closed reader on either stream changes nothing
516        let _ = stdout.run(broken);
517        let _ = stderr.run(broken);
518        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
519        assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
520        assert!(report.is_empty());
521    }
522
523    #[test]
524    fn a_failed_stdout_is_exit_1_and_is_reported() {
525        let (stdout, stderr) = (Channel::new(), Channel::new());
526        let _ = stdout.run(full);
527        let mut report = Vec::new();
528        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
529        assert_eq!(
530            String::from_utf8(report).unwrap(),
531            "proc: cannot write to stdout: no space left on device\n"
532        );
533        // a command that failed on its own keeps its code
534        assert_eq!(resolve(2, &stdout, &stderr, &mut Vec::new()), 2);
535    }
536
537    #[test]
538    fn a_failed_stderr_is_exit_1_and_says_nothing() {
539        let (stdout, stderr) = (Channel::new(), Channel::new());
540        let _ = stderr.run(full);
541        let mut report = Vec::new();
542        assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
543        assert!(report.is_empty());
544        assert_eq!(resolve(3, &stdout, &stderr, &mut report), 3);
545    }
546
547    #[test]
548    fn the_message_is_lost_quietly_when_the_report_fails_too() {
549        let (stdout, stderr) = (Channel::new(), Channel::new());
550        let _ = stdout.run(full);
551        let mut gone = crate::test_support::FailsAfter {
552            left: 0,
553            written: Vec::new(),
554        };
555        assert_eq!(resolve(0, &stdout, &stderr, &mut gone), 1);
556    }
557
558    #[test]
559    fn the_real_streams_take_an_empty_write() {
560        for mut out in [stdout(), stderr()] {
561            assert_eq!(out.write(b"").unwrap(), 0);
562            out.flush().unwrap();
563        }
564    }
565
566    #[test]
567    fn a_stream_is_usable_until_something_goes_wrong_on_it() {
568        let channel = Channel::new();
569        assert!(channel.usable());
570        let _ = channel.run(broken);
571        assert!(!channel.usable());
572
573        let channel = Channel::new();
574        let _ = channel.run(full);
575        assert!(!channel.usable());
576    }
577
578    #[test]
579    fn on_stderr_sends_stdout_to_stderr_and_puts_it_back() {
580        let on = || matches!(stdout().stream, Stream::Stderr);
581        assert!(!on());
582        assert!(on_stderr(on));
583        // inside another on_stderr, it stays on when the inner one ends
584        assert!(on_stderr(|| {
585            on_stderr(|| ());
586            on()
587        }));
588        assert!(!on());
589        // and it is put back when the list panics
590        let caught = std::panic::catch_unwind(|| on_stderr(|| panic!("a list that panics")));
591        assert!(caught.is_err());
592        assert!(!on());
593    }
594
595    #[test]
596    fn unrecorded_writers_take_an_empty_write() {
597        for mut out in [stdout_unrecorded(), stderr_unrecorded()] {
598            assert_eq!(out.write(b"").unwrap(), 0);
599            out.flush().unwrap();
600        }
601    }
602
603    #[test]
604    fn a_wait_with_a_reader_still_there_lasts_the_whole_time() {
605        let started = std::time::Instant::now();
606        assert!(!reader_gone_within(Duration::from_millis(40)));
607        assert!(started.elapsed() >= Duration::from_millis(40));
608    }
609
610    #[cfg(unix)]
611    mod hangup {
612        use super::super::*;
613        use std::os::fd::{AsRawFd, IntoRawFd};
614        use std::time::Instant;
615
616        #[test]
617        fn a_pipe_whose_reader_left_ends_the_wait_at_once() {
618            let (reader, writer) = std::io::pipe().unwrap();
619            let fd = writer.as_raw_fd();
620
621            // reader still there: the wait runs its course
622            let started = Instant::now();
623            assert!(!poll_hangup(fd, Duration::from_millis(40)));
624            assert!(started.elapsed() >= Duration::from_millis(40));
625
626            drop(reader);
627            let started = Instant::now();
628            assert!(poll_hangup(fd, Duration::from_secs(30)));
629            assert!(started.elapsed() < Duration::from_secs(5));
630        }
631
632        #[test]
633        fn a_file_has_no_reader_to_lose() {
634            let file = std::fs::File::open("/dev/null").unwrap();
635            let started = Instant::now();
636            assert!(!poll_hangup(file.as_raw_fd(), Duration::from_millis(40)));
637            assert!(started.elapsed() >= Duration::from_millis(40));
638        }
639
640        #[test]
641        fn a_descriptor_that_is_not_open_just_waits() {
642            let (reader, _writer) = std::io::pipe().unwrap();
643            let closed = reader.into_raw_fd();
644            // SAFETY: closing the descriptor taken from `reader` just above
645            unsafe { libc::close(closed) };
646            let started = Instant::now();
647            assert!(!poll_hangup(closed, Duration::from_millis(40)));
648            assert!(started.elapsed() >= Duration::from_millis(40));
649        }
650    }
651
652    #[test]
653    fn print_to_writes_the_text_and_the_newline() {
654        let mut out = Vec::new();
655        print_to(&mut out, format_args!("{} {}", "a", 1), true);
656        print_to(&mut out, format_args!("b"), false);
657        assert_eq!(out, b"a 1\nb");
658    }
659
660    #[test]
661    fn print_to_survives_a_failing_writer() {
662        let mut gone = crate::test_support::FailsAfter {
663            left: 1,
664            written: Vec::new(),
665        };
666        print_to(&mut gone, format_args!("abc"), true);
667        assert_eq!(gone.written, b"a");
668    }
669}