fix: report what happened instead of losing it
This commit is contained in:
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user