Files
ahab/src/output.rs

110 lines
3.2 KiB
Rust

use std::fmt::Arguments;
use std::io::{self, Write};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU8, Ordering};
use anyhow::{Result, anyhow};
// progress, on stderr and only when it was asked for
macro_rules! note {
($ctx:expr, $($arg:tt)*) => {
if !$ctx.quiet {
$crate::output::write_err(format_args!($($arg)*))
}
};
}
// not progress: it speaks about the result, so --quiet keeps it
macro_rules! warning {
($($arg:tt)*) => {
$crate::output::write_err(format_args!("warning: {}", format_args!($($arg)*)))
};
}
// what a command was asked for, on stdout
macro_rules! line {
($($arg:tt)*) => { $crate::output::write_line(format_args!($($arg)*)) };
}
// the same, for a caller that terminates its own entries
macro_rules! text {
($($arg:tt)*) => { $crate::output::write_text(format_args!($($arg)*)) };
}
// stdout is open, or it is not and why
const OPEN: u8 = 0;
// the reader left: nothing is wrong, there is just nowhere to write
const CLOSED: u8 = 1;
// the write itself failed, so what was asked for was never delivered
const FAILED: u8 = 2;
static STDOUT: AtomicU8 = AtomicU8::new(OPEN);
static REASON: OnceLock<String> = OnceLock::new();
pub(crate) fn write_line(args: Arguments) {
if writable() {
finish(writeln!(io::stdout(), "{args}"));
}
}
pub(crate) fn write_text(args: Arguments) {
if writable() {
finish(write!(io::stdout(), "{args}"));
}
}
// a path on unix is bytes, not text, so a record naming one is written as bytes
pub(crate) fn write_bytes(bytes: &[u8]) {
if writable() {
finish(io::stdout().write_all(bytes));
}
}
// stderr is commentary about the work, not the work itself. there is nowhere to
// report that it could not be written, so the error goes nowhere -- eprintln!
// panics instead, which turns a closed pipe into a crash
pub(crate) fn write_err(args: Arguments) {
let _ = writeln!(io::stderr(), "{args}");
}
fn writable() -> bool {
STDOUT.load(Ordering::Relaxed) == OPEN
}
// a failed write must not end the process: a command halfway through moving
// files would leave the rest undone and still report the success it had planned
// on. writing stops, the command runs to its end, and main asks how it went
fn finish(written: io::Result<()>) {
let Err(e) = written else {
return;
};
match e.kind() {
io::ErrorKind::BrokenPipe => STDOUT.store(CLOSED, Ordering::Relaxed),
_ => {
STDOUT.store(FAILED, Ordering::Relaxed);
let _ = REASON.set(e.to_string());
}
}
}
// whether everything a command was asked for actually reached stdout. the
// buffered writes text! makes are flushed here rather than by the runtime after
// main returns, which discards the error and truncates the output in silence
pub(crate) fn delivered() -> Result<()> {
if writable() {
finish(io::stdout().flush());
}
if STDOUT.load(Ordering::Relaxed) != FAILED {
return Ok(());
}
Err(match REASON.get() {
Some(reason) => anyhow!("writing to stdout failed: {reason}"),
None => anyhow!("writing to stdout failed"),
})
}
pub(crate) use {line, note, text, warning};