From 4c51fce22f65c2c8cd7abcb412cb9c4b2ae3e3cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Mar 2022 23:39:54 +0100 Subject: [PATCH] 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 --- lychee-bin/src/commands/check.rs | 19 ++++++++++++------ lychee-bin/src/main.rs | 33 +++++++++++++++++++++++++------- lychee-lib/src/types/input.rs | 2 +- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lychee-bin/src/commands/check.rs b/lychee-bin/src/commands/check.rs index cbe8464..2df406d 100644 --- a/lychee-bin/src/commands/check.rs +++ b/lychee-bin/src/commands/check.rs @@ -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, request: Request) -> Respons response } -fn show_progress(progress_bar: &Option, response: &Response, verbose: bool) { +fn show_progress( + progress_bar: &Option, + 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, response: &Response, verbos } } else { if (response.status().is_success() || response.status().is_excluded()) && !verbose { - return; + return Ok(()); } - println!("{out}"); + writeln!(io::stdout(), "{out}")?; } + Ok(()) } diff --git a/lychee-bin/src/main.rs b/lychee-bin/src/main.rs index 3badf9d..9364d99 100644 --- a/lychee-bin/src/main.rs +++ b/lychee-bin/src/main.rs @@ -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 { 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 { 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 { /// Set up runtime and call lychee entrypoint fn run_main() -> Result { + use std::process::exit; + let opts = load_config()?; let runtime = match opts.config.threads { Some(threads) => { @@ -194,7 +196,24 @@ fn run_main() -> Result { 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 { + for cause in error.chain() { + if let Some(io_error) = cause.downcast_ref::() { + 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(()) } diff --git a/lychee-lib/src/types/input.rs b/lychee-lib/src/types/input.rs index 374d329..1d4eadd 100644 --- a/lychee-lib/src/types/input.rs +++ b/lychee-lib/src/types/input.rs @@ -266,7 +266,7 @@ impl Input { let content: InputContent = Self::path_content(&path).await?; yield content; } - Err(e) => println!("{e:?}"), + Err(e) => eprintln!("{e:?}"), } } }