From 2124e49db1666ce03313cb2f39e35175e9023591 Mon Sep 17 00:00:00 2001 From: Oskar Manhart <52569953+oskardotglobal@users.noreply.github.com> Date: Sun, 8 Oct 2023 00:32:50 +0200 Subject: [PATCH] feat: custom error handling, logging with tracing --- Cargo.toml | 1 + winapps/Cargo.toml | 6 +- winapps/src/errors.rs | 145 ++++++++++++++++++++++++++++++++++++++++++ winapps/src/lib.rs | 82 ++++++++++++++++-------- 4 files changed, 205 insertions(+), 29 deletions(-) create mode 100644 winapps/src/errors.rs diff --git a/Cargo.toml b/Cargo.toml index 8cd05f6..2d1fc48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ members = [ "winapps-cli", "winapps-gui", ] +resolver = "2" \ No newline at end of file diff --git a/winapps/Cargo.toml b/winapps/Cargo.toml index abd213a..24f4774 100644 --- a/winapps/Cargo.toml +++ b/winapps/Cargo.toml @@ -6,7 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.75" derive-new = "0.5.9" home = "0.5.5" +lazy_static = "1.4.0" serde = { version = "1.0.171", features = ["derive"] } -toml = "0.7.6" +thiserror = "1.0.49" +toml = "0.8.2" +tracing = "0.1.37" diff --git a/winapps/src/errors.rs b/winapps/src/errors.rs new file mode 100644 index 0000000..0545fed --- /dev/null +++ b/winapps/src/errors.rs @@ -0,0 +1,145 @@ +#![allow(clippy::crate_in_macro_def)] + +use std::error::Error; +use std::fmt::Debug; +use std::process::exit; + +/// This enum represents all possible errors that can occur in this crate. +/// It is used as a return type for most functions should they return an error. +/// There's 2 base variants: `Message` and `WithError`. +/// `Message` is used for simple errors that don't have an underlying cause. +/// `WithError` is used for errors that occur from another error. +#[derive(thiserror::Error, Debug)] +pub enum WinappsError { + #[error("{0}")] + Message(String), + #[error("{0}\n{1}")] + WithError(#[source] anyhow::Error, String), +} + +impl WinappsError { + /// This function prints the error to the console. + /// It is used internally by the `unrecoverable` and `panic` functions. + /// All lines are logged as seperate messages, and the source of the error is also logged if it exists. + fn error(&self) { + let messages: Vec = self.to_string().split('\n').map(|s| s.into()).collect(); + messages.iter().for_each(|s| tracing::error!("{}", s)); + + if self.source().is_some() { + tracing::error!("Caused by: {}", self.source().unwrap()); + } + } + + /// This function prints the error to the console and exits the program with an exit code of 1. + pub fn unrecoverable(&self) -> ! { + self.error(); + + tracing::error!("Unrecoverable error, exiting..."); + exit(1); + } + + /// This function prints the error to the console and panics. + pub fn panic(&self) -> ! { + self.error(); + + panic!("Program crashed, see log above"); + } +} + +/// This macro is a shortcut for creating a `WinappsError` from a string. +/// You can use normal `format!` syntax inside the macro. +#[macro_export] +macro_rules! error { + ($($fmt:tt)*) => { + crate::errors::WinappsError::Message(format!($($fmt)*)) + }; +} + +/// This macro is a shortcut for creating a `WinappsError` from a string. +/// The first argument is the source error. +/// You can use normal `format!` syntax inside the macro. +#[macro_export] +macro_rules! error_from { + ($err:expr, $($fmt:tt)*) => { + crate::errors::WinappsError::WithError(anyhow::Error::new($err), format!($($fmt)*)) + }; +} + +/// This trait serves as a generic way to convert a `Result` or `Option` into a `WinappsError`. +pub trait IntoError { + fn into_error(self, msg: String) -> Result; +} + +impl IntoError for Result +where + T: Debug, + U: Error + Send + Sync + 'static, +{ + fn into_error(self, msg: String) -> Result { + if let Err(error) = self { + return Err(WinappsError::WithError(anyhow::Error::new(error), msg)); + } + + Ok(self.unwrap()) + } +} + +impl IntoError for Option { + fn into_error(self, msg: String) -> Result { + if self.is_none() { + return Err(WinappsError::Message(msg)); + } + + Ok(self.unwrap()) + } +} + +/// This function unwraps a `Result` or `Option` and returns the value if it exists. +/// Should the value not exist, then the program will exit with an exit code of 1. +/// Called internally by the `unwrap_or_exit!` macro, which you should probably use instead. +pub fn unwrap_or_exit(val: U, msg: String) -> T +where + T: Sized + Debug, + U: IntoError, +{ + val.into_error(msg).unwrap_or_else(|e| e.unrecoverable()) +} + +/// This function unwraps a `Result` or `Option` and returns the value if it exists. +/// Should the value not exist, then the program will panic. +/// Called internally by the `unwrap_or_panic!` macro, which you should probably use instead. +pub fn unwrap_or_panic(val: U, msg: String) -> T +where + T: Sized + Debug, + U: IntoError, +{ + val.into_error(msg).unwrap_or_else(|e| e.panic()) +} + +/// This macro unwraps a `Result` or `Option` and returns the value if it exists. +/// Should the value not exist, then the program will exit with exit code 1. +/// Optionally, a message can be passed to the function which uses standard `format!` syntax. +/// The result type has to implement `Debug` and `Sized`, and the error type has to implement `Error`, `Send`, `Sync` has to be `'static`. +#[macro_export] +macro_rules! unwrap_or_exit { + ($expr:expr) => {{ + crate::errors::unwrap_or_exit($expr, "Expected a value, got None / Error".into()) + }}; + ($expr:expr, $($fmt:tt)*) => {{ + crate::errors::unwrap_or_exit($expr, format!($($fmt)*)) + }}; +} + +/// This macro unwraps a `Result` or `Option` and returns the value if it exists. +/// Should the value not exist, then the program will panic. +/// Optionally, a message can be passed to the function which uses standard `format!` syntax. +/// The result type has to implement `Debug` and `Sized`, and the error type has to implement `Error`, `Send`, `Sync` has to be `'static`. +#[macro_export] +macro_rules! unwrap_or_panic { + ($expr:expr) => {{ + crate::errors::unwrap_or_panic($expr, "Expected a value, got None / Error".into()) + }}; + ($expr:expr, $($fmt:tt)*) => {{ + crate::errors::unwrap_or_panic($expr, format!($($fmt)*)) + }}; +} diff --git a/winapps/src/lib.rs b/winapps/src/lib.rs index 41347d6..1cf5b4c 100644 --- a/winapps/src/lib.rs +++ b/winapps/src/lib.rs @@ -1,5 +1,8 @@ +pub mod errors; +pub mod freerdp; pub mod quickemu; +use crate::errors::WinappsError; use derive_new::new; use home::home_dir; use serde::{Deserialize, Serialize}; @@ -10,8 +13,7 @@ use std::{ fs::{self, File}, path::Path, }; - -pub mod freerdp; +use tracing::{info, warn}; pub trait RemoteClient { fn check_depends(&self, config: Config); @@ -59,22 +61,24 @@ pub fn get_config_file(path: Option<&str>) -> PathBuf { let default = match env::var("XDG_CONFIG_HOME") { Ok(dir) => PathBuf::from(dir).join("winapps"), Err(_) => { - println!("Couldn't read XDG_CONFIG_HOME, falling back to ~/.config"); - home_dir() - .expect("Could not find the home path!") - .join(".config/winapps") + warn!("Couldn't read XDG_CONFIG_HOME, falling back to ~/.config"); + unwrap_or_panic!(home_dir(), "Couldn't find the home directory").join(".config/winapps") } }; - let path = Path::new(path.unwrap_or(default.to_str().unwrap())); + let path = Path::new(path.unwrap_or(unwrap_or_panic!( + default.to_str(), + "Couldn't convert path {:?} to string", + default + ))); if !path.exists() { - println!("{:?} does not exist! Creating...", path); + info!("{:?} does not exist! Creating...", path); fs::create_dir_all(path).expect("Failed to create directory"); } if !path.is_dir() { - panic!("Config directory {:?} is not a directory!", path); + error!("Config directory {:?} is not a directory", path).panic(); } path.join("config.toml") @@ -85,54 +89,76 @@ pub fn load_config(path: Option<&str>) -> Config { let config_path = get_config_file(path); if !config_path.exists() { - save_config(&config, path).expect("Failed to write default configuration"); + unwrap_or_panic!( + save_config(&config, path), + "Failed to write default configuration" + ); + return config; } - let config_file = fs::read_to_string(config_path).expect("Failed to read configuration file"); - let config: Config = - toml::from_str(config_file.as_str()).expect("Failed to parse the configuration"); + let config_file = unwrap_or_panic!( + fs::read_to_string(config_path), + "Failed to read configuration file" + ); + + let config: Config = unwrap_or_panic!( + toml::from_str(config_file.as_str()), + "Failed to parse configuration file", + ); config } -pub fn save_config(config: &Config, path: Option<&str>) -> std::io::Result<()> { +pub fn save_config(config: &Config, path: Option<&str>) -> Result<(), WinappsError> { let config_path = get_config_file(path); - let serialized_config = toml::to_string(&config).expect("Failed to serialize configuration"); + let serialized_config = unwrap_or_panic!( + toml::to_string(&config), + "Failed to serialize configuration" + ); let mut config_file = match config_path.exists() { - true => File::open(&config_path).expect("Failed to open configuration file"), - false => File::create(&config_path).expect("Failed to create configuration file"), + true => unwrap_or_panic!( + File::open(&config_path), + "Failed to open configuration file" + ), + false => unwrap_or_panic!( + File::create(&config_path), + "Failed to create configuration file" + ), }; - write!(config_file, "{}", serialized_config) + if let Err(e) = write!(config_file, "{}", serialized_config) { + return Err(error_from!(e, "Failed to write configuration file")); + } + + Ok(()) } pub fn get_data_dir() -> PathBuf { - let data_dir = match env::var("XDG_DATA_HOME") { + let path = match env::var("XDG_DATA_HOME") { Ok(dir) => PathBuf::from(dir).join("winapps"), Err(_) => { - println!("Couldn't read XDG_DATA_HOME, falling back to ~/.local/share"); - home_dir() - .expect("Could not find the home path!") + warn!("Couldn't read XDG_DATA_HOME, falling back to ~/.local/share"); + unwrap_or_panic!(home_dir(), "Couldn't find the home directory") .join(".local/share/winapps") } }; - if !data_dir.exists() { - let dir = data_dir.clone(); - println!( + if !path.exists() { + let dir = path.clone(); + info!( "Data directory {:?} does not exist! Creating...", dir.to_str() ); fs::create_dir_all(dir).expect("Failed to create directory"); } - if !data_dir.is_dir() { - panic!("Data directory {:?} is not a directory!", data_dir); + if !path.is_dir() { + error!("Data directory {:?} is not a directory", path).panic(); } - data_dir + path } pub fn add(left: usize, right: usize) -> usize {