mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-10-28 16:18:57 +00:00
Handle broken pipe errors on stdout gracefully
Replace `println!` calls on the results paths with `writeln!` on `io::stdout()` and propagate the `io::Error` to `main`, where `ErrorKind::BrokenPipe` is turned into a quiet `exit(0)`. This matches the conventional behavior of command-line tools when their output pipe is closed early (e.g. `hyperfine ... | head -n 0`), and fixes panics in all run-output paths: the per-benchmark header and results, the speed comparison summary, and `-`-style exports to stdout. The warnings and "Note" outputs to stderr are converted the same way. The earlier fix for #453 only covered the `-h`/`--help` output. A regression test runs hyperfine with a pre-closed stdout pipe. Assisted-by: AI tooling (patch generated and verified automatically)
This commit is contained in:
1 parent
4d073e08ae
commit
47339b0862
5 files changed
+104
-30
No files matched your search
+16
-11
@@ -5,6 +5,7 @@ pub mod scheduler;
|
||||
pub mod timing_result;
|
||||
|
||||
use std::cmp;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::benchmark::executor::BenchmarkIteration;
|
||||
use crate::command::Command;
|
||||
@@ -140,12 +141,13 @@ impl<'a> Benchmark<'a> {
|
||||
/// Run the benchmark for a single command
|
||||
pub fn run(&self) -> Result<BenchmarkResult> {
|
||||
if self.options.output_style != OutputStyleOption::Disabled {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"{}{}: {}",
|
||||
"Benchmark ".bold(),
|
||||
(self.number + 1).to_string().bold(),
|
||||
self.command.get_name_with_unused_parameters(),
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut times_real: Vec<Second> = vec![];
|
||||
@@ -359,18 +361,20 @@ impl<'a> Benchmark<'a> {
|
||||
|
||||
if self.options.output_style != OutputStyleOption::Disabled {
|
||||
if times_real.len() == 1 {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
" Time ({} ≡): {:>8} {:>8} [User: {}, System: {}]",
|
||||
"abs".green().bold(),
|
||||
mean_str.green().bold(),
|
||||
" ", // alignment
|
||||
user_str.blue(),
|
||||
system_str.blue()
|
||||
);
|
||||
)?;
|
||||
} else {
|
||||
let stddev_str = format_duration(t_stddev.unwrap(), Some(time_unit));
|
||||
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
" Time ({} ± {}): {:>8} ± {:>8} [User: {}, System: {}]",
|
||||
"mean".green().bold(),
|
||||
"σ".green(),
|
||||
@@ -378,16 +382,17 @@ impl<'a> Benchmark<'a> {
|
||||
stddev_str.green(),
|
||||
user_str.blue(),
|
||||
system_str.blue()
|
||||
);
|
||||
)?;
|
||||
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
" Range ({} … {}): {:>8} … {:>8} {}",
|
||||
"min".cyan(),
|
||||
"max".purple(),
|
||||
min_str.cyan(),
|
||||
max_str.purple(),
|
||||
num_str.dimmed()
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,15 +435,15 @@ impl<'a> Benchmark<'a> {
|
||||
}
|
||||
|
||||
if !warnings.is_empty() {
|
||||
eprintln!(" ");
|
||||
writeln!(io::stderr(), " ")?;
|
||||
|
||||
for warning in &warnings {
|
||||
eprintln!(" {}: {}", "Warning".yellow(), warning);
|
||||
writeln!(io::stderr(), " {}: {}", "Warning".yellow(), warning)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.options.output_style != OutputStyleOption::Disabled {
|
||||
println!(" ");
|
||||
writeln!(io::stdout(), " ")?;
|
||||
}
|
||||
|
||||
self.run_cleanup_command(self.command.get_parameters().iter().cloned(), output_policy)?;
|
||||
|
||||
+20
-13
@@ -3,6 +3,7 @@ use super::executor::{Executor, MockExecutor, RawExecutor, ShellExecutor};
|
||||
use super::{relative_speed, Benchmark};
|
||||
use colored::*;
|
||||
use std::cmp::Ordering;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::command::{Command, Commands};
|
||||
use crate::export::ExportManager;
|
||||
@@ -58,13 +59,13 @@ impl<'a> Scheduler<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_relative_speed_comparison(&self) {
|
||||
pub fn print_relative_speed_comparison(&self) -> Result<()> {
|
||||
if self.options.output_style == OutputStyleOption::Disabled {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.results.len() < 2 {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reference = self
|
||||
@@ -81,15 +82,16 @@ impl<'a> Scheduler<'a> {
|
||||
) {
|
||||
match self.options.sort_order_speed_comparison {
|
||||
SortOrder::MeanTime => {
|
||||
println!("{}", "Summary".bold());
|
||||
writeln!(io::stdout(), "{}", "Summary".bold())?;
|
||||
|
||||
let reference = annotated_results.iter().find(|r| r.is_reference).unwrap();
|
||||
let others = annotated_results.iter().filter(|r| !r.is_reference);
|
||||
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
" {} ran",
|
||||
reference.result.command_with_unused_parameters.cyan()
|
||||
);
|
||||
)?;
|
||||
|
||||
for item in others {
|
||||
let stddev = if let Some(stddev) = item.relative_speed_stddev {
|
||||
@@ -114,18 +116,20 @@ impl<'a> Scheduler<'a> {
|
||||
stddev
|
||||
),
|
||||
};
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
"{} {}",
|
||||
comparator,
|
||||
item.result.command_with_unused_parameters.magenta()
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
SortOrder::Command => {
|
||||
println!("{}", "Relative speed comparison".bold());
|
||||
writeln!(io::stdout(), "{}", "Relative speed comparison".bold())?;
|
||||
|
||||
for item in annotated_results {
|
||||
println!(
|
||||
writeln!(
|
||||
io::stdout(),
|
||||
" {}{} {}",
|
||||
format!("{:10.2}", item.relative_speed).bold().green(),
|
||||
if item.is_reference {
|
||||
@@ -136,12 +140,13 @@ impl<'a> Scheduler<'a> {
|
||||
" ".into()
|
||||
},
|
||||
item.result.command_with_unused_parameters,
|
||||
);
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
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 \
|
||||
of hyperfine, in combination with very fast commands (faster than a few milliseconds). \
|
||||
@@ -149,8 +154,10 @@ impl<'a> Scheduler<'a> {
|
||||
--shell=none/-N option. If it does not help either, you command is most likely too fast \
|
||||
to be accurately benchmarked by hyperfine.",
|
||||
"Note".bold().red()
|
||||
);
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn final_export(&self) -> Result<()> {
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::io::{self, Write};
|
||||
|
||||
mod asciidoc;
|
||||
mod csv;
|
||||
@@ -144,8 +144,8 @@ impl ExportManager {
|
||||
}
|
||||
ExportTarget::Stdout => {
|
||||
if !intermediate {
|
||||
println!();
|
||||
println!("{}", String::from_utf8(content()?).unwrap());
|
||||
writeln!(io::stdout())?;
|
||||
writeln!(io::stdout(), "{}", String::from_utf8(content()?).unwrap())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-2
@@ -4,6 +4,7 @@
|
||||
)]
|
||||
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use benchmark::scheduler::Scheduler;
|
||||
use cli::get_cli_arguments;
|
||||
@@ -44,17 +45,37 @@ fn run() -> Result<()> {
|
||||
|
||||
let mut scheduler = Scheduler::new(&commands, &options, &export_manager);
|
||||
scheduler.run_benchmarks()?;
|
||||
scheduler.print_relative_speed_comparison();
|
||||
scheduler.print_relative_speed_comparison()?;
|
||||
scheduler.final_export()?;
|
||||
|
||||
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::<io::Error>()
|
||||
.is_some_and(|e| e.kind() == io::ErrorKind::BrokenPipe)
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("{} {:#}", "Error:".red(), 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) {
|
||||
std::process::exit(0);
|
||||
}
|
||||
// The write to stderr can itself fail if stderr is closed; the
|
||||
// error message cannot be shown in that case anyway.
|
||||
let _ = writeln!(io::stderr(), "{} {:#}", "Error:".red(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod common;
|
||||
use common::hyperfine;
|
||||
use common::{hyperfine, hyperfine_raw_command};
|
||||
|
||||
use predicates::prelude::*;
|
||||
|
||||
@@ -33,6 +33,47 @@ fn one_run_is_supported() {
|
||||
.success();
|
||||
}
|
||||
|
||||
/// Regression test: hyperfine must not panic when writing to a closed
|
||||
/// stdout pipe (e.g. `hyperfine ... | head -n 1` or a downstream process
|
||||
/// that terminates early). A `BrokenPipe` error should result in a quiet
|
||||
/// exit with code 0 instead of a `println!` panic.
|
||||
#[test]
|
||||
fn exits_quietly_when_stdout_is_closed() {
|
||||
use std::process::Stdio;
|
||||
|
||||
for args in [
|
||||
vec!["--runs=1", "echo dummy benchmark"],
|
||||
vec!["--runs=1", "--export-json=-", "echo dummy benchmark"],
|
||||
vec!["--runs=1", "echo dummy benchmark", "echo second benchmark"],
|
||||
] {
|
||||
// A pipe whose read end has been closed before hyperfine is even
|
||||
// started: every write to it will fail with `BrokenPipe`.
|
||||
let (reader, writer) = std::io::pipe().expect("failed to create pipe");
|
||||
drop(reader);
|
||||
|
||||
let output = hyperfine_raw_command()
|
||||
.args(&args)
|
||||
.stdout(writer)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to spawn hyperfine")
|
||||
.wait_with_output()
|
||||
.expect("failed to wait for hyperfine");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
assert!(
|
||||
!stderr.contains("panicked"),
|
||||
"hyperfine panicked while writing to a closed pipe:\n{}",
|
||||
stderr
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"hyperfine did not exit cleanly; stderr:\n{}",
|
||||
stderr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_run_commands_without_a_shell() {
|
||||
hyperfine()
|
||||
|
||||
Reference in new issue
Block a user