From 8cde79b8e3ad0c55e66a79bba285df429c2f1b9b Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 2 Oct 2026 21:16:34 +0200 Subject: [PATCH] fix(cli): stop quietly when stdout is closed `pdfs ls | head` ended in a "Broken pipe" panic once `head` had its lines, because std's `println!` panics on a failed write. The CLI now shadows `println!` and `print!` with versions that exit 0 on a closed pipe and report any other write error, such as a full disk, with exit 1. Restoring the default SIGPIPE would also stop the panic, but the same binary runs the daemon, which must not die when a socket's peer goes away. --- crates/pdfs-cli/src/main.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 7dfb5ad..6b26f0b 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -20,6 +20,39 @@ use pdfs_core::control::{ use pdfs_core::db::Db; use pdfs_core::service; +/// `println!` for this binary, shadowing std's, which panics when stdout is +/// closed: `pdfs ls | head` ended in a "Broken pipe" panic once `head` had its +/// lines. This one hands the failure to [`stdout_written`] instead. +/// +/// Restoring the default `SIGPIPE` would also stop the panic, but the same +/// binary runs the daemon, which must not die when a socket's peer goes away. +macro_rules! println { + ($($arg:tt)*) => {{ + use std::io::Write as _; + $crate::stdout_written(writeln!(std::io::stdout(), $($arg)*)) + }}; +} + +/// [`println!`] without the newline. +macro_rules! print { + ($($arg:tt)*) => {{ + use std::io::Write as _; + $crate::stdout_written(write!(std::io::stdout(), $($arg)*)) + }}; +} + +/// Exit on a failed write to stdout. A closed pipe means the reader has all it +/// wanted, so that exit is a success. +fn stdout_written(result: std::io::Result<()>) { + if let Err(error) = result { + if error.kind() == std::io::ErrorKind::BrokenPipe { + std::process::exit(0); + } + eprintln!("Error: writing to stdout: {error}"); + std::process::exit(1); + } +} + #[derive(Parser)] #[command( name = "pdfs",