diff --git a/Cargo.toml b/Cargo.toml index 8cd05f6..0e5b907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ members = [ "winapps-cli", "winapps-gui", ] +resolver = "2" diff --git a/winapps-cli/src/main.rs b/winapps-cli/src/main.rs index 3887292..3fb78ee 100644 --- a/winapps-cli/src/main.rs +++ b/winapps-cli/src/main.rs @@ -1,7 +1,7 @@ use clap::{arg, Command}; use winapps::freerdp::freerdp_back::Freerdp; use winapps::quickemu::{create_vm, kill_vm, start_vm}; -use winapps::RemoteClient; +use winapps::{unwrap_or_panic, RemoteClient}; fn cli() -> Command { Command::new("winapps-cli") @@ -69,18 +69,22 @@ fn main() { } Some((_, _)) => { - cli.about("Command not found, try existing ones!") - .print_help() - .expect("Couldn't print help"); + unwrap_or_panic!( + cli.about("Command not found, try existing ones!") + .print_help(), + "Couldn't print help" + ); } _ => unreachable!(), }; } Some((_, _)) => { - cli.about("Command not found, try existing ones!") - .print_help() - .expect("Couldn't print help"); + unwrap_or_panic!( + cli.about("Command not found, try existing ones!") + .print_help(), + "Couldn't print help" + ); } _ => unreachable!(), } diff --git a/winapps-gui/src/main.rs b/winapps-gui/src/main.rs index acf379e..f328e4d 100644 --- a/winapps-gui/src/main.rs +++ b/winapps-gui/src/main.rs @@ -1,3 +1 @@ -fn main() { - println!("Test lib: {}", winapps::add(1, 2)); -} +fn main() {} diff --git a/winapps/Cargo.toml b/winapps/Cargo.toml index abd213a..8d6d0a5 100644 --- a/winapps/Cargo.toml +++ b/winapps/Cargo.toml @@ -6,7 +6,10 @@ 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" 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..fa449db --- /dev/null +++ b/winapps/src/errors.rs @@ -0,0 +1,158 @@ +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. + /// 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 exit(&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 macro creates a `Result<_, WinappsError>` from either a `Result` or an `Option`. +/// It also works for all other types that implement `IntoError`. +/// Used internally by `winapps::unwrap_or_exit!` and `winapps::unwrap_or_panic!`. +#[macro_export] +macro_rules! into_error { + ($val:expr) => {{ + fn into_error_impl(val: U) -> std::result::Result + where + T: std::marker::Sized + std::fmt::Debug, + U: $crate::errors::IntoError, + { + val.into_error( + "Expected a value, got None / an Error. \ + See log above for more detail." + .into(), + ) + } + + into_error_impl($val) + }}; + ($val:expr, $msg:expr) => {{ + fn into_error_impl( + val: U, + msg: String, + ) -> std::result::Result + where + T: std::marker::Sized + std::fmt::Debug, + U: $crate::errors::IntoError, + { + val.into_error(msg) + } + + into_error_impl($val, $msg.into()) + }}; +} + +/// 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 using standard `format!` syntax. +/// The result type has to implement `Debug` and `Sized`, and the source error type has to implement `Error`, `Send`, `Sync` and has to be `'static`. +/// See `winapps::unwrap_or_panic!` for a version that panics instead of exiting. +#[macro_export] +macro_rules! unwrap_or_exit { + ($expr:expr) => {{ + $crate::into_error!($expr).unwrap_or_else(|e| e.exit()) + }}; + ($expr:expr, $($fmt:tt)*) => {{ + $crate::into_error!($expr, format!($($fmt)*)).unwrap_or_else(|e| e.exit()) + }}; +} + +/// 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 using standard `format!` syntax. +/// The result type has to implement `Debug` and `Sized`, and the error type has to implement `Error`, `Send`, `Sync` and has to be `'static`. +/// See `winapps::unwrap_or_exit!` for a version that exits instead of panicking. +#[macro_export] +macro_rules! unwrap_or_panic { + ($expr:expr) => {{ + $crate::into_error!($expr).unwrap_or_else(|e| e.panic()) + }}; + ($expr:expr, $($fmt:tt)*) => {{ + $crate::into_error!($expr, format!($($fmt)*)).unwrap_or_else(|e| e.panic()) + }}; +} diff --git a/winapps/src/freerdp.rs b/winapps/src/freerdp.rs index cf7baa4..b88936a 100644 --- a/winapps/src/freerdp.rs +++ b/winapps/src/freerdp.rs @@ -1,7 +1,8 @@ pub mod freerdp_back { use std::process::{Command, Stdio}; + use tracing::{info, warn}; - use crate::{Config, RemoteClient}; + use crate::{unwrap_or_exit, Config, RemoteClient}; pub struct Freerdp {} @@ -11,18 +12,21 @@ pub mod freerdp_back { xfreerdp.stdout(Stdio::null()); xfreerdp.stderr(Stdio::null()); xfreerdp.args(["-h"]); - xfreerdp - .spawn() - .expect("Freerdp execution failed! It needs to be installed!"); - println!("Freerdp found!"); - println!("All dependencies found!"); - println!("Running explorer as test!"); - println!("Check yourself if it appears correctly!"); + unwrap_or_exit!( + xfreerdp.spawn(), + "Freerdp execution failed! It needs to be installed!", + ); + + info!("Freerdp found!"); + + info!("All dependencies found!"); + info!("Running explorer as test!"); + warn!("Check yourself if it appears correctly!"); self.run_app(config, Some(&"explorer.exe".to_string())); - println!("Test finished!"); + info!("Test finished!"); } fn run_app(&self, config: Config, app: Option<&String>) { @@ -56,7 +60,11 @@ pub mod freerdp_back { ]); } } - xfreerdp.spawn().expect("Freerdp execution failed!"); + + unwrap_or_exit!( + xfreerdp.spawn(), + "Freerdp execution failed, check logs above!", + ); } } } diff --git a/winapps/src/lib.rs b/winapps/src/lib.rs index 8c5a265..148f331 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,56 +89,74 @@ 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 -} - -pub fn add(left: usize, right: usize) -> usize { - left + right + path } diff --git a/winapps/src/quickemu.rs b/winapps/src/quickemu.rs index 73b3300..d2cb54c 100644 --- a/winapps/src/quickemu.rs +++ b/winapps/src/quickemu.rs @@ -1,29 +1,25 @@ -use crate::{get_data_dir, save_config, Config}; +use crate::{get_data_dir, save_config, unwrap_or_exit, Config}; use std::fs; -use std::process::exit; use std::process::Command; +use tracing::info; pub fn create_vm(mut config: Config) { let data_dir = get_data_dir(); - let output = match Command::new("quickget") - .current_dir(data_dir) - .arg("windows") - .arg("10") - .output() - { - Ok(o) => o, - Err(e) => { - println!("Failed to execute quickget: {}", e); - println!("Please make sure quickget is installed and in your PATH"); - exit(1); - } - }; + let output = unwrap_or_exit!( + Command::new("quickget") + .current_dir(data_dir) + .arg("windows") + .arg("10") + .output(), + "Failed to execute quickget: \n\ + Please make sure quickget is installed and in your PATH" + ); config.vm.name = "windows-10-22H2".to_string(); config.vm.short_name = "windows-10".to_string(); - save_config(&config, None).expect("Failed to save config, VM will not start properly"); + unwrap_or_exit!(save_config(&config, None), "Failed to save config"); println!("{}", String::from_utf8_lossy(&output.stdout)); } @@ -31,33 +27,25 @@ pub fn create_vm(mut config: Config) { pub fn start_vm(config: Config) { let data_dir = get_data_dir(); - let command = match Command::new("quickemu") - .current_dir(data_dir) - .args([ - "--ignore-msrs-always", - "--vm", - &format!("{}.conf", config.vm.name), - "--display", - "none", - ]) - .spawn() - { - Ok(c) => c, - Err(e) => { - println!("Failed to execute quickemu: {}", e); - println!("Please make sure quickemu is installed and in your PATH"); - exit(1); - } - }; + let command = unwrap_or_exit!( + Command::new("quickemu") + .current_dir(data_dir) + .args([ + "--ignore-msrs-always", + "--vm", + &format!("{}.conf", config.vm.name), + "--display", + "none", + ]) + .spawn(), + "Failed to execute quickemu: \n\ + Please make sure quickemu is installed and in your PATH" + ); - let output = match command.wait_with_output() { - Ok(o) => o, - Err(e) => { - println!("Failed to gather output from quickemu: {}", e); - println!("Please make sure quickemu is installed and in your PATH"); - exit(1); - } - }; + let output = unwrap_or_exit!( + command.wait_with_output(), + "Failed to gather output from quickemu / stdout" + ); println!("{}", String::from_utf8_lossy(&output.stdout)); } @@ -65,25 +53,17 @@ pub fn start_vm(config: Config) { pub fn kill_vm(config: Config) { let data_dir = get_data_dir(); - match fs::read_to_string( - data_dir.join(format!("{}/{}.pid", config.vm.short_name, config.vm.name)), - ) { - Ok(pid) => { - let pid = pid.trim(); + let pid = unwrap_or_exit!( + fs::read_to_string( + data_dir.join(format!("{}/{}.pid", config.vm.short_name, config.vm.name)) + ), + "Failed to read PID file, is the VM running and the config correct?" + ); - println!("Killing VM with PID {}", pid); + info!("Killing VM with PID {}", pid); - match Command::new("kill").arg(pid).spawn() { - Ok(_) => (), - Err(e) => { - println!("Failed to kill VM: {}", e); - exit(1); - } - } - } - Err(e) => { - println!("Failed to read PID file: {}", e); - exit(1); - } - } + unwrap_or_exit!( + Command::new("kill").arg(pid.trim()).spawn(), + "Failed to kill VM (execute kill)" + ); }