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 std::sync::Arc;
use indicatif::ProgressBar; use indicatif::ProgressBar;
@ -69,10 +70,10 @@ where
let verbose = cfg.verbose; let verbose = cfg.verbose;
async move { async move {
while let Some(response) = recv_resp.recv().await { while let Some(response) = recv_resp.recv().await {
show_progress(&pb, &response, verbose); show_progress(&pb, &response, verbose)?;
stats.add(response); stats.add(response);
} }
(pb, stats) Ok((pb, stats))
} }
}); });
@ -93,7 +94,8 @@ where
// the show_results_task to finish // the show_results_task to finish
drop(send_req); 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 // Note that print statements may interfere with the progress bar, so this
// must go before printing the stats // must go before printing the stats
@ -139,7 +141,11 @@ async fn handle(client: &Client, cache: Arc<Cache>, request: Request) -> Respons
response 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); let out = color_response(&response.1);
if let Some(pb) = progress_bar { if let Some(pb) = progress_bar {
pb.inc(1); pb.inc(1);
@ -149,8 +155,9 @@ fn show_progress(progress_bar: &Option<ProgressBar>, response: &Response, verbos
} }
} else { } else {
if (response.status().is_success() || response.status().is_excluded()) && !verbose { 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 // required for apple silicon
use ring as _; use ring as _;
use anyhow::{Context, Result}; use anyhow::{Context, Error, Result};
use openssl_sys as _; // required for vendored-openssl feature use openssl_sys as _; // required for vendored-openssl feature
use ring as _; use ring as _;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{BufRead, BufReader}; use std::io::{self, BufRead, BufReader, ErrorKind, Write};
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
@ -159,7 +159,7 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
let modified = metadata.modified().ok()?; let modified = metadata.modified().ok()?;
let elapsed = modified.elapsed().ok()?; let elapsed = modified.elapsed().ok()?;
if elapsed > cfg.max_cache_age { if elapsed > cfg.max_cache_age {
println!( eprintln!(
"Cache is too old (age: {}, max age: {}). Discarding", "Cache is too old (age: {}, max age: {}). Discarding",
humantime::format_duration(elapsed), humantime::format_duration(elapsed),
humantime::format_duration(cfg.max_cache_age) humantime::format_duration(cfg.max_cache_age)
@ -173,7 +173,7 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
match cache { match cache {
Ok(cache) => Some(cache), Ok(cache) => Some(cache),
Err(e) => { Err(e) => {
println!("Error while loading cache: {e}. Continuing without."); eprintln!("Error while loading cache: {e}. Continuing without.");
None None
} }
} }
@ -181,6 +181,8 @@ fn load_cache(cfg: &Config) -> Option<Cache> {
/// Set up runtime and call lychee entrypoint /// Set up runtime and call lychee entrypoint
fn run_main() -> Result<i32> { fn run_main() -> Result<i32> {
use std::process::exit;
let opts = load_config()?; let opts = load_config()?;
let runtime = match opts.config.threads { let runtime = match opts.config.threads {
Some(threads) => { Some(threads) => {
@ -194,7 +196,24 @@ fn run_main() -> Result<i32> {
None => tokio::runtime::Runtime::new()?, 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 /// Run lychee on the given inputs
@ -244,10 +263,10 @@ fn write_stats(stats: ResponseStats, cfg: &Config) -> Result<()> {
} else { } else {
if cfg.verbose && !is_empty { if cfg.verbose && !is_empty {
// separate summary from the verbose list of links above // 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 // we assume that the formatted stats don't have a final newline
println!("{formatted}"); writeln!(io::stdout(), "{formatted}")?;
} }
Ok(()) Ok(())
} }

View file

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