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;
|
||||
|
||||
#[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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue