From 73e79ebd079d44df0f6b0beb9be74ba2a356cef7 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 2 Oct 2026 11:12:03 +0200 Subject: [PATCH] Distinguish console writes from other writes --- src/benchmark/mod.rs | 23 +++++++++++++---------- src/benchmark/scheduler.rs | 20 +++++++++++--------- src/error.rs | 4 ++++ src/export/mod.rs | 6 ++++-- src/main.rs | 21 +++++++-------------- src/output/mod.rs | 10 ++++++++++ 6 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/benchmark/mod.rs b/src/benchmark/mod.rs index 83d7858..b6c615d 100644 --- a/src/benchmark/mod.rs +++ b/src/benchmark/mod.rs @@ -13,6 +13,7 @@ use crate::options::{ CmdFailureAction, CommandOutputPolicy, ExecutorKind, Options, OutputStyleOption, }; use crate::outlier_detection::{modified_zscores, OUTLIER_THRESHOLD}; +use crate::output::console_writeln; use crate::output::format::{format_duration, format_duration_unit}; use crate::output::progress_bar::get_progress_bar; use crate::output::warnings::{OutlierWarningOptions, Warnings}; @@ -141,7 +142,7 @@ impl<'a> Benchmark<'a> { /// Run the benchmark for a single command pub fn run(&self) -> Result { if self.options.output_style != OutputStyleOption::Disabled { - writeln!( + console_writeln!( io::stdout(), "{}{}: {}", "Benchmark ".bold(), @@ -360,9 +361,10 @@ impl<'a> Benchmark<'a> { let system_str = format_duration(system_mean, Some(time_unit)); if self.options.output_style != OutputStyleOption::Disabled { + let mut stdout = io::stdout().lock(); if times_real.len() == 1 { - writeln!( - io::stdout(), + console_writeln!( + stdout, " Time ({} ≡): {:>8} {:>8} [User: {}, System: {}]", "abs".green().bold(), mean_str.green().bold(), @@ -373,8 +375,8 @@ impl<'a> Benchmark<'a> { } else { let stddev_str = format_duration(t_stddev.unwrap(), Some(time_unit)); - writeln!( - io::stdout(), + console_writeln!( + stdout, " Time ({} ± {}): {:>8} ± {:>8} [User: {}, System: {}]", "mean".green().bold(), "σ".green(), @@ -384,8 +386,8 @@ impl<'a> Benchmark<'a> { system_str.blue() )?; - writeln!( - io::stdout(), + console_writeln!( + stdout, " Range ({} … {}): {:>8} … {:>8} {}", "min".cyan(), "max".purple(), @@ -435,15 +437,16 @@ impl<'a> Benchmark<'a> { } if !warnings.is_empty() { - writeln!(io::stderr(), " ")?; + let mut stderr = io::stderr().lock(); + console_writeln!(stderr, " ")?; for warning in &warnings { - writeln!(io::stderr(), " {}: {}", "Warning".yellow(), warning)?; + console_writeln!(stderr, " {}: {}", "Warning".yellow(), warning)?; } } if self.options.output_style != OutputStyleOption::Disabled { - writeln!(io::stdout(), " ")?; + console_writeln!(io::stdout(), " ")?; } self.run_cleanup_command(self.command.get_parameters().iter().cloned(), output_policy)?; diff --git a/src/benchmark/scheduler.rs b/src/benchmark/scheduler.rs index 9dc56b2..ce5fd81 100644 --- a/src/benchmark/scheduler.rs +++ b/src/benchmark/scheduler.rs @@ -8,6 +8,7 @@ use std::io::{self, Write}; use crate::command::{Command, Commands}; use crate::export::ExportManager; use crate::options::{ExecutorKind, Options, OutputStyleOption, SortOrder}; +use crate::output::console_writeln; use anyhow::Result; @@ -80,15 +81,16 @@ impl<'a> Scheduler<'a> { reference, self.options.sort_order_speed_comparison, ) { + let mut stdout = io::stdout().lock(); match self.options.sort_order_speed_comparison { SortOrder::MeanTime => { - writeln!(io::stdout(), "{}", "Summary".bold())?; + console_writeln!(stdout, "{}", "Summary".bold())?; let reference = annotated_results.iter().find(|r| r.is_reference).unwrap(); let others = annotated_results.iter().filter(|r| !r.is_reference); - writeln!( - io::stdout(), + console_writeln!( + stdout, " {} ran", reference.result.command_with_unused_parameters.cyan() )?; @@ -116,8 +118,8 @@ impl<'a> Scheduler<'a> { stddev ), }; - writeln!( - io::stdout(), + console_writeln!( + stdout, "{} {}", comparator, item.result.command_with_unused_parameters.magenta() @@ -125,11 +127,11 @@ impl<'a> Scheduler<'a> { } } SortOrder::Command => { - writeln!(io::stdout(), "{}", "Relative speed comparison".bold())?; + console_writeln!(stdout, "{}", "Relative speed comparison".bold())?; for item in annotated_results { - writeln!( - io::stdout(), + console_writeln!( + stdout, " {}{} {}", format!("{:10.2}", item.relative_speed).bold().green(), if item.is_reference { @@ -145,7 +147,7 @@ impl<'a> Scheduler<'a> { } } } else { - writeln!( + console_writeln!( io::stderr(), "{}: The benchmark comparison could not be computed as some benchmark times are zero. \ This could be caused by background interference during the initial calibration phase \ diff --git a/src/error.rs b/src/error.rs index 53d39a5..9d1f482 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,10 @@ use std::num::{self, ParseFloatError, ParseIntError}; use rust_decimal::Error as DecimalError; use thiserror::Error; +#[derive(Debug, Error)] +#[error(transparent)] +pub struct ConsoleOutputError(pub std::io::Error); + #[derive(Debug, Error)] pub enum ParameterScanError { #[error("Error while parsing parameter scan arguments ({0})")] diff --git a/src/export/mod.rs b/src/export/mod.rs index 8957e26..e13c1d6 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -18,6 +18,7 @@ use self::orgmode::OrgmodeExporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; +use crate::output::console_writeln; use crate::util::units::Unit; use anyhow::{Context, Result}; @@ -144,8 +145,9 @@ impl ExportManager { } ExportTarget::Stdout => { if !intermediate { - writeln!(io::stdout())?; - writeln!(io::stdout(), "{}", String::from_utf8(content()?).unwrap())?; + let mut stdout = io::stdout().lock(); + console_writeln!(stdout)?; + console_writeln!(stdout, "{}", String::from_utf8(content()?).unwrap())?; } } } diff --git a/src/main.rs b/src/main.rs index 06c5d9b..691e693 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::io::{self, Write}; use benchmark::scheduler::Scheduler; use cli::get_cli_arguments; use command::Commands; +use error::ConsoleOutputError; use export::ExportManager; use options::Options; @@ -51,26 +52,18 @@ fn run() -> Result<()> { Ok(()) } -/// Check whether `error` or one of its causes is an `io::Error` of kind -/// `BrokenPipe`. This happens when the process on the reading end of a -/// pipe (e.g. `hyperfine … | head -n 1`) terminates before hyperfine is -/// done writing its results. -fn caused_by_broken_pipe(error: &anyhow::Error) -> bool { - error.chain().any(|cause| { - cause - .downcast_ref::() - .is_some_and(|e| e.kind() == io::ErrorKind::BrokenPipe) - }) +fn caused_by_broken_console_pipe(error: &anyhow::Error) -> bool { + error + .downcast_ref::() + .is_some_and(|e| e.0.kind() == io::ErrorKind::BrokenPipe) } fn main() { match run() { Ok(_) => {} Err(e) => { - // When writing to a closed pipe, exit quietly with a zero exit - // code — the same behavior that non-Rust CLI tools exhibit when - // they are terminated by `SIGPIPE`. - if caused_by_broken_pipe(&e) { + // Exit quietly when a reader stops consuming console output early. + if caused_by_broken_console_pipe(&e) { std::process::exit(0); } // The write to stderr can itself fail if stderr is closed; the diff --git a/src/output/mod.rs b/src/output/mod.rs index d80ffee..aa2f85b 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,3 +1,13 @@ pub mod format; pub mod progress_bar; pub mod warnings; + +/// Mark console write errors so broken pipes can be handled separately from file errors. +macro_rules! console_writeln { + ($writer:expr $(, $($args:tt)*)?) => { + writeln!($writer $(, $($args)*)?) + .map_err($crate::error::ConsoleOutputError) + }; +} + +pub(crate) use console_writeln;