1#![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#[derive(Default)]
39struct State {
40 broken: bool,
42 failure: Option<(ErrorKind, String)>,
44}
45
46struct 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 self.state
66 .lock()
67 .unwrap_or_else(std::sync::PoisonError::into_inner)
68 }
69
70 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 fn usable(&self) -> bool {
97 let state = self.lock();
98 !state.broken && state.failure.is_none()
99 }
100
101 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#[derive(Clone, Copy)]
115enum Stream {
116 Stdout,
117 Stderr,
118}
119
120#[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 static STDOUT_ON_STDERR: Cell<bool> = const { Cell::new(false) };
159}
160
161pub fn stdout() -> Out {
163 Out {
164 stream: if STDOUT_ON_STDERR.get() {
165 Stream::Stderr
166 } else {
167 Stream::Stdout
168 },
169 }
170}
171
172pub fn on_stderr<T>(show: impl FnOnce() -> T) -> T {
178 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
189pub fn stdout_usable() -> bool {
191 STDOUT.usable()
192}
193
194pub fn stderr_usable() -> bool {
196 STDERR.usable()
197}
198
199pub 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
230pub fn stdout_unrecorded() -> Unrecorded {
232 Unrecorded { to_stderr: false }
233}
234
235pub fn stderr_unrecorded() -> Unrecorded {
237 Unrecorded { to_stderr: true }
238}
239
240pub fn stderr() -> Out {
242 Out {
243 stream: Stream::Stderr,
244 }
245}
246
247pub fn stdout_is_terminal() -> bool {
249 io::stdout().is_terminal()
250}
251
252pub fn stderr_is_terminal() -> bool {
254 io::stderr().is_terminal()
255}
256
257pub 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#[cfg(unix)]
283fn poll_hangup(fd: i32, span: Duration) -> bool {
284 let deadline = std::time::Instant::now() + span;
285 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 other => {
295 std::thread::sleep(other.map_or(Duration::ZERO, |(_, _, left)| left));
296 false
297 }
298 }
299}
300
301#[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 let millis = left.as_micros().div_ceil(1000).min(i32::MAX as u128) as i32;
309 let mut watched = libc::pollfd {
312 fd,
313 events: libc::POLLHUP,
314 revents: 0,
315 };
316 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
322pub fn recorded(result: io::Result<()>) {
325 drop(result);
326}
327
328pub fn print_stdout(args: fmt::Arguments<'_>, newline: bool) {
331 print_to(&mut stdout(), args, newline);
332}
333
334pub 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
348pub fn through_stdout(print: impl FnOnce() -> io::Result<()>) {
352 recorded(STDOUT.run(print));
353}
354
355pub fn write_stdout(bytes: &[u8]) {
357 recorded(stdout().write_all(bytes));
358}
359
360#[macro_export]
362macro_rules! out_print {
363 ($($arg:tt)*) => {
364 $crate::ui::out::print_stdout(::std::format_args!($($arg)*), false)
365 };
366}
367
368#[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#[macro_export]
381macro_rules! err_print {
382 ($($arg:tt)*) => {
383 $crate::ui::out::print_stderr(::std::format_args!($($arg)*), false)
384 };
385}
386
387#[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
398pub 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 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 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 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 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 assert!(on_stderr(|| {
570 on_stderr(|| ());
571 on()
572 }));
573 assert!(!on());
574 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 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 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}