Fix broken pipe error on failing writes to stdout (#535)

Make sure that broken pipes (e.g. when a reader of a
pipe prematurely exits during execution) get handled gracefully.
This change also moves some error messages to stderr by using
eprintln.

More info: https://github.com/jez/as-tree/issues/15
This commit is contained in:
Matthias 2022-03-02 23:39:54 +01:00 committed by GitHub
parent 595a713b4b
commit 4c51fce22f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 40 additions and 14 deletions

View file

@ -1,3 +1,4 @@
use std::io::{self, Write};
use std::sync::Arc;
use indicatif::ProgressBar;
@ -69,10 +70,10 @@ where
let verbose = cfg.verbose;
async move {
while let Some(response) = recv_resp.recv().await {
show_progress(&pb, &response, verbose);
show_progress(&pb, &response, verbose)?;
stats.add(response);
}
(pb, stats)
Ok((pb, stats))
}
});
@ -93,7 +94,8 @@ where
// the show_results_task to finish
drop(send_req);
let (pb, stats) = show_results_task.await?;
let result: Result<(_, _)> = show_results_task.await?;
let (pb, stats) = result?;
// Note that print statements may interfere with the progress bar, so this
// must go before printing the stats
@ -139,7 +141,11 @@ async fn handle(client: &Client, cache: Arc<Cache>, request: Request) -> Respons
response
}
fn show_progress(progress_bar: &Option<ProgressBar>, response: &Response, verbose: bool) {
fn show_progress(
progress_bar: &Option<ProgressBar>,
response: &Response,
verbose: bool,
) -> Result<()> {
let out = color_response(&response.1);
if let Some(pb) = progress_bar {
pb.inc(1);
@ -149,8 +155,9 @@ fn show_progress(progress_bar: &Option<ProgressBar>, response: &Response, verbos
}
} else {
if (response.status().is_success() || response.status().is_excluded()) && !verbose {
return;
return Ok(());
}
println!("{out}");
writeln!(io::stdout(), "{out}")?;
}
Ok(())
}

View file

@ -62,11 +62,11 @@ use lychee_lib::Collector;
// required for apple silicon
use ring as _;
use anyhow::{Context, Result};
use anyhow::{Context, Error, Result};
use openssl_sys as _; // required for vendored-openssl feature
use ring as _;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::io::{self, BufRead, BufReader, ErrorKind, Write};
use std::sync::Arc;
use structopt::StructOpt;
@ -159,7 +159,7 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
let modified = metadata.modified().ok()?;
let elapsed = modified.elapsed().ok()?;
if elapsed > cfg.max_cache_age {
println!(
eprintln!(
"Cache is too old (age: {}, max age: {}). Discarding",
humantime::format_duration(elapsed),
humantime::format_duration(cfg.max_cache_age)
@ -173,7 +173,7 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
match cache {
Ok(cache) => Some(cache),
Err(e) => {
println!("Error while loading cache: {e}. Continuing without.");
eprintln!("Error while loading cache: {e}. Continuing without.");
None
}
}
@ -181,6 +181,8 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
/// Set up runtime and call lychee entrypoint
fn run_main() -> Result<i32> {
use std::process::exit;
let opts = load_config()?;
let runtime = match opts.config.threads {
Some(threads) => {
@ -194,7 +196,24 @@ fn run_main() -> Result<i32> {
None => tokio::runtime::Runtime::new()?,
};
runtime.block_on(run(&opts))
match runtime.block_on(run(&opts)) {
Err(e) if Some(ErrorKind::BrokenPipe) == underlying_io_error_kind(&e) => {
exit(ExitCode::Success as i32);
}
res => res,
}
}
/// Check if the given error can be traced back to an `io::ErrorKind`
/// This is helpful for troubleshooting the root cause of an error.
/// Code is taken from the anyhow documentation.
fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
for cause in error.chain() {
if let Some(io_error) = cause.downcast_ref::<io::Error>() {
return Some(io_error.kind());
}
}
None
}
/// Run lychee on the given inputs
@ -244,10 +263,10 @@ fn write_stats(stats: ResponseStats, cfg: &Config) -> Result<()> {
} else {
if cfg.verbose && !is_empty {
// separate summary from the verbose list of links above
println!();
writeln!(io::stdout())?;
}
// we assume that the formatted stats don't have a final newline
println!("{formatted}");
writeln!(io::stdout(), "{formatted}")?;
}
Ok(())
}

View file

@ -266,7 +266,7 @@ impl Input {
let content: InputContent = Self::path_content(&path).await?;
yield content;
}
Err(e) => println!("{e:?}"),
Err(e) => eprintln!("{e:?}"),
}
}
}