Fix #15, don't panic on closed pipes

Logging will now use stderr instead of stdout.
This commit is contained in:
Malte Tammena 2021-10-27 15:03:04 +02:00
parent 13ed33f51f
commit 485676095e
8 changed files with 76 additions and 68 deletions

View file

@ -9,7 +9,7 @@
# If omitted, `$XDG_CONFIG_HOME/mensa/config.toml` or # If omitted, `$XDG_CONFIG_HOME/mensa/config.toml` or
# `$HOME/.config/mensa/config.toml` (if $XDG_CONFIG_HOME is unset) # `$HOME/.config/mensa/config.toml` (if $XDG_CONFIG_HOME is unset)
# are checked. # are checked.
# #
# All options can also be specified on the command line or in the environment # All options can also be specified on the command line or in the environment
# 1) CLI flags take precedence over # 1) CLI flags take precedence over
# 2) ENVIRONMENT VARIABLES, which overwrite # 2) ENVIRONMENT VARIABLES, which overwrite

15
src/cache/dummy.rs vendored
View file

@ -38,10 +38,6 @@ impl Cache for DummyCache {
let entry = read let entry = read
.get(&path) .get(&path)
.expect("BUG: Metadata exists, but entry does not!"); .expect("BUG: Metadata exists, but entry does not!");
eprintln!(
"Cache read for {:?}\n-> Returning: {:#?}",
meta.integrity, entry.text
);
Ok(entry.text.clone()) Ok(entry.text.clone())
} }
@ -56,10 +52,6 @@ impl Cache for DummyCache {
text: text.to_owned(), text: text.to_owned(),
}, },
); );
eprintln!(
"Cache write to {:?}\n-> Headers: {:#?}\n-> Content: {:#?}",
url, headers, text
);
Ok(()) Ok(())
} }
@ -67,7 +59,6 @@ impl Cache for DummyCache {
let hash = path_from_key(url); let hash = path_from_key(url);
let read = self.content.read().expect("Reading cache failed"); let read = self.content.read().expect("Reading cache failed");
let entry = read.get(&hash); let entry = read.get(&hash);
eprintln!("Cache Metadata for {:?}: {:?}", url, entry);
match entry { match entry {
Some(entry) => Ok(Some(clone_metadata(&entry.meta))), Some(entry) => Ok(Some(clone_metadata(&entry.meta))),
None => Ok(None), None => Ok(None),
@ -76,7 +67,6 @@ impl Cache for DummyCache {
fn clear(&self) -> Result<()> { fn clear(&self) -> Result<()> {
self.content.write().expect("Writing cache failed").clear(); self.content.write().expect("Writing cache failed").clear();
eprintln!("Cache cleared");
Ok(()) Ok(())
} }
@ -92,9 +82,7 @@ impl Cache for DummyCache {
fn path_from_key(key: &str) -> String { fn path_from_key(key: &str) -> String {
let integrity = Integrity::from(key); let integrity = Integrity::from(key);
let path = path_from_integrity(&integrity); path_from_integrity(&integrity)
eprintln!("Hashing key {:?} -> {:#?}", key, path);
path
} }
fn path_from_integrity(integrity: &Integrity) -> String { fn path_from_integrity(integrity: &Integrity) -> String {
@ -102,7 +90,6 @@ fn path_from_integrity(integrity: &Integrity) -> String {
let (algorithm, digest) = integrity.to_hex(); let (algorithm, digest) = integrity.to_hex();
path += &algorithm.to_string(); path += &algorithm.to_string();
path += &digest; path += &digest;
eprintln!("Hashing integrity {:?} -> {:#?}", integrity, path);
path path
} }

2
src/cache/tests.rs vendored
View file

@ -14,7 +14,7 @@ fn print_cache_list(header: &'static str) -> Result<()> {
CACHE.list()?.iter().for_each(|meta| { CACHE.list()?.iter().for_each(|meta| {
let age_ms = meta.time; let age_ms = meta.time;
let cache_age = chrono::Utc.timestamp((age_ms / 1000) as i64, (age_ms % 1000) as u32); let cache_age = chrono::Utc.timestamp((age_ms / 1000) as i64, (age_ms % 1000) as u32);
eprintln!( println!(
"| - {}\n| SIZE: {}\n| AGE: {}", "| - {}\n| SIZE: {}\n| AGE: {}",
meta.key, meta.size, cache_age meta.key, meta.size, cache_age
) )

View file

@ -99,13 +99,12 @@ impl Canteen {
.initial_indent(ADRESS_INDENT) .initial_indent(ADRESS_INDENT)
.subsequent_indent(ADRESS_INDENT), .subsequent_indent(ADRESS_INDENT),
); );
println!( try_println!(
"{} {}\n{}", "{} {}\n{}",
color!(format!("{:>4}", self.id); bold, bright_yellow), color!(format!("{:>4}", self.id); bold, bright_yellow),
color!(self.meta()?.name; bold), color!(self.meta()?.name; bold),
color!(address; bright_black), color!(address; bright_black),
); )
Ok(())
} }
pub fn id(&self) -> CanteenId { pub fn id(&self) -> CanteenId {
@ -132,7 +131,7 @@ impl Canteen {
Self::print_all_json(canteens) Self::print_all_json(canteens)
} else { } else {
for canteen in canteens { for canteen in canteens {
println!(); try_println!()?;
canteen.print()?; canteen.print()?;
} }
Ok(()) Ok(())

View file

@ -57,7 +57,7 @@
//! //!
//! ### Examples //! ### Examples
//! //!
//! #### //! ####
//! <details> //! <details>
//! <summary><b>Meals on monday</b> (<i>Click me!</i>)</summary> //! <summary><b>Meals on monday</b> (<i>Click me!</i>)</summary>
//! //!
@ -70,7 +70,7 @@
//! ┊ //! ┊
//! ┊ ╭───╴Bohnengemüse //! ┊ ╭───╴Bohnengemüse
//! ┊ ├─╴Gemüsebeilage 🌱 //! ┊ ├─╴Gemüsebeilage 🌱
//! ┊ ╰╴( 0.55€ ) //! ┊ ╰╴( 0.55€ )
//! ... //! ...
//! ``` //! ```
//! </details> //! </details>
@ -95,13 +95,13 @@
//! //!
//! ```console //! ```console
//! $ mensa tags //! $ mensa tags
//! //!
//! 0 Acidifier //! 0 Acidifier
//! Contains artificial acidifier //! Contains artificial acidifier
//! //!
//! 1 Alcohol //! 1 Alcohol
//! Contains alcohol //! Contains alcohol
//! //!
//! 2 Antioxidant //! 2 Antioxidant
//! Contains an antioxidant //! Contains an antioxidant
//! ... //! ...
@ -113,7 +113,7 @@
//! //!
//! ```console //! ```console
//! $ mensa meals close --date sun //! $ mensa meals close --date sun
//! //!
//! Leipzig, Cafeteria Dittrichring //! Leipzig, Cafeteria Dittrichring
//! ┊ //! ┊
//! ┊ ╭───╴Vegetarisch gefüllte Zucchini //! ┊ ╭───╴Vegetarisch gefüllte Zucchini
@ -121,7 +121,7 @@
//! ┊ ├╴Rucola-Kartoffelpüree //! ┊ ├╴Rucola-Kartoffelpüree
//! ┊ ├╴Tomaten-Ratatouille-Soße //! ┊ ├╴Tomaten-Ratatouille-Soße
//! ┊ ╰╴( 2.65€ ) 2 11 12 19 //! ┊ ╰╴( 2.65€ ) 2 11 12 19
//! //!
//! Leipzig, Mensa am Park //! Leipzig, Mensa am Park
//! ┊ //! ┊
//! ┊ ╭───╴Apfelrotkohl //! ┊ ╭───╴Apfelrotkohl
@ -147,12 +147,14 @@
//! - `$HOME/Library/Application Support/mensa/config.toml` on **macOS**, //! - `$HOME/Library/Application Support/mensa/config.toml` on **macOS**,
//! - `{FOLDERID_RoamingAppData}\mensa\config.toml` on **Windows** //! - `{FOLDERID_RoamingAppData}\mensa\config.toml` on **Windows**
use std::io;
use cache::Cache; use cache::Cache;
use chrono::Duration; use chrono::Duration;
use directories_next::ProjectDirs; use directories_next::ProjectDirs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::Serialize; use serde::Serialize;
use tracing::error; use tracing::{error, info};
/// Colorizes the output. /// 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 cache;
mod canteen; mod canteen;
mod config; mod config;
@ -240,17 +254,25 @@ lazy_static! {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let res = real_main(); match real_main() {
match res { Ok(_) => Ok(()),
Ok(_) => {} // Ignore broken pipe errors, but log them
Err(ref why) => error!("{}", why), 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<()> { fn real_main() -> Result<()> {
// Initialize logger // Initialize logger
tracing_subscriber::fmt::init(); tracing_subscriber::FmtSubscriber::builder()
.with_writer(::std::io::stderr)
.init();
// Clear cache if requested // Clear cache if requested
if CONF.args.clear_cache { if CONF.args.clear_cache {
CACHE.clear()?; CACHE.clear()?;

View file

@ -5,7 +5,7 @@ use lazy_static::lazy_static;
use serde::Serialize; use serde::Serialize;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::get_sane_terminal_dimensions; use crate::{error::Result, get_sane_terminal_dimensions};
use super::{MealId, Meta, PRE}; use super::{MealId, Meta, PRE};
@ -27,39 +27,40 @@ pub struct MealComplete<'c> {
impl<'c> MealComplete<'c> { impl<'c> MealComplete<'c> {
/// Print this [`MealComplete`] to the terminal. /// 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(); let (width, _height) = get_sane_terminal_dimensions();
// Print meal name // Print meal name
self.print_name_to_terminal(width, highlight); self.print_name_to_terminal(width, highlight)?;
// Get notes, i.e. allergenes, descriptions, tags // Get notes, i.e. allergenes, descriptions, tags
self.print_category_and_primary_tags(highlight); self.print_category_and_primary_tags(highlight)?;
self.print_descriptions(width, highlight); self.print_descriptions(width, highlight)?;
self.print_price_and_secondary_tags(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 max_name_width = width - NAME_PRE.width() - PRE.width();
let mut name_parts = textwrap::wrap(&self.meta.name, max_name_width).into_iter(); 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 // There will always be a first part of the splitted string
let first_name_part = name_parts.next().unwrap(); let first_name_part = name_parts.next().unwrap();
println!( try_println!(
"{}{}{}", "{}{}{}",
*PRE, *PRE,
hl_if(highlight, *NAME_PRE), hl_if(highlight, *NAME_PRE),
color!(hl_if(highlight, first_name_part); bold), color!(hl_if(highlight, first_name_part); bold),
); )?;
for name_part in name_parts { for name_part in name_parts {
let name_part = hl_if(highlight, name_part); let name_part = hl_if(highlight, name_part);
println!( try_println!(
"{}{}{}", "{}{}{}",
*PRE, *PRE,
hl_if(highlight, *NAME_CONTINUE_PRE), hl_if(highlight, *NAME_CONTINUE_PRE),
color!(name_part; bold), 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 let mut tag_str = self
.meta .meta
.tags .tags
@ -69,39 +70,40 @@ impl<'c> MealComplete<'c> {
let tag_str_colored = let tag_str_colored =
if_plain!(color!(tag_str.join(" "); bright_black), tag_str.join(", ")); if_plain!(color!(tag_str.join(" "); bright_black), tag_str.join(", "));
let comma_if_plain = if_plain!("", ","); let comma_if_plain = if_plain!("", ",");
println!( try_println!(
"{}{}{}{} {}", "{}{}{}{} {}",
*PRE, *PRE,
hl_if(highlight, *CATEGORY_PRE), hl_if(highlight, *CATEGORY_PRE),
color!(self.meta.category; bright_blue), color!(self.meta.category; bright_blue),
color!(comma_if_plain; bright_black), color!(comma_if_plain; bright_black),
tag_str_colored 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(); let max_note_width = width - OTHER_NOTE_PRE.width() - PRE.width();
for note in &self.meta.descs { for note in &self.meta.descs {
let mut note_parts = textwrap::wrap(note, max_note_width).into_iter(); let mut note_parts = textwrap::wrap(note, max_note_width).into_iter();
// There will always be a first part in the splitted string // There will always be a first part in the splitted string
println!( try_println!(
"{}{}{}", "{}{}{}",
*PRE, *PRE,
hl_if(highlight, *OTHER_NOTE_PRE), hl_if(highlight, *OTHER_NOTE_PRE),
note_parts.next().unwrap() note_parts.next().unwrap()
); )?;
for part in note_parts { for part in note_parts {
println!( try_println!(
"{}{}{}", "{}{}{}",
*PRE, *PRE,
hl_if(highlight, *OTHER_NOTE_CONTINUE_PRE), hl_if(highlight, *OTHER_NOTE_CONTINUE_PRE),
part 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 prices = self.meta.prices.to_terminal_string();
let mut secondary: Vec<_> = self let mut secondary: Vec<_> = self
.meta .meta
@ -111,13 +113,13 @@ impl<'c> MealComplete<'c> {
.collect(); .collect();
secondary.sort_unstable(); secondary.sort_unstable();
let secondary_str = secondary.iter().map(|tag| tag.as_id()).join(" "); let secondary_str = secondary.iter().map(|tag| tag.as_id()).join(" ");
println!( try_println!(
"{}{}{} {}", "{}{}{} {}",
*PRE, *PRE,
hl_if(highlight, *PRICES_PRE), hl_if(highlight, *PRICES_PRE),
prices, prices,
color!(secondary_str; bright_black), color!(secondary_str; bright_black),
); )
} }
} }

View file

@ -96,7 +96,7 @@ impl Meal {
let day = CONF.date(); let day = CONF.date();
for canteen in canteens { for canteen in canteens {
let name = canteen.name()?; let name = canteen.name()?;
println!("\n {}", color!(name; bright_black)); try_println!("\n {}", color!(name; bright_black))?;
match canteen.meals_at_mut(day)? { match canteen.meals_at_mut(day)? {
Some(meals) => { Some(meals) => {
let mut printed_at_least_one_meal = false; let mut printed_at_least_one_meal = false;
@ -104,18 +104,16 @@ impl Meal {
let complete = meal.complete()?; let complete = meal.complete()?;
if filter.is_match(&complete) { if filter.is_match(&complete) {
let is_fav = favs.is_non_empty_match(&complete); let is_fav = favs.is_non_empty_match(&complete);
println!("{}", *PRE); try_println!("{}", *PRE)?;
complete.print(is_fav); complete.print(is_fav)?;
printed_at_least_one_meal = true; printed_at_least_one_meal = true;
} }
} }
if !printed_at_least_one_meal { if !printed_at_least_one_meal {
println!("{} {}", *PRE, color!("no matching meals found"; dimmed)); try_println!("{} {}", *PRE, color!("no matching meals found"; dimmed))?
} }
} }
None => { None => try_println!("{} {}", *PRE, color!("closed"; dimmed))?,
println!("{} {}", *PRE, color!("closed"; dimmed))
}
} }
} }
Ok(()) Ok(())

View file

@ -212,7 +212,7 @@ impl Tag {
/// Print this tag. /// Print this tag.
/// ///
/// Does **not** respect `--json`, use [`Self::print_all`]. /// 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() { let emoji = if CONF.args.plain && self.is_primary() {
format!("{:>width$}", "-", width = ID_WIDTH) format!("{:>width$}", "-", width = ID_WIDTH)
} else { } else {
@ -231,12 +231,12 @@ impl Tag {
.initial_indent(TEXT_INDENT) .initial_indent(TEXT_INDENT)
.subsequent_indent(TEXT_INDENT), .subsequent_indent(TEXT_INDENT),
); );
println!( try_println!(
"{} {}\n{}", "{} {}\n{}",
color!(emoji; bright_yellow, bold), color!(emoji; bright_yellow, bold),
color!(self; bold), color!(self; bold),
color!(description; bright_black), color!(description; bright_black),
); )
} }
/// Print all tags. /// Print all tags.
@ -245,8 +245,8 @@ impl Tag {
Self::print_all_json() Self::print_all_json()
} else { } else {
for tag in Tag::iter() { for tag in Tag::iter() {
println!(); try_println!()?;
tag.print(); tag.print()?;
} }
Ok(()) Ok(())
} }