#![allow(clippy::disallowed_macros, clippy::disallowed_methods)]
use std::cell::Cell;
use std::fmt;
use std::io::{self, ErrorKind, IsTerminal, Write};
use std::sync::Mutex;
use std::time::Duration;
#[derive(Default)]
struct State {
broken: bool,
failure: Option<(ErrorKind, String)>,
}
struct Channel {
state: Mutex<State>,
}
impl Channel {
const fn new() -> Self {
Channel {
state: Mutex::new(State {
broken: false,
failure: None,
}),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn run<T>(&self, write: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
{
let state = self.lock();
if state.broken {
return Err(ErrorKind::BrokenPipe.into());
}
if let Some((kind, message)) = &state.failure {
return Err(io::Error::new(*kind, message.clone()));
}
}
let result = write();
if let Err(err) = &result {
let mut state = self.lock();
if err.kind() == ErrorKind::BrokenPipe {
state.broken = true;
} else if state.failure.is_none() {
state.failure = Some((err.kind(), err.to_string()));
}
}
result
}
fn usable(&self) -> bool {
let state = self.lock();
!state.broken && state.failure.is_none()
}
fn failure(&self) -> Option<String> {
self.lock()
.failure
.as_ref()
.map(|(_, message)| message.clone())
}
}
static STDOUT: Channel = Channel::new();
static STDERR: Channel = Channel::new();
#[derive(Clone, Copy)]
enum Stream {
Stdout,
Stderr,
}
#[derive(Clone, Copy)]
pub struct Out {
stream: Stream,
}
impl Out {
fn channel(self) -> &'static Channel {
match self.stream {
Stream::Stdout => &STDOUT,
Stream::Stderr => &STDERR,
}
}
}
impl Write for Out {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_all(buf).map(|()| buf.len())
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.channel().run(|| match self.stream {
Stream::Stdout => io::stdout().write_all(buf),
Stream::Stderr => io::stderr().write_all(buf),
})
}
fn flush(&mut self) -> io::Result<()> {
self.channel().run(|| match self.stream {
Stream::Stdout => io::stdout().flush(),
Stream::Stderr => io::stderr().flush(),
})
}
}
thread_local! {
static STDOUT_ON_STDERR: Cell<bool> = const { Cell::new(false) };
}
pub fn stdout() -> Out {
Out {
stream: if STDOUT_ON_STDERR.get() {
Stream::Stderr
} else {
Stream::Stdout
},
}
}
pub fn on_stderr<T>(show: impl FnOnce() -> T) -> T {
struct Reset(bool);
impl Drop for Reset {
fn drop(&mut self) {
STDOUT_ON_STDERR.set(self.0);
}
}
let _reset = Reset(STDOUT_ON_STDERR.replace(true));
show()
}
pub fn stdout_usable() -> bool {
STDOUT.usable()
}
pub fn stderr_usable() -> bool {
STDERR.usable()
}
pub struct Unrecorded {
to_stderr: bool,
}
impl Write for Unrecorded {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.write_all(buf).map(|()| buf.len())
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
if self.to_stderr {
io::stderr().write_all(buf)
} else {
io::stdout().write_all(buf)
}
}
fn flush(&mut self) -> io::Result<()> {
if self.to_stderr {
io::stderr().flush()
} else {
io::stdout().flush()
}
}
}
pub fn stdout_unrecorded() -> Unrecorded {
Unrecorded { to_stderr: false }
}
pub fn stderr_unrecorded() -> Unrecorded {
Unrecorded { to_stderr: true }
}
pub fn stderr() -> Out {
Out {
stream: Stream::Stderr,
}
}
pub fn stdout_is_terminal() -> bool {
io::stdout().is_terminal()
}
pub fn stderr_is_terminal() -> bool {
io::stderr().is_terminal()
}
pub fn reader_gone_within(span: Duration) -> bool {
!STDOUT.usable() || hung_up_within(span)
}
#[cfg(unix)]
fn hung_up_within(span: Duration) -> bool {
poll_hangup(libc::STDOUT_FILENO, span)
}
#[cfg(not(unix))]
fn hung_up_within(span: Duration) -> bool {
std::thread::sleep(span);
false
}
#[cfg(unix)]
fn poll_hangup(fd: i32, span: Duration) -> bool {
let deadline = std::time::Instant::now() + span;
let outcome = std::iter::repeat_with(|| poll_once(fd, deadline)).find_map(|done| done);
match outcome {
Some((0, _, _)) => false,
Some((ready, events, _)) if ready > 0 && events & (libc::POLLERR | libc::POLLHUP) != 0 => {
true
}
other => {
std::thread::sleep(other.map_or(Duration::ZERO, |(_, _, left)| left));
false
}
}
}
#[cfg(unix)]
fn poll_once(fd: i32, deadline: std::time::Instant) -> Option<(i32, i16, Duration)> {
let left = deadline.saturating_duration_since(std::time::Instant::now());
let millis = left.as_micros().div_ceil(1000).min(i32::MAX as u128) as i32;
let mut watched = libc::pollfd {
fd,
events: libc::POLLHUP,
revents: 0,
};
let ready = unsafe { libc::poll(&mut watched, 1, millis) };
let interrupted = ready < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted;
(!interrupted).then_some((ready, watched.revents, left))
}
pub fn recorded(result: io::Result<()>) {
drop(result);
}
pub fn print_stdout(args: fmt::Arguments<'_>, newline: bool) {
print_to(&mut stdout(), args, newline);
}
pub fn print_stderr(args: fmt::Arguments<'_>, newline: bool) {
print_to(&mut stderr(), args, newline);
}
fn print_to(out: &mut impl Write, args: fmt::Arguments<'_>, newline: bool) {
let written = out.write_fmt(args);
recorded(written);
if newline {
recorded(out.write_all(b"\n"));
}
}
pub fn through_stdout(print: impl FnOnce() -> io::Result<()>) {
recorded(STDOUT.run(print));
}
pub fn write_stdout(bytes: &[u8]) {
recorded(stdout().write_all(bytes));
}
#[macro_export]
macro_rules! out_print {
($($arg:tt)*) => {
$crate::ui::out::print_stdout(::std::format_args!($($arg)*), false)
};
}
#[macro_export]
macro_rules! out_println {
() => {
$crate::ui::out::print_stdout(::std::format_args!(""), true)
};
($($arg:tt)*) => {
$crate::ui::out::print_stdout(::std::format_args!($($arg)*), true)
};
}
#[macro_export]
macro_rules! err_print {
($($arg:tt)*) => {
$crate::ui::out::print_stderr(::std::format_args!($($arg)*), false)
};
}
#[macro_export]
macro_rules! err_println {
() => {
$crate::ui::out::print_stderr(::std::format_args!(""), true)
};
($($arg:tt)*) => {
$crate::ui::out::print_stderr(::std::format_args!($($arg)*), true)
};
}
pub fn finish(code: u8) -> u8 {
recorded(stdout().flush());
resolve(code, &STDOUT, &STDERR, &mut stderr())
}
fn resolve(code: u8, stdout: &Channel, stderr: &Channel, report: &mut impl Write) -> u8 {
let stdout_failure = stdout.failure();
if let Some(message) = &stdout_failure {
recorded(writeln!(report, "proc: cannot write to stdout: {message}"));
}
if code == 0 && (stdout_failure.is_some() || stderr.failure().is_some()) {
1
} else {
code
}
}
#[cfg(test)]
mod tests {
use super::*;
fn broken() -> io::Result<()> {
Err(ErrorKind::BrokenPipe.into())
}
fn full() -> io::Result<()> {
Err(io::Error::other("no space left on device"))
}
#[test]
fn a_broken_pipe_stops_the_stream_without_a_failure() {
let channel = Channel::new();
assert!(channel.run(|| Ok(())).is_ok());
assert_eq!(
channel.run(broken).unwrap_err().kind(),
ErrorKind::BrokenPipe
);
let err = channel
.run(|| -> io::Result<()> { panic!("must not write after a broken pipe") })
.unwrap_err();
assert_eq!(err.kind(), ErrorKind::BrokenPipe);
assert_eq!(channel.failure(), None);
}
#[test]
fn another_write_error_is_a_failure_that_sticks() {
let channel = Channel::new();
assert_eq!(
channel.run(full).unwrap_err().to_string(),
"no space left on device"
);
let err = channel
.run(|| -> io::Result<()> { panic!("must not write after a failure") })
.unwrap_err();
assert_eq!(err.to_string(), "no space left on device");
assert_eq!(
channel.failure().as_deref(),
Some("no space left on device")
);
}
#[test]
fn the_first_failure_is_the_one_kept() {
let channel = Channel::new();
let _ = channel.run(full);
let _ = channel.run(|| Err::<(), _>(io::Error::new(ErrorKind::BrokenPipe, "late")));
assert_eq!(
channel.failure().as_deref(),
Some("no space left on device")
);
}
#[test]
fn a_poisoned_lock_is_still_usable() {
let channel = Channel::new();
std::thread::scope(|scope| {
let _ = scope
.spawn(|| {
let _held = channel.state.lock().unwrap();
panic!("poison the lock");
})
.join();
});
assert!(channel.state.is_poisoned());
assert!(channel.run(|| Ok(())).is_ok());
}
#[test]
fn the_exit_code_follows_the_command_unless_a_write_failed() {
let (stdout, stderr) = (Channel::new(), Channel::new());
let mut report = Vec::new();
assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
assert!(report.is_empty());
let _ = stdout.run(broken);
let _ = stderr.run(broken);
assert_eq!(resolve(0, &stdout, &stderr, &mut report), 0);
assert_eq!(resolve(2, &stdout, &stderr, &mut report), 2);
assert!(report.is_empty());
}
#[test]
fn a_failed_stdout_is_exit_1_and_is_reported() {
let (stdout, stderr) = (Channel::new(), Channel::new());
let _ = stdout.run(full);
let mut report = Vec::new();
assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
assert_eq!(
String::from_utf8(report).unwrap(),
"proc: cannot write to stdout: no space left on device\n"
);
assert_eq!(resolve(2, &stdout, &stderr, &mut Vec::new()), 2);
}
#[test]
fn a_failed_stderr_is_exit_1_and_says_nothing() {
let (stdout, stderr) = (Channel::new(), Channel::new());
let _ = stderr.run(full);
let mut report = Vec::new();
assert_eq!(resolve(0, &stdout, &stderr, &mut report), 1);
assert!(report.is_empty());
assert_eq!(resolve(3, &stdout, &stderr, &mut report), 3);
}
#[test]
fn the_message_is_lost_quietly_when_the_report_fails_too() {
let (stdout, stderr) = (Channel::new(), Channel::new());
let _ = stdout.run(full);
let mut gone = crate::test_support::FailsAfter {
left: 0,
written: Vec::new(),
};
assert_eq!(resolve(0, &stdout, &stderr, &mut gone), 1);
}
#[test]
fn the_real_streams_take_an_empty_write() {
for mut out in [stdout(), stderr()] {
assert_eq!(out.write(b"").unwrap(), 0);
out.flush().unwrap();
}
}
#[test]
fn a_stream_is_usable_until_something_goes_wrong_on_it() {
let channel = Channel::new();
assert!(channel.usable());
let _ = channel.run(broken);
assert!(!channel.usable());
let channel = Channel::new();
let _ = channel.run(full);
assert!(!channel.usable());
}
#[test]
fn on_stderr_sends_stdout_to_stderr_and_puts_it_back() {
let on = || matches!(stdout().stream, Stream::Stderr);
assert!(!on());
assert!(on_stderr(on));
assert!(on_stderr(|| {
on_stderr(|| ());
on()
}));
assert!(!on());
let caught = std::panic::catch_unwind(|| on_stderr(|| panic!("a list that panics")));
assert!(caught.is_err());
assert!(!on());
}
#[test]
fn unrecorded_writers_take_an_empty_write() {
for mut out in [stdout_unrecorded(), stderr_unrecorded()] {
assert_eq!(out.write(b"").unwrap(), 0);
out.flush().unwrap();
}
}
#[test]
fn a_wait_with_a_reader_still_there_lasts_the_whole_time() {
let started = std::time::Instant::now();
assert!(!reader_gone_within(Duration::from_millis(40)));
assert!(started.elapsed() >= Duration::from_millis(40));
}
#[cfg(unix)]
mod hangup {
use super::super::*;
use std::os::fd::{AsRawFd, IntoRawFd};
use std::time::Instant;
#[test]
fn a_pipe_whose_reader_left_ends_the_wait_at_once() {
let (reader, writer) = std::io::pipe().unwrap();
let fd = writer.as_raw_fd();
let started = Instant::now();
assert!(!poll_hangup(fd, Duration::from_millis(40)));
assert!(started.elapsed() >= Duration::from_millis(40));
drop(reader);
let started = Instant::now();
assert!(poll_hangup(fd, Duration::from_secs(30)));
assert!(started.elapsed() < Duration::from_secs(5));
}
#[test]
fn a_file_has_no_reader_to_lose() {
let file = std::fs::File::open("/dev/null").unwrap();
let started = Instant::now();
assert!(!poll_hangup(file.as_raw_fd(), Duration::from_millis(40)));
assert!(started.elapsed() >= Duration::from_millis(40));
}
#[test]
fn a_descriptor_that_is_not_open_just_waits() {
let (reader, _writer) = std::io::pipe().unwrap();
let closed = reader.into_raw_fd();
unsafe { libc::close(closed) };
let started = Instant::now();
assert!(!poll_hangup(closed, Duration::from_millis(40)));
assert!(started.elapsed() >= Duration::from_millis(40));
}
}
#[test]
fn print_to_writes_the_text_and_the_newline() {
let mut out = Vec::new();
print_to(&mut out, format_args!("{} {}", "a", 1), true);
print_to(&mut out, format_args!("b"), false);
assert_eq!(out, b"a 1\nb");
}
#[test]
fn print_to_survives_a_failing_writer() {
let mut gone = crate::test_support::FailsAfter {
left: 1,
written: Vec::new(),
};
print_to(&mut gone, format_args!("abc"), true);
assert_eq!(gone.written, b"a");
}
}