term: Catch EPIPE and swallow

Rust (since 1.62) ignores EPIPE by default (see [Rust #62569]),
causing writes to closed pipes to return `io::ErrorKind::BrokenPipe`
errors.  The `println!` macro panics on these errors, producing a
confusing backtrace instead of the silent exit expected of Unix CLI
tools.

To avoid the use of `print!` and `println!`, the `radicle-term`
helper functions use `write!` and `writeln!`. This lays the groundwork
for the `rad` CLI to transition to using only `radicle-term`
functions.

There was a first attempt made, which is documented here.  A
process-wide SIGPIPE reset (`SIG_DFL`) was ruled out because `rad`
manages child processes via libgit2 pipes and communicates with the
radicle node over Unix sockets. Resetting SIGPIPE globally caused
`rad` itself to be killed during internal pipe operations (e.g. during
`rad remote add --fetch`), producing flaky test failures.

[Rust #62569] https://github.com/rust-lang/rust/issues/62569
This commit is contained in:
Fintan Halpenny 2026-04-02 16:57:28 +00:00 committed by Lorenz Leutgeb
parent 8f4b90db7b
commit e1f16bee26
3 changed files with 79 additions and 25 deletions

View File

@ -40,7 +40,6 @@ use radicle::profile;
use crate::util::environment::Environment;
#[ignore = "test fails"]
#[test]
fn config() {
let mut environment = Environment::new();
@ -121,7 +120,6 @@ fn help() {
}
/// `rad self` uses `Element::print()` for table output.
#[ignore = "test fails"]
#[test]
fn rad_self() {
let mut environment = Environment::new();

View File

@ -84,8 +84,12 @@ pub trait Element: fmt::Debug + Send + Sync {
/// Print this element to stdout.
fn print(&self) {
use std::io::Write;
let mut stdout = io::stdout().lock();
for line in self.render(Constraint::from_env().unwrap_or_default()) {
println!("{}", line.to_string().trim_end());
let _ = writeln!(stdout, "{}", line.to_string().trim_end())
.or_else(crate::io::swallow_broken_pipe_stdout);
}
}

View File

@ -74,7 +74,7 @@ macro_rules! info {
writeln!($writer, $($arg)*).ok();
});
($($arg:tt)*) => ({
println!("{}", format_args!($($arg)*));
$crate::io::print(format_args!($($arg)*));
})
}
@ -118,11 +118,11 @@ pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
}
pub fn tip_args(args: fmt::Arguments) {
println!(
print(format_args!(
"{} {}",
format::yellow("*"),
style(format!("{args}")).italic()
);
));
}
pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
@ -148,27 +148,76 @@ pub fn viewport() -> Option<Size> {
}
pub fn headline(headline: impl fmt::Display) {
println!();
println!("{}", style(headline).bold());
println!();
print("");
print(style(headline).bold());
print("");
}
pub fn header(header: &str) {
println!();
println!("{}", style(format::yellow(header)).bold().underline());
println!();
print("");
print(style(format::yellow(header)).bold().underline());
print("");
}
pub fn blob(text: impl fmt::Display) {
println!("{}", style(text.to_string().trim()).dim());
print(style(text.to_string().trim()).dim());
}
pub fn blank() {
println!()
print("");
}
/// Print a line to stdout, silently ignoring broken pipe errors.
///
/// Use this function instead of [`println!`] when you want to print to standard
/// output, but silently ignore broken pipe errors.
///
/// See also [`self::print`].
///
/// # Panics
///
/// If writing to standard output fails with an error not of kind [`io::ErrorKind::BrokenPipe`].
pub fn print(msg: impl fmt::Display) {
println!("{msg}");
use io::Write;
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{msg}").or_else(swallow_broken_pipe_stdout);
}
/// Print to stdout without a trailing newline, silently ignoring broken pipe
/// errors.
///
/// Use this function instead of [`print!`] when you want to print to standard
/// output, but silently ignore broken pipe errors.
///
/// See also [`self::print`].
///
/// # Panics
///
/// If writing to standard output fails with an error not of kind [`io::ErrorKind::BrokenPipe`].
pub fn print_inline(msg: impl fmt::Display) {
use io::Write;
let mut stdout = io::stdout().lock();
let _ = write!(stdout, "{msg}").or_else(swallow_broken_pipe_stdout);
}
/// If the given `err` is of kind [`io::ErrorKind::BrokenPipe`], return `Ok(())`
/// to silently ignore it. Otherwise, panic saying "failed printing to stdout",
/// followed by the error message.
///
/// This may be used with [`Result::or_else`] to ignore broken pipes when
/// writing to standard output.
///
/// # Panics
///
/// If `err` is not of kind [`io::ErrorKind::BrokenPipe`].
pub(crate) fn swallow_broken_pipe_stdout(err: io::Error) -> io::Result<()> {
if err.kind() == io::ErrorKind::BrokenPipe {
Ok(())
} else {
panic!("failed printing to stdout: {err}")
}
}
pub fn prefixed(prefix: &str, text: &str) -> String {
@ -179,7 +228,7 @@ pub fn prefixed(prefix: &str, text: &str) -> String {
}
pub fn help(name: &str, version: &str, description: &str, usage: &str) {
println!("rad-{name} {version}\n{description}\n{usage}");
print(format_args!("rad-{name} {version}\n{description}\n{usage}"));
}
pub fn manual(name: &str) -> io::Result<process::ExitStatus> {
@ -192,40 +241,43 @@ pub fn manual(name: &str) -> io::Result<process::ExitStatus> {
}
pub fn usage(name: &str, usage: &str) {
println!(
print(format_args!(
"{} {}\n{}",
PREFIX_ERROR,
Paint::red(format!("Error: rad-{name}: invalid usage")),
Paint::red(prefixed(TAB, usage)).dim()
);
));
}
pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
println!("{prefix} {msg}");
print(format_args!("{prefix} {msg}"));
}
pub fn indented(msg: impl fmt::Display) {
println!("{TAB}{msg}");
print(format_args!("{TAB}{msg}"));
}
pub fn subcommand(msg: impl fmt::Display) {
println!("{}", style(format!("Running `{msg}`...")).dim());
print(style(format!("Running `{msg}`...")).dim());
}
pub fn warning(warning: impl fmt::Display) {
println!(
print(format_args!(
"{} {} {warning}",
PREFIX_WARNING,
Paint::yellow("Warning:").bold(),
);
));
}
pub fn error(error: impl fmt::Display) {
println!("{PREFIX_ERROR} {} {error}", Paint::red("Error:"));
print(format_args!(
"{PREFIX_ERROR} {} {error}",
Paint::red("Error:")
));
}
pub fn hint(hint: impl fmt::Display) {
println!("{}", format::hint(format!("{SYMBOL_ERROR} Hint: {hint}")));
print(format::hint(format!("{SYMBOL_ERROR} Hint: {hint}")));
}
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {