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:
parent
8f4b90db7b
commit
e1f16bee26
|
|
@ -40,7 +40,6 @@ use radicle::profile;
|
||||||
|
|
||||||
use crate::util::environment::Environment;
|
use crate::util::environment::Environment;
|
||||||
|
|
||||||
#[ignore = "test fails"]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config() {
|
fn config() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
@ -121,7 +120,6 @@ fn help() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `rad self` uses `Element::print()` for table output.
|
/// `rad self` uses `Element::print()` for table output.
|
||||||
#[ignore = "test fails"]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rad_self() {
|
fn rad_self() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,12 @@ pub trait Element: fmt::Debug + Send + Sync {
|
||||||
|
|
||||||
/// Print this element to stdout.
|
/// Print this element to stdout.
|
||||||
fn print(&self) {
|
fn print(&self) {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let mut stdout = io::stdout().lock();
|
||||||
for line in self.render(Constraint::from_env().unwrap_or_default()) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ macro_rules! info {
|
||||||
writeln!($writer, $($arg)*).ok();
|
writeln!($writer, $($arg)*).ok();
|
||||||
});
|
});
|
||||||
($($arg:tt)*) => ({
|
($($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) {
|
pub fn tip_args(args: fmt::Arguments) {
|
||||||
println!(
|
print(format_args!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
format::yellow("*"),
|
format::yellow("*"),
|
||||||
style(format!("{args}")).italic()
|
style(format!("{args}")).italic()
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
|
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) {
|
pub fn headline(headline: impl fmt::Display) {
|
||||||
println!();
|
print("");
|
||||||
println!("{}", style(headline).bold());
|
print(style(headline).bold());
|
||||||
println!();
|
print("");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(header: &str) {
|
pub fn header(header: &str) {
|
||||||
println!();
|
print("");
|
||||||
println!("{}", style(format::yellow(header)).bold().underline());
|
print(style(format::yellow(header)).bold().underline());
|
||||||
println!();
|
print("");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blob(text: impl fmt::Display) {
|
pub fn blob(text: impl fmt::Display) {
|
||||||
println!("{}", style(text.to_string().trim()).dim());
|
print(style(text.to_string().trim()).dim());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blank() {
|
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) {
|
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 {
|
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) {
|
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> {
|
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) {
|
pub fn usage(name: &str, usage: &str) {
|
||||||
println!(
|
print(format_args!(
|
||||||
"{} {}\n{}",
|
"{} {}\n{}",
|
||||||
PREFIX_ERROR,
|
PREFIX_ERROR,
|
||||||
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
||||||
Paint::red(prefixed(TAB, usage)).dim()
|
Paint::red(prefixed(TAB, usage)).dim()
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
|
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) {
|
pub fn indented(msg: impl fmt::Display) {
|
||||||
println!("{TAB}{msg}");
|
print(format_args!("{TAB}{msg}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subcommand(msg: impl fmt::Display) {
|
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) {
|
pub fn warning(warning: impl fmt::Display) {
|
||||||
println!(
|
print(format_args!(
|
||||||
"{} {} {warning}",
|
"{} {} {warning}",
|
||||||
PREFIX_WARNING,
|
PREFIX_WARNING,
|
||||||
Paint::yellow("Warning:").bold(),
|
Paint::yellow("Warning:").bold(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(error: impl fmt::Display) {
|
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) {
|
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 {
|
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue