fix: report what happened instead of losing it

This commit is contained in:
2026-09-09 11:59:58 +00:00
parent 75f293ce36
commit b4b6d5918f
7 changed files with 168 additions and 33 deletions

View File

@@ -1,18 +1,24 @@
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 {
eprintln!($($arg)*)
$crate::output::write_err(format_args!($($arg)*))
}
};
}
// not progress: it speaks about the result, so --quiet keeps it
macro_rules! warning {
($($arg:tt)*) => { eprintln!("warning: {}", format_args!($($arg)*)) };
($($arg:tt)*) => {
$crate::output::write_err(format_args!("warning: {}", format_args!($($arg)*)))
};
}
// what a command was asked for, on stdout
@@ -25,19 +31,72 @@ 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) {
finish(writeln!(io::stdout(), "{args}"));
}
pub(crate) fn write_text(args: Arguments) {
finish(write!(io::stdout(), "{args}"));
}
// a reader leaving early ends the pipe; println! would panic instead
fn finish(written: io::Result<()>) {
if written.is_err() {
std::process::exit(0);
if writable() {
finish(writeln!(io::stdout(), "{args}"));
}
}
pub(crate) fn write_text(args: Arguments) {
if writable() {
finish(write!(io::stdout(), "{args}"));
}
}
// 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};