cli: Handle broken pipe (SIGPIPE) gracefully
Prevent `rad` from panicking when its stdout is a closed pipe, e.g. when running `rad config | head`. The broken pipes are handled at the output boundaries using three layers of defence: 1. Inner: Route all stdout output through `term::print()` and `term::print_inline()`, which use explicit `writeln!`/`write!` to a locked stdout and silently ignore write errors. All `println!` calls in `radicle-cli` and `radicle-term` are converted. 2. Middle: Catch `BrokenPipe` errors that propagate up via `anyhow` from subcommands in `main::run()`, and exit cleanly with code 0. 3. Outer: Install a panic hook that intercepts `println!` panics containing "Broken pipe" (from dependencies like clap) and exits cleanly, chained in front of human_panic's hook. To prevent regressions, `#![deny(clippy::print_stdout)]` is added to `radicle-cli`, requiring all future stdout output to go through the safe `term::` functions. Note: `eprintln!` calls (stderr) are not handled, as broken stderr pipes are extremely rare in practice.
This commit is contained in:
parent
e1f16bee26
commit
1a31a9f541
|
|
@ -3022,6 +3022,7 @@ name = "radicle-cli"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"backtrace",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ rust-version = "1.85.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
amplify = { version = "4.0.0", default-features = false }
|
amplify = { version = "4.0.0", default-features = false }
|
||||||
|
backtrace = "0.3.75"
|
||||||
bstr = "1.3"
|
bstr = "1.3"
|
||||||
bytes = "1.11.1"
|
bytes = "1.11.1"
|
||||||
chrono = { version = "0.4.26", default-features = false }
|
chrono = { version = "0.4.26", default-features = false }
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ name = "rad"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["i2p", "tor"]
|
default = ["backtrace", "i2p","tor"]
|
||||||
i2p = ["radicle/i2p"]
|
i2p = ["radicle/i2p"]
|
||||||
tor = ["radicle/tor"]
|
tor = ["radicle/tor"]
|
||||||
|
|
||||||
|
|
@ -57,6 +57,7 @@ tree-sitter-toml-ng = "0.6.0"
|
||||||
tree-sitter-typescript = "0.23.2"
|
tree-sitter-typescript = "0.23.2"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
backtrace = { workspace = true, optional = true }
|
||||||
shlex = { workspace = true }
|
shlex = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
oid
|
oid
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
println!("{oid}");
|
term::print(oid);
|
||||||
}
|
}
|
||||||
Migrate => {
|
Migrate => {
|
||||||
let mut db = profile.cobs_db_mut()?;
|
let mut db = profile.cobs_db_mut()?;
|
||||||
|
|
@ -100,7 +100,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
FilteredTypeName::from(type_name).as_ref(),
|
FilteredTypeName::from(type_name).as_ref(),
|
||||||
)?;
|
)?;
|
||||||
for cob in cobs {
|
for cob in cobs {
|
||||||
println!("{}", cob.id);
|
term::print(cob.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log {
|
Log {
|
||||||
|
|
@ -215,7 +215,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{oid}");
|
term::print(oid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -400,7 +400,6 @@ mod test {
|
||||||
.chain(ARGS.iter())
|
.chain(ARGS.iter())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
println!("{args:?}");
|
|
||||||
assert!(args.is_ok())
|
assert!(args.is_ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ fn debug(profile: Option<&Profile>) -> anyhow::Result<()> {
|
||||||
warnings: collect_warnings(profile),
|
warnings: collect_warnings(profile),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string_pretty(&debug).unwrap());
|
term::print(serde_json::to_string_pretty(&debug).unwrap());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,7 @@ fn print(
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
print_meta(revision, previous, profile)?;
|
print_meta(revision, previous, profile)?;
|
||||||
println!();
|
term::blank();
|
||||||
print_diff(revision.parent.as_ref(), &revision.id, repo)?;
|
print_diff(revision.parent.as_ref(), &revision.id, repo)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -514,7 +514,7 @@ fn print_diff(
|
||||||
|
|
||||||
if let Some(modified) = diff.modified().next() {
|
if let Some(modified) = diff.modified().next() {
|
||||||
let diff = modified.diff.to_unified_string()?;
|
let diff = modified.diff.to_unified_string()?;
|
||||||
print!("{diff}");
|
term::print_inline(diff);
|
||||||
} else {
|
} else {
|
||||||
term::print(term::format::italic("No changes."));
|
term::print(term::format::italic("No changes."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let refs = RefsAt::new(&repo, remote)?;
|
let refs = RefsAt::new(&repo, remote)?;
|
||||||
let sigrefs = SignedRefs::load_at(refs.at, remote, &repo);
|
let sigrefs = SignedRefs::load_at(refs.at, remote, &repo);
|
||||||
|
|
||||||
println!(
|
term::print(format_args!(
|
||||||
"{:<48} {} {}",
|
"{:<48} {} {}",
|
||||||
term::format::tertiary(remote.to_human()),
|
term::format::tertiary(remote.to_human()),
|
||||||
term::format::secondary(refs.at),
|
term::format::secondary(refs.at),
|
||||||
|
|
@ -103,7 +103,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
term::format::negative("missing".to_string())
|
term::format::negative("missing".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Target::Policy => {
|
Target::Policy => {
|
||||||
|
|
@ -111,19 +111,19 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let seed = policies.seed_policy(&rid)?;
|
let seed = policies.seed_policy(&rid)?;
|
||||||
match seed.policy {
|
match seed.policy {
|
||||||
SeedingPolicy::Allow { scope } => {
|
SeedingPolicy::Allow { scope } => {
|
||||||
println!(
|
term::print(format_args!(
|
||||||
"Repository {} is {} with scope {}",
|
"Repository {} is {} with scope {}",
|
||||||
term::format::tertiary(&rid),
|
term::format::tertiary(&rid),
|
||||||
term::format::positive("being seeded"),
|
term::format::positive("being seeded"),
|
||||||
term::format::dim(format!("`{scope}`"))
|
term::format::dim(format!("`{scope}`"))
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
SeedingPolicy::Block => {
|
SeedingPolicy::Block => {
|
||||||
println!(
|
term::print(format_args!(
|
||||||
"Repository {} is {}",
|
"Repository {} is {}",
|
||||||
term::format::tertiary(&rid),
|
term::format::tertiary(&rid),
|
||||||
term::format::negative("not being seeded"),
|
term::format::negative("not being seeded"),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,19 +132,19 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let aliases = profile.aliases();
|
let aliases = profile.aliases();
|
||||||
for did in doc.delegates().iter() {
|
for did in doc.delegates().iter() {
|
||||||
if let Some(alias) = aliases.alias(did) {
|
if let Some(alias) = aliases.alias(did) {
|
||||||
println!(
|
term::print(format_args!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
term::format::tertiary(&did),
|
term::format::tertiary(&did),
|
||||||
term::format::parens(term::format::dim(alias))
|
term::format::parens(term::format::dim(alias))
|
||||||
);
|
));
|
||||||
} else {
|
} else {
|
||||||
println!("{}", term::format::tertiary(&did));
|
term::print(term::format::tertiary(&did));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Target::Visibility => {
|
Target::Visibility => {
|
||||||
let (_, doc) = repo(rid, storage)?;
|
let (_, doc) = repo(rid, storage)?;
|
||||||
println!("{}", term::format::visibility(doc.visibility()));
|
term::print(term::format::visibility(doc.visibility()));
|
||||||
}
|
}
|
||||||
Target::History => {
|
Target::History => {
|
||||||
let (repo, _) = repo(rid, storage)?;
|
let (repo, _) = repo(rid, storage)?;
|
||||||
|
|
@ -177,22 +177,22 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
.with_timezone(&timezone)
|
.with_timezone(&timezone)
|
||||||
.to_rfc2822();
|
.to_rfc2822();
|
||||||
|
|
||||||
println!(
|
term::print(format_args!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
term::format::yellow("commit"),
|
term::format::yellow("commit"),
|
||||||
term::format::yellow(oid),
|
term::format::yellow(oid),
|
||||||
);
|
));
|
||||||
if let Ok(parent) = tip.parent_id(0) {
|
if let Ok(parent) = tip.parent_id(0) {
|
||||||
println!("parent {parent}");
|
term::print(format_args!("parent {parent}"));
|
||||||
}
|
}
|
||||||
println!("blob {}", revision.blob);
|
term::print(format_args!("blob {}", revision.blob));
|
||||||
println!("date {time}");
|
term::print(format_args!("date {time}"));
|
||||||
println!();
|
term::blank();
|
||||||
|
|
||||||
if let Some(msg) = tip.message() {
|
if let Some(msg) = tip.message() {
|
||||||
for line in msg.lines() {
|
for line in msg.lines() {
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
println!();
|
term::blank();
|
||||||
} else {
|
} else {
|
||||||
term::indented(term::format::dim(line));
|
term::indented(term::format::dim(line));
|
||||||
}
|
}
|
||||||
|
|
@ -200,10 +200,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
term::blank();
|
term::blank();
|
||||||
}
|
}
|
||||||
for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
|
for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
|
||||||
println!(" {line}");
|
term::print(format_args!(" {line}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
term::blank();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Target::RepoId => {
|
Target::RepoId => {
|
||||||
|
|
@ -232,7 +232,7 @@ fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("{}", tree(refs));
|
term::print_inline(tree(refs));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
Command::Inventory { nid } => {
|
Command::Inventory { nid } => {
|
||||||
let nid = nid.as_ref().unwrap_or(profile.id());
|
let nid = nid.as_ref().unwrap_or(profile.id());
|
||||||
for rid in profile.routing()?.get_inventory(nid)? {
|
for rid in profile.routing()?.get_inventory(nid)? {
|
||||||
println!("{}", term::format::tertiary(rid));
|
term::print(term::format::tertiary(rid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Status {
|
Command::Status {
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ pub fn logs(lines: usize, follow: Option<time::Duration>, profile: &Profile) ->
|
||||||
}
|
}
|
||||||
tail.reverse();
|
tail.reverse();
|
||||||
|
|
||||||
print!("{}", term::format::dim(String::from_utf8_lossy(&tail)));
|
term::print_inline(term::format::dim(String::from_utf8_lossy(&tail)));
|
||||||
|
|
||||||
if let Some(timeout) = follow {
|
if let Some(timeout) = follow {
|
||||||
file.seek(SeekFrom::End(0))?;
|
file.seek(SeekFrom::End(0))?;
|
||||||
|
|
@ -184,7 +184,7 @@ pub fn logs(lines: usize, follow: Option<time::Duration>, profile: &Profile) ->
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
thread::sleep(time::Duration::from_millis(250));
|
thread::sleep(time::Duration::from_millis(250));
|
||||||
} else {
|
} else {
|
||||||
print!("{}", term::format::dim(line));
|
term::print_inline(term::format::dim(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -414,7 +414,7 @@ pub fn config(node: &Node) -> anyhow::Result<()> {
|
||||||
let cfg = node.config()?;
|
let cfg = node.config()?;
|
||||||
let cfg = serde_json::to_string_pretty(&cfg)?;
|
let cfg = serde_json::to_string_pretty(&cfg)?;
|
||||||
|
|
||||||
println!("{cfg}");
|
term::print(cfg);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ use std::time;
|
||||||
|
|
||||||
use radicle::node::{Event, Handle};
|
use radicle::node::{Event, Handle};
|
||||||
|
|
||||||
|
use crate::terminal as term;
|
||||||
|
|
||||||
pub fn run<H>(node: H, count: usize, timeout: time::Duration) -> anyhow::Result<()>
|
pub fn run<H>(node: H, count: usize, timeout: time::Duration) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
H: Handle<Event = Result<Event, <H as Handle>::Error>>,
|
H: Handle<Event = Result<Event, <H as Handle>::Error>>,
|
||||||
|
|
@ -11,7 +13,7 @@ where
|
||||||
let event = event?;
|
let event = event?;
|
||||||
let obj = serde_json::to_string(&event)?;
|
let obj = serde_json::to_string(&event)?;
|
||||||
|
|
||||||
println!("{obj}");
|
term::print(&obj);
|
||||||
|
|
||||||
// Only output up to `count` events.
|
// Only output up to `count` events.
|
||||||
if i + 1 >= count {
|
if i + 1 >= count {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,6 @@ fn print_table(entries: impl IntoIterator<Item = (RepoId, NodeId)>) {
|
||||||
|
|
||||||
fn print_json(entries: impl IntoIterator<Item = (RepoId, NodeId)>) {
|
fn print_json(entries: impl IntoIterator<Item = (RepoId, NodeId)>) {
|
||||||
for (rid, nid) in entries {
|
for (rid, nid) in entries {
|
||||||
println!("{}", serde_json::json!({ "rid": rid, "nid": nid }));
|
term::print(serde_json::json!({ "rid": rid, "nid": nid }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub use args::Args;
|
||||||
pub fn run(_args: Args, _ctx: impl term::Context) -> anyhow::Result<()> {
|
pub fn run(_args: Args, _ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let home = profile::home()?;
|
let home = profile::home()?;
|
||||||
|
|
||||||
println!("{}", home.path().display());
|
term::print(home.path().display());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
// Prevent use of `println!` and `print!` which panic on broken pipes.
|
||||||
|
// Use `terminal::io::println` or `terminal::io::print` instead.
|
||||||
|
#![deny(clippy::print_stdout)]
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,25 @@ fn main() {
|
||||||
.homepage(env!("CARGO_PKG_HOMEPAGE"))
|
.homepage(env!("CARGO_PKG_HOMEPAGE"))
|
||||||
.support("Open a support request at https://radicle.zulipchat.com/ or file an issue via Radicle itself, or e-mail to team@radicle.dev"));
|
.support("Open a support request at https://radicle.zulipchat.com/ or file an issue via Radicle itself, or e-mail to team@radicle.dev"));
|
||||||
|
|
||||||
|
// Install a panic hook that intercepts panics caused by broken pipes and exits
|
||||||
|
// cleanly. This is a backstop for any uses of `println!` (in our code or
|
||||||
|
// dependencies like `clap`) that were not converted to `term::print`.
|
||||||
|
//
|
||||||
|
// `println!` panics with "failed printing to stdout: Broken pipe" when
|
||||||
|
// failing to write to a closed standard output. We chain our hook in front
|
||||||
|
// of `human_panic`'s hook so that panics not caused by broken pipes are
|
||||||
|
// still handled by `human_panic`.
|
||||||
|
//
|
||||||
|
// See also <https://github.com/rust-lang/rust/issues/62569>.
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let default_hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
handle_broken_pipe(info);
|
||||||
|
default_hook(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(lvl) = radicle::logger::env_level() {
|
if let Some(lvl) = radicle::logger::env_level() {
|
||||||
let logger = Box::new(radicle::logger::Logger::new(lvl));
|
let logger = Box::new(radicle::logger::Logger::new(lvl));
|
||||||
log::set_boxed_logger(logger).expect("no other logger should have been set already");
|
log::set_boxed_logger(logger).expect("no other logger should have been set already");
|
||||||
|
|
@ -146,12 +165,95 @@ fn run(command: Command, ctx: impl term::Context) -> ! {
|
||||||
match run_command(command, ctx) {
|
match run_command(command, ctx) {
|
||||||
Ok(()) => process::exit(0),
|
Ok(()) => process::exit(0),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
// If the error is a broken pipe, exit cleanly. This happens when
|
||||||
|
// output is piped to a command that exits before reading all our
|
||||||
|
// output, e.g. `rad config | head`.
|
||||||
|
//
|
||||||
|
// Rust ignores `SIGPIPE` by default (since 1.62), so broken pipes
|
||||||
|
// and instead returns `io::ErrorKind::BrokenPipe` errors on writes.
|
||||||
|
// We want to catch these and exit cleanly.
|
||||||
|
//
|
||||||
|
// See <https://github.com/rust-lang/rust/issues/62569>.
|
||||||
|
#[cfg(unix)]
|
||||||
|
if is_broken_pipe(&err) {
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
term::fail(&err);
|
term::fail(&err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle an error of kind [`ErrorKind::BrokenPipe`] during a panic, and
|
||||||
|
/// exit the process with exit code 0.
|
||||||
|
///
|
||||||
|
/// # Debug
|
||||||
|
///
|
||||||
|
/// If compiled with `debug_assertions` enabled, then the panic is written to
|
||||||
|
/// [`std::io::stderr`].
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn handle_broken_pipe(info: &std::panic::PanicHookInfo<'_>) {
|
||||||
|
if !is_broken_pipe_panic(info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let thread = std::thread::current();
|
||||||
|
let thread = thread.name().unwrap_or("<unnamed>");
|
||||||
|
|
||||||
|
let mut stderr = std::io::stderr().lock();
|
||||||
|
|
||||||
|
match info.location() {
|
||||||
|
Some(location) => {
|
||||||
|
let _ = writeln!(
|
||||||
|
stderr,
|
||||||
|
"broken pipe in thread '{thread}' at: {}:{}",
|
||||||
|
location.file(),
|
||||||
|
location.line(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let _ = writeln!(stderr, "broken pipe in thread '{thread}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "backtrace")]
|
||||||
|
let backtrace = format!("{:?}", backtrace::Backtrace::new());
|
||||||
|
|
||||||
|
#[cfg(not(feature = "backtrace"))]
|
||||||
|
let backtrace = "(no backtrace available)";
|
||||||
|
|
||||||
|
let _ = writeln!(stderr, "{backtrace}");
|
||||||
|
}
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any error in the [`anyhow::Error::chain`] of `err` is of kind
|
||||||
|
/// [`ErrorKind::BrokenPipe`].
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn is_broken_pipe(err: &anyhow::Error) -> bool {
|
||||||
|
err.chain()
|
||||||
|
.filter_map(|cause| cause.downcast_ref::<io::Error>())
|
||||||
|
.any(|io_err| io_err.kind() == ErrorKind::BrokenPipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a panic was caused by writing to a broken pipe.
|
||||||
|
///
|
||||||
|
/// The standard library panics with a [`String`] payload containing
|
||||||
|
/// "Broken pipe" when [`println!`] or [`print!`] fail to write because standard
|
||||||
|
/// output is closed. This is stable behaviour across all Unix platforms, since
|
||||||
|
/// it is adopted from the description of `EPIPE` in [`errno.h` in POSIX.1-2024].
|
||||||
|
///
|
||||||
|
/// [`errno.h` in POSIX.1-2024]: https://pubs.opengroup.org/onlinepubs/9799919799.2024edition/basedefs/errno.h.html
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn is_broken_pipe_panic(info: &std::panic::PanicHookInfo<'_>) -> bool {
|
||||||
|
info.payload()
|
||||||
|
.downcast_ref::<&'static str>()
|
||||||
|
.copied()
|
||||||
|
.or(info.payload().downcast_ref::<String>().map(|s| s.as_str()))
|
||||||
|
.is_some_and(|message| message.contains("Broken pipe"))
|
||||||
|
}
|
||||||
|
|
||||||
fn run_command(command: Command, ctx: impl term::Context) -> Result<(), anyhow::Error> {
|
fn run_command(command: Command, ctx: impl term::Context) -> Result<(), anyhow::Error> {
|
||||||
match command {
|
match command {
|
||||||
Command::Auth(args) => auth::run(args, ctx),
|
Command::Auth(args) => auth::run(args, ctx),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "radi
|
||||||
tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"]
|
tor = ["cyphernet/tor", "radicle/tor", "radicle-protocol/tor"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
backtrace = { version = "0.3.75", optional = true }
|
backtrace = { workspace = true, optional = true }
|
||||||
bytes = { workspace = true }
|
bytes = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
cyphernet = { workspace = true, features = ["dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
|
cyphernet = { workspace = true, features = ["dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue