From 485676095e6bc2aaf889e401fa140b130259626f Mon Sep 17 00:00:00 2001 From: Malte Tammena Date: Wed, 27 Oct 2021 15:03:04 +0200 Subject: [PATCH] Fix #15, don't panic on closed pipes Logging will now use stderr instead of stdout. --- config.toml | 2 +- src/cache/dummy.rs | 15 +------------ src/cache/tests.rs | 2 +- src/canteen/mod.rs | 7 +++---- src/main.rs | 50 +++++++++++++++++++++++++++++++------------- src/meal/complete.rs | 46 +++++++++++++++++++++------------------- src/meal/mod.rs | 12 +++++------ src/tag.rs | 10 ++++----- 8 files changed, 76 insertions(+), 68 deletions(-) diff --git a/config.toml b/config.toml index edd436f..d97849b 100644 --- a/config.toml +++ b/config.toml @@ -9,7 +9,7 @@ # If omitted, `$XDG_CONFIG_HOME/mensa/config.toml` or # `$HOME/.config/mensa/config.toml` (if $XDG_CONFIG_HOME is unset) # are checked. -# +# # All options can also be specified on the command line or in the environment # 1) CLI flags take precedence over # 2) ENVIRONMENT VARIABLES, which overwrite diff --git a/src/cache/dummy.rs b/src/cache/dummy.rs index 3ff97cc..77484c9 100644 --- a/src/cache/dummy.rs +++ b/src/cache/dummy.rs @@ -38,10 +38,6 @@ impl Cache for DummyCache { let entry = read .get(&path) .expect("BUG: Metadata exists, but entry does not!"); - eprintln!( - "Cache read for {:?}\n-> Returning: {:#?}", - meta.integrity, entry.text - ); Ok(entry.text.clone()) } @@ -56,10 +52,6 @@ impl Cache for DummyCache { text: text.to_owned(), }, ); - eprintln!( - "Cache write to {:?}\n-> Headers: {:#?}\n-> Content: {:#?}", - url, headers, text - ); Ok(()) } @@ -67,7 +59,6 @@ impl Cache for DummyCache { let hash = path_from_key(url); let read = self.content.read().expect("Reading cache failed"); let entry = read.get(&hash); - eprintln!("Cache Metadata for {:?}: {:?}", url, entry); match entry { Some(entry) => Ok(Some(clone_metadata(&entry.meta))), None => Ok(None), @@ -76,7 +67,6 @@ impl Cache for DummyCache { fn clear(&self) -> Result<()> { self.content.write().expect("Writing cache failed").clear(); - eprintln!("Cache cleared"); Ok(()) } @@ -92,9 +82,7 @@ impl Cache for DummyCache { fn path_from_key(key: &str) -> String { let integrity = Integrity::from(key); - let path = path_from_integrity(&integrity); - eprintln!("Hashing key {:?} -> {:#?}", key, path); - path + path_from_integrity(&integrity) } fn path_from_integrity(integrity: &Integrity) -> String { @@ -102,7 +90,6 @@ fn path_from_integrity(integrity: &Integrity) -> String { let (algorithm, digest) = integrity.to_hex(); path += &algorithm.to_string(); path += &digest; - eprintln!("Hashing integrity {:?} -> {:#?}", integrity, path); path } diff --git a/src/cache/tests.rs b/src/cache/tests.rs index a20a78a..940cbbc 100644 --- a/src/cache/tests.rs +++ b/src/cache/tests.rs @@ -14,7 +14,7 @@ fn print_cache_list(header: &'static str) -> Result<()> { CACHE.list()?.iter().for_each(|meta| { let age_ms = meta.time; let cache_age = chrono::Utc.timestamp((age_ms / 1000) as i64, (age_ms % 1000) as u32); - eprintln!( + println!( "| - {}\n| SIZE: {}\n| AGE: {}", meta.key, meta.size, cache_age ) diff --git a/src/canteen/mod.rs b/src/canteen/mod.rs index cf14ada..6ce45b7 100644 --- a/src/canteen/mod.rs +++ b/src/canteen/mod.rs @@ -99,13 +99,12 @@ impl Canteen { .initial_indent(ADRESS_INDENT) .subsequent_indent(ADRESS_INDENT), ); - println!( + try_println!( "{} {}\n{}", color!(format!("{:>4}", self.id); bold, bright_yellow), color!(self.meta()?.name; bold), color!(address; bright_black), - ); - Ok(()) + ) } pub fn id(&self) -> CanteenId { @@ -132,7 +131,7 @@ impl Canteen { Self::print_all_json(canteens) } else { for canteen in canteens { - println!(); + try_println!()?; canteen.print()?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index a8f6941..8c1e63f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,7 +57,7 @@ //! //! ### Examples //! -//! #### +//! #### //!
//! Meals on monday (Click me!) //! @@ -70,7 +70,7 @@ //! ┊ //! ┊ ╭───╴Bohnengemüse //! ┊ ├─╴Gemüsebeilage 🌱 -//! ┊ ╰╴( 0.55€ ) +//! ┊ ╰╴( 0.55€ ) //! ... //! ``` //!
@@ -95,13 +95,13 @@ //! //! ```console //! $ mensa tags -//! +//! //! 0 Acidifier //! Contains artificial acidifier -//! +//! //! 1 Alcohol //! Contains alcohol -//! +//! //! 2 Antioxidant //! Contains an antioxidant //! ... @@ -113,7 +113,7 @@ //! //! ```console //! $ mensa meals close --date sun -//! +//! //! Leipzig, Cafeteria Dittrichring //! ┊ //! ┊ ╭───╴Vegetarisch gefüllte Zucchini @@ -121,7 +121,7 @@ //! ┊ ├╴Rucola-Kartoffelpüree //! ┊ ├╴Tomaten-Ratatouille-Soße //! ┊ ╰╴( 2.65€ ) 2 11 12 19 -//! +//! //! Leipzig, Mensa am Park //! ┊ //! ┊ ╭───╴Apfelrotkohl @@ -147,12 +147,14 @@ //! - `$HOME/Library/Application Support/mensa/config.toml` on **macOS**, //! - `{FOLDERID_RoamingAppData}\mensa\config.toml` on **Windows** +use std::io; + use cache::Cache; use chrono::Duration; use directories_next::ProjectDirs; use lazy_static::lazy_static; use serde::Serialize; -use tracing::error; +use tracing::{error, info}; /// Colorizes the output. /// @@ -211,6 +213,18 @@ macro_rules! if_plain { }; } +/// Safer `println` which doesn't panic, but errors. +macro_rules! try_println { + () => { + try_println!("\n") + }; + ($str:literal $(, $args:expr )* $(,)?) => ({ + use std::io::Write; + writeln!(::std::io::stdout(), $str, $( $args ),* ) + .map_err(|why| crate::error::Error::Io(why, "printing")) + }) +} + mod cache; mod canteen; mod config; @@ -240,17 +254,25 @@ lazy_static! { } fn main() -> Result<()> { - let res = real_main(); - match res { - Ok(_) => {} - Err(ref why) => error!("{}", why), + match real_main() { + Ok(_) => Ok(()), + // Ignore broken pipe errors, but log them + Err(Error::Io(err, _)) if err.kind() == io::ErrorKind::BrokenPipe => { + info!("Pipe was closed"); + Ok(()) + } + Err(why) => { + error!("{}", why); + Err(why) + } } - res } fn real_main() -> Result<()> { // Initialize logger - tracing_subscriber::fmt::init(); + tracing_subscriber::FmtSubscriber::builder() + .with_writer(::std::io::stderr) + .init(); // Clear cache if requested if CONF.args.clear_cache { CACHE.clear()?; diff --git a/src/meal/complete.rs b/src/meal/complete.rs index f6f9eb9..1bdeb87 100644 --- a/src/meal/complete.rs +++ b/src/meal/complete.rs @@ -5,7 +5,7 @@ use lazy_static::lazy_static; use serde::Serialize; use unicode_width::UnicodeWidthStr; -use crate::get_sane_terminal_dimensions; +use crate::{error::Result, get_sane_terminal_dimensions}; use super::{MealId, Meta, PRE}; @@ -27,39 +27,40 @@ pub struct MealComplete<'c> { impl<'c> MealComplete<'c> { /// Print this [`MealComplete`] to the terminal. - pub fn print(&self, highlight: bool) { + pub fn print(&self, highlight: bool) -> Result<()> { let (width, _height) = get_sane_terminal_dimensions(); // Print meal name - self.print_name_to_terminal(width, highlight); + self.print_name_to_terminal(width, highlight)?; // Get notes, i.e. allergenes, descriptions, tags - self.print_category_and_primary_tags(highlight); - self.print_descriptions(width, highlight); - self.print_price_and_secondary_tags(highlight); + self.print_category_and_primary_tags(highlight)?; + self.print_descriptions(width, highlight)?; + self.print_price_and_secondary_tags(highlight) } - fn print_name_to_terminal(&self, width: usize, highlight: bool) { + fn print_name_to_terminal(&self, width: usize, highlight: bool) -> Result<()> { let max_name_width = width - NAME_PRE.width() - PRE.width(); let mut name_parts = textwrap::wrap(&self.meta.name, max_name_width).into_iter(); // There will always be a first part of the splitted string let first_name_part = name_parts.next().unwrap(); - println!( + try_println!( "{}{}{}", *PRE, hl_if(highlight, *NAME_PRE), color!(hl_if(highlight, first_name_part); bold), - ); + )?; for name_part in name_parts { let name_part = hl_if(highlight, name_part); - println!( + try_println!( "{}{}{}", *PRE, hl_if(highlight, *NAME_CONTINUE_PRE), color!(name_part; bold), - ); + )?; } + Ok(()) } - fn print_category_and_primary_tags(&self, highlight: bool) { + fn print_category_and_primary_tags(&self, highlight: bool) -> Result<()> { let mut tag_str = self .meta .tags @@ -69,39 +70,40 @@ impl<'c> MealComplete<'c> { let tag_str_colored = if_plain!(color!(tag_str.join(" "); bright_black), tag_str.join(", ")); let comma_if_plain = if_plain!("", ","); - println!( + try_println!( "{}{}{}{} {}", *PRE, hl_if(highlight, *CATEGORY_PRE), color!(self.meta.category; bright_blue), color!(comma_if_plain; bright_black), tag_str_colored - ); + ) } - fn print_descriptions(&self, width: usize, highlight: bool) { + fn print_descriptions(&self, width: usize, highlight: bool) -> Result<()> { let max_note_width = width - OTHER_NOTE_PRE.width() - PRE.width(); for note in &self.meta.descs { let mut note_parts = textwrap::wrap(note, max_note_width).into_iter(); // There will always be a first part in the splitted string - println!( + try_println!( "{}{}{}", *PRE, hl_if(highlight, *OTHER_NOTE_PRE), note_parts.next().unwrap() - ); + )?; for part in note_parts { - println!( + try_println!( "{}{}{}", *PRE, hl_if(highlight, *OTHER_NOTE_CONTINUE_PRE), part - ); + )?; } } + Ok(()) } - fn print_price_and_secondary_tags(&self, highlight: bool) { + fn print_price_and_secondary_tags(&self, highlight: bool) -> Result<()> { let prices = self.meta.prices.to_terminal_string(); let mut secondary: Vec<_> = self .meta @@ -111,13 +113,13 @@ impl<'c> MealComplete<'c> { .collect(); secondary.sort_unstable(); let secondary_str = secondary.iter().map(|tag| tag.as_id()).join(" "); - println!( + try_println!( "{}{}{} {}", *PRE, hl_if(highlight, *PRICES_PRE), prices, color!(secondary_str; bright_black), - ); + ) } } diff --git a/src/meal/mod.rs b/src/meal/mod.rs index c67145d..1e4c340 100644 --- a/src/meal/mod.rs +++ b/src/meal/mod.rs @@ -96,7 +96,7 @@ impl Meal { let day = CONF.date(); for canteen in canteens { let name = canteen.name()?; - println!("\n {}", color!(name; bright_black)); + try_println!("\n {}", color!(name; bright_black))?; match canteen.meals_at_mut(day)? { Some(meals) => { let mut printed_at_least_one_meal = false; @@ -104,18 +104,16 @@ impl Meal { let complete = meal.complete()?; if filter.is_match(&complete) { let is_fav = favs.is_non_empty_match(&complete); - println!("{}", *PRE); - complete.print(is_fav); + try_println!("{}", *PRE)?; + complete.print(is_fav)?; printed_at_least_one_meal = true; } } if !printed_at_least_one_meal { - println!("{} {}", *PRE, color!("no matching meals found"; dimmed)); + try_println!("{} {}", *PRE, color!("no matching meals found"; dimmed))? } } - None => { - println!("{} {}", *PRE, color!("closed"; dimmed)) - } + None => try_println!("{} {}", *PRE, color!("closed"; dimmed))?, } } Ok(()) diff --git a/src/tag.rs b/src/tag.rs index 2da750e..3467bdd 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -212,7 +212,7 @@ impl Tag { /// Print this tag. /// /// Does **not** respect `--json`, use [`Self::print_all`]. - pub fn print(&self) { + pub fn print(&self) -> Result<()> { let emoji = if CONF.args.plain && self.is_primary() { format!("{:>width$}", "-", width = ID_WIDTH) } else { @@ -231,12 +231,12 @@ impl Tag { .initial_indent(TEXT_INDENT) .subsequent_indent(TEXT_INDENT), ); - println!( + try_println!( "{} {}\n{}", color!(emoji; bright_yellow, bold), color!(self; bold), color!(description; bright_black), - ); + ) } /// Print all tags. @@ -245,8 +245,8 @@ impl Tag { Self::print_all_json() } else { for tag in Tag::iter() { - println!(); - tag.print(); + try_println!()?; + tag.print()?; } Ok(()) }