diff options
| author | akbash <akbash@herdr.dev> | 2026-08-19 20:39:24 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-19 20:39:24 +0300 |
| commit | e3c3d443584b629803a73ad66e0e298943c7f201 (patch) | |
| tree | 639abd8b3d8f03d07d05eef47ca025cafaeda02b /src | |
| parent | 5203a5dc0f39a082938ea0f9836d6257ea7e155f (diff) | |
fix(cli): exit quietly when output pipes close (#2996)
refs #2994
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.rs | 15 | ||||
| -rw-r--r-- | src/cli/completion.rs | 2 | ||||
| -rw-r--r-- | src/cli/spec.rs | 14 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/platform/mod.rs | 8 | ||||
| -rw-r--r-- | src/platform/unix_common.rs | 19 |
6 files changed, 58 insertions, 4 deletions
@@ -8,6 +8,20 @@ use crate::api::schema::{ ReadSource, Request, SplitDirection, }; +macro_rules! print { + ($($arg:tt)*) => {{ + crate::platform::begin_cli_output(); + std::print!($($arg)*); + }}; +} + +macro_rules! println { + ($($arg:tt)*) => {{ + crate::platform::begin_cli_output(); + std::println!($($arg)*); + }}; +} + mod agent; mod api; mod completion; @@ -194,6 +208,7 @@ fn channel_set(args: &[String]) -> std::io::Result<i32> { ChannelSetInstallAction::RunSelfUpdate => {} } + crate::platform::end_cli_output(); if let Err(err) = crate::update::self_update(crate::update::SelfUpdateOptions::default()) { eprintln!("update failed: {err}"); eprintln!("Run `herdr update` to retry."); diff --git a/src/cli/completion.rs b/src/cli/completion.rs index f049175d..02391ec8 100644 --- a/src/cli/completion.rs +++ b/src/cli/completion.rs @@ -35,8 +35,10 @@ pub(super) fn run_completion_command(args: &[String]) -> std::io::Result<i32> { let script = String::from_utf8(output).map_err(|err| { std::io::Error::new(std::io::ErrorKind::InvalidData, err.utf8_error()) })?; + crate::platform::begin_cli_output(); std::io::stdout().write_all(space_separated_zsh_long_options(&script).as_bytes())?; } else { + crate::platform::begin_cli_output(); generate(shell, &mut command, "herdr", &mut std::io::stdout()); } Ok(0) diff --git a/src/cli/spec.rs b/src/cli/spec.rs index b2b6cb03..134bf0bf 100644 --- a/src/cli/spec.rs +++ b/src/cli/spec.rs @@ -64,10 +64,14 @@ fn configure_help(command: Command, depth: usize) -> Command { pub(super) fn print_requested_help(args: &[String]) -> std::io::Result<bool> { let mut stdout = std::io::stdout().lock(); - write_requested_help(args, &mut stdout) + write_requested_help(args, &mut stdout, crate::platform::begin_cli_output) } -fn write_requested_help(args: &[String], output: &mut impl Write) -> std::io::Result<bool> { +fn write_requested_help( + args: &[String], + output: &mut impl Write, + before_write: impl FnOnce(), +) -> std::io::Result<bool> { let Some(help_index) = args .iter() .position(|arg| matches!(arg.as_str(), "--help" | "-h")) @@ -99,6 +103,7 @@ fn write_requested_help(args: &[String], output: &mut impl Write) -> std::io::Re } selected.set_bin_name(path.join(" ")); + before_write(); selected.write_long_help(&mut *output)?; writeln!(output)?; Ok(true) @@ -1083,7 +1088,7 @@ mod tests { args.push(flag.to_string()); let mut output = Vec::new(); assert!( - super::write_requested_help(&args, &mut output).unwrap(), + super::write_requested_help(&args, &mut output, || {}).unwrap(), "help was not handled for herdr {} {flag}", path.join(" ") ); @@ -1197,6 +1202,7 @@ mod tests { "--help".to_string(), ], &mut help, + || {}, ) .unwrap(); assert!(String::from_utf8(help) @@ -1304,7 +1310,7 @@ mod tests { args.push("--help".to_string()); let mut output = Vec::new(); assert!( - super::write_requested_help(&args, &mut output).unwrap(), + super::write_requested_help(&args, &mut output, || {}).unwrap(), "help was not handled for herdr {}", path.join(" ") ); diff --git a/src/main.rs b/src/main.rs index 36b1d021..56dbb8df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -614,6 +614,7 @@ fn main() -> io::Result<()> { } if args.iter().any(|a| a == "--help" || a == "-h") { + platform::begin_cli_output(); println!("herdr — terminal workspace manager for AI coding agents"); println!(); println!("Usage: herdr [options]"); @@ -729,16 +730,19 @@ fn main() -> io::Result<()> { } if args.iter().any(|a| a == "--version" || a == "-V") { + platform::begin_cli_output(); println!("herdr {}", crate::build_info::version()); return Ok(()); } if args.iter().any(|a| a == "--default-config") { + platform::begin_cli_output(); print!("{DEFAULT_CONFIG}"); return Ok(()); } if args.iter().any(|a| a == "--skill") { + platform::begin_cli_output(); print!("{SKILL}"); return Ok(()); } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index e3f9c7f4..a63b7565 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -198,6 +198,14 @@ pub(crate) struct RemoteSshConfigPaths { #[cfg(unix)] mod unix_common; +#[cfg(unix)] +pub(crate) use unix_common::{begin_cli_output, end_cli_output}; + +#[cfg(not(unix))] +pub(crate) fn begin_cli_output() {} + +#[cfg(not(unix))] +pub(crate) fn end_cli_output() {} #[cfg(target_os = "linux")] mod linux; diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index fa7b6d20..b997049f 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -1,5 +1,24 @@ use std::path::{Path, PathBuf}; +fn set_sigpipe_disposition(handler: libc::sighandler_t) { + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = handler; + unsafe { + libc::sigemptyset(&mut action.sa_mask); + // Rust starts with SIGPIPE ignored. If this best-effort transition + // fails, stdout retains the existing Rust behavior. + libc::sigaction(libc::SIGPIPE, &action, std::ptr::null_mut()); + } +} + +pub(crate) fn begin_cli_output() { + set_sigpipe_disposition(libc::SIG_DFL); +} + +pub(crate) fn end_cli_output() { + set_sigpipe_disposition(libc::SIG_IGN); +} + pub(crate) fn remote_ssh_config_paths() -> super::RemoteSshConfigPaths { super::RemoteSshConfigPaths { user_config: std::env::var_os("HOME") |
