From a13bb0272b62a989ea079f498cfa1c9925d8b63b Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 16 Feb 2023 11:45:52 +0100 Subject: [PATCH] gui: add logger module and handle log file destinations Installer has its own log file installer.log that is removed after successful install. When changing network, the destination log file change to //liana-gui.log --- gui/Cargo.lock | 102 ++++++++++++++++++++++- gui/Cargo.toml | 3 +- gui/src/app/config.rs | 27 +++++++ gui/src/app/mod.rs | 9 ++- gui/src/app/state/settings.rs | 3 +- gui/src/daemon/client/jsonrpc.rs | 2 +- gui/src/daemon/client/mod.rs | 2 +- gui/src/hw.rs | 2 +- gui/src/installer/mod.rs | 40 ++++----- gui/src/lib.rs | 1 + gui/src/loader.rs | 67 ++++++--------- gui/src/logger.rs | 115 ++++++++++++++++++++++++++ gui/src/main.rs | 135 ++++++++++++------------------- 13 files changed, 355 insertions(+), 153 deletions(-) create mode 100644 gui/src/logger.rs diff --git a/gui/Cargo.lock b/gui/Cargo.lock index 9bd7e070..1aaeb986 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -1686,7 +1686,6 @@ dependencies = [ "base64", "chrono", "dirs", - "fern", "iced", "iced_lazy", "iced_native", @@ -1696,6 +1695,8 @@ dependencies = [ "serde_json", "tokio", "toml", + "tracing", + "tracing-subscriber", ] [[package]] @@ -2107,6 +2108,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2254,6 +2265,12 @@ dependencies = [ "syn", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owned_ttf_parser" version = "0.15.0" @@ -2849,6 +2866,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_library" version = "0.1.9" @@ -3076,6 +3102,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.1.44" @@ -3178,6 +3214,64 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec 1.9.0", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "ttf-parser" version = "0.12.3" @@ -3297,6 +3391,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/gui/Cargo.toml b/gui/Cargo.toml index b232b244..34d16347 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -28,8 +28,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Logging stuff +tracing = "0.1.37" +tracing-subscriber = "0.3.16" log = "0.4" -fern = "0.6" dirs = "3.0.1" toml = "0.5" diff --git a/gui/src/app/config.rs b/gui/src/app/config.rs index 459de2d7..5d8d0b11 100644 --- a/gui/src/app/config.rs +++ b/gui/src/app/config.rs @@ -1,6 +1,7 @@ use crate::hw::HardwareWalletConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; +use tracing_subscriber::filter; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { @@ -38,12 +39,35 @@ impl Config { ConfigError::ReadingFile(format!("Parsing configuration file: {}", e)) }) })?; + + // check if log_level field is valid + config.log_level()?; Ok(config) } + + /// TODO: Deserialize directly in the struct. + pub fn log_level(&self) -> Result { + if let Some(level) = &self.log_level { + match level.as_ref() { + "info" => Ok(filter::LevelFilter::INFO), + "debug" => Ok(filter::LevelFilter::DEBUG), + "trace" => Ok(filter::LevelFilter::TRACE), + _ => Err(ConfigError::InvalidField( + "log_level", + format!("Unknown value '{}'", level), + )), + } + } else if let Some(true) = self.debug { + Ok(filter::LevelFilter::DEBUG) + } else { + Ok(filter::LevelFilter::INFO) + } + } } #[derive(PartialEq, Eq, Debug, Clone)] pub enum ConfigError { + InvalidField(&'static str, String), NotFound, ReadingFile(String), Unexpected(String), @@ -53,6 +77,9 @@ impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::NotFound => write!(f, "Config file not found"), + Self::InvalidField(field, message) => { + write!(f, "Config field {} is invalid: {}", field, message) + } Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e), Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), } diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index acab0d48..aa1b8163 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -15,6 +15,7 @@ use std::sync::Arc; use std::time::Duration; use iced::{clipboard, time, Command, Element, Subscription}; +use tracing::{info, warn}; pub use liana::config::Config as DaemonConfig; @@ -98,12 +99,12 @@ impl App { } pub fn stop(&mut self) { - log::info!("Close requested"); + info!("Close requested"); if !self.daemon.is_external() { - log::info!("Stopping internal daemon..."); + info!("Stopping internal daemon..."); if let Some(d) = Arc::get_mut(&mut self.daemon) { d.stop().expect("Daemon is internal"); - log::info!("Internal daemon stopped"); + info!("Internal daemon stopped"); } } } @@ -165,7 +166,7 @@ impl App { daemon_config_file .write_all(content.as_bytes()) .map_err(|e| { - log::warn!("failed to write to file: {:?}", e); + warn!("failed to write to file: {:?}", e); Error::Config(e.to_string()) })?; diff --git a/gui/src/app/state/settings.rs b/gui/src/app/state/settings.rs index 29d7d093..24fd3c26 100644 --- a/gui/src/app/state/settings.rs +++ b/gui/src/app/state/settings.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use chrono::prelude::*; use iced::{Command, Element}; +use tracing::info; use liana::config::{BitcoinConfig, BitcoindConfig, Config}; @@ -338,7 +339,7 @@ impl Setting for RescanSetting { .and_hms(0, 0, 0); let t = date_time.timestamp() as u32; self.processing = true; - log::info!("Asking deamon to rescan with timestamp: {}", t); + info!("Asking deamon to rescan with timestamp: {}", t); return Command::perform( async move { daemon.start_rescan(t).map_err(|e| e.into()) }, Message::StartRescan, diff --git a/gui/src/daemon/client/jsonrpc.rs b/gui/src/daemon/client/jsonrpc.rs index fc600c04..1d9fd302 100644 --- a/gui/src/daemon/client/jsonrpc.rs +++ b/gui/src/daemon/client/jsonrpc.rs @@ -31,7 +31,7 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::Deserializer; -use log::debug; +use tracing::debug; /// A handle to a remote JSONRPC server #[derive(Debug, Clone)] diff --git a/gui/src/daemon/client/mod.rs b/gui/src/daemon/client/mod.rs index d80ae28e..80605201 100644 --- a/gui/src/daemon/client/mod.rs +++ b/gui/src/daemon/client/mod.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; use std::fmt::Debug; -use log::{error, info}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; +use tracing::{error, info}; pub mod error; pub mod jsonrpc; diff --git a/gui/src/hw.rs b/gui/src/hw.rs index e086f0f1..b524d3a0 100644 --- a/gui/src/hw.rs +++ b/gui/src/hw.rs @@ -5,8 +5,8 @@ use liana::miniscript::bitcoin::{ hashes::hex::{FromHex, ToHex}, util::bip32::Fingerprint, }; -use log::debug; use serde::{Deserialize, Serialize}; +use tracing::debug; #[derive(Debug, Clone)] pub enum HardwareWallet { diff --git a/gui/src/installer/mod.rs b/gui/src/installer/mod.rs index f63ce5fd..76f57c16 100644 --- a/gui/src/installer/mod.rs +++ b/gui/src/installer/mod.rs @@ -6,6 +6,7 @@ mod view; use iced::{clipboard, Command, Element, Subscription}; use liana::miniscript::bitcoin; +use tracing::{error, info, warn}; use context::Context; use std::io::Write; @@ -141,15 +142,15 @@ impl Installer { data_dir.push(self.context.bitcoin_config.network.to_string()); // In case of failure during install, block the thread to // deleted the data_dir/network directory in order to start clean again. - log::warn!("Installation failed. Cleaning up the leftover data directory."); + warn!("Installation failed. Cleaning up the leftover data directory."); if let Err(e) = std::fs::remove_dir_all(&data_dir) { - log::error!( + error!( "Failed to completely delete the data directory (path: '{}'): {}", data_dir.to_string_lossy(), e ); } else { - log::warn!( + warn!( "Successfully deleted data directory at '{}'.", data_dir.to_string_lossy() ); @@ -208,18 +209,19 @@ pub fn daemon_check(cfg: liana::config::Config) -> Result<(), Error> { } pub async fn install(ctx: Context) -> Result { - log::info!("installing"); let mut cfg: liana::config::Config = ctx.extract_daemon_config(); - daemon_check(cfg.clone())?; - log::info!("daemon checked"); - - cfg.data_dir = - Some(cfg.data_dir.unwrap().canonicalize().map_err(|e| { + let data_dir = + cfg.data_dir.unwrap().canonicalize().map_err(|e| { Error::Unexpected(format!("Failed to canonicalize datadir path: {}", e)) - })?); + })?; + cfg.data_dir = Some(data_dir.clone()); - let mut datadir_path = cfg.data_dir.clone().unwrap(); - datadir_path.push(cfg.bitcoin_config.network.to_string()); + daemon_check(cfg.clone())?; + + info!("daemon checked"); + + let mut network_datadir_path = data_dir; + network_datadir_path.push(cfg.bitcoin_config.network.to_string()); // Step needed because of ValueAfterTable error in the toml serialize implementation. let daemon_config = toml::Value::try_from(&cfg) @@ -227,12 +229,12 @@ pub async fn install(ctx: Context) -> Result { // create lianad configuration file let daemon_config_path = create_and_write_file( - datadir_path.clone(), + network_datadir_path.clone(), "daemon.toml", daemon_config.to_string().as_bytes(), )?; - log::info!("Daemon configuration file created"); + info!("Daemon configuration file created"); if let Some(signer) = &ctx.signer { signer @@ -242,12 +244,12 @@ pub async fn install(ctx: Context) -> Result { ) .map_err(|e| Error::Unexpected(format!("Failed to store mnemonic: {}", e)))?; - log::info!("Hot signer mnemonic stored"); + info!("Hot signer mnemonic stored"); } // create liana GUI configuration file let gui_config_path = create_and_write_file( - datadir_path.clone(), + network_datadir_path.clone(), gui_config::DEFAULT_FILE_NAME, toml::to_string(&gui_config::Config::new( daemon_config_path.canonicalize().map_err(|e| { @@ -258,19 +260,19 @@ pub async fn install(ctx: Context) -> Result { .as_bytes(), )?; - log::info!("Gui configuration file created"); + info!("Gui configuration file created"); // create liana GUI settings file let settings: gui_settings::Settings = ctx.extract_gui_settings(); create_and_write_file( - datadir_path, + network_datadir_path, gui_settings::DEFAULT_FILE_NAME, serde_json::to_string_pretty(&settings) .map_err(|e| Error::Unexpected(format!("Failed to serialize settings: {}", e)))? .as_bytes(), )?; - log::info!("Settings file created"); + info!("Settings file created"); Ok(gui_config_path) } diff --git a/gui/src/lib.rs b/gui/src/lib.rs index 7e6c20c1..782fcf10 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -4,6 +4,7 @@ pub mod hw; pub mod installer; pub mod launcher; pub mod loader; +pub mod logger; pub mod signer; pub mod ui; pub mod utils; diff --git a/gui/src/loader.rs b/gui/src/loader.rs index 67b30a08..a83e28eb 100644 --- a/gui/src/loader.rs +++ b/gui/src/loader.rs @@ -1,6 +1,6 @@ use std::convert::From; use std::io::ErrorKind; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use iced::{ @@ -8,7 +8,7 @@ use iced::{ Element, }; use iced::{Alignment, Command, Length, Subscription}; -use log::{debug, info}; +use tracing::{debug, info}; use liana::{ config::{Config, ConfigError}, @@ -20,7 +20,7 @@ use liana::{ use crate::{ app::{ cache::Cache, - config::{default_datadir, Config as GUIConfig}, + config::Config as GUIConfig, settings::{self, Settings}, wallet::Wallet, }, @@ -36,7 +36,7 @@ use crate::{ type Lianad = client::Lianad; pub struct Loader { - pub datadir_path: Option, + pub datadir_path: PathBuf, pub network: bitcoin::Network, pub gui_config: GUIConfig, pub daemon_started: bool, @@ -68,18 +68,15 @@ pub enum Message { impl Loader { pub fn new( - datadir_path: Option, + datadir_path: PathBuf, gui_config: GUIConfig, daemon_config: Config, ) -> (Self, Command) { - let path = socket_path( - &daemon_config.data_dir, - daemon_config.bitcoin_config.network, - ) - .unwrap(); + let path = socket_path(&datadir_path, daemon_config.bitcoin_config.network); + let network = daemon_config.bitcoin_config.network; ( Loader { - network: daemon_config.bitcoin_config.network, + network, datadir_path, daemon_config: daemon_config.clone(), gui_config, @@ -171,13 +168,13 @@ impl Loader { } pub fn stop(&mut self) { - log::info!("Close requested"); + info!("Close requested"); if let Step::Syncing { daemon, .. } = &mut self.step { if !daemon.is_external() { - log::info!("Stopping internal daemon..."); + info!("Stopping internal daemon..."); if let Some(d) = Arc::get_mut(daemon) { d.stop().expect("Daemon is internal"); - log::info!("Internal daemon stopped"); + info!("Internal daemon stopped"); } else { } } @@ -215,7 +212,7 @@ impl Loader { } pub fn view(&self) -> Element { - view(self.datadir_path.as_ref(), &self.step).map(Message::View) + view(&self.step).map(Message::View) } } @@ -223,7 +220,7 @@ pub async fn load_application( daemon: Arc, info: GetInfoResult, gui_config: GUIConfig, - datadir_path: Option, + datadir_path: PathBuf, network: bitcoin::Network, ) -> Result<(Arc, Cache, Arc), Error> { let coins = daemon.list_coins().map(|res| res.coins)?; @@ -235,7 +232,7 @@ pub async fn load_application( spend_txs, ..Default::default() }; - let settings_path = settings_path(&datadir_path, network).unwrap(); + let settings_path = settings_path(&datadir_path, network); let gui_config_hws = gui_config .hardware_wallets .as_ref() @@ -258,7 +255,7 @@ pub async fn load_application( Err(e) => return Err(e.into()), }; - let hot_signers = match HotSigner::from_datadir(&get_datadir_path(&datadir_path)?, network) { + let hot_signers = match HotSigner::from_datadir(&datadir_path, network) { Ok(signers) => signers, Err(e) => match e { liana::signer::SignerError::MnemonicStorage(e) => { @@ -290,7 +287,7 @@ pub enum ViewMessage { SwitchNetwork, } -pub fn view<'a>(datadir_path: Option<&'a PathBuf>, step: &'a Step) -> Element<'a, ViewMessage> { +pub fn view(step: &Step) -> Element { match &step { Step::StartingDaemon => cover( None, @@ -333,10 +330,10 @@ pub fn view<'a>(datadir_path: Option<&'a PathBuf>, step: &'a Step) -> Element<'a .push( Row::new() .spacing(10) - .push_maybe(datadir_path.map(|_| { + .push( button::border(None, "Use another Bitcoin network") - .on_press(ViewMessage::SwitchNetwork) - })) + .on_press(ViewMessage::SwitchNetwork), + ) .push( button::primary(None, "Retry") .width(Length::Units(200)) @@ -438,32 +435,18 @@ impl From for Error { } } -fn get_datadir_path(datadir_path: &Option) -> Result { - if let Some(ref datadir) = datadir_path { - Ok(datadir.clone()) - } else { - default_datadir().map_err(|_| ConfigError::DatadirNotFound) - } -} - /// default lianad socket path is .liana/bitcoin/lianad_rpc -fn socket_path( - datadir: &Option, - network: bitcoin::Network, -) -> Result { - let mut path = get_datadir_path(datadir)?; +fn socket_path(datadir: &Path, network: bitcoin::Network) -> PathBuf { + let mut path = datadir.to_path_buf(); path.push(network.to_string()); path.push("lianad_rpc"); - Ok(path) + path } /// default liana settings path is .liana/bitcoin/settings.json -fn settings_path( - datadir: &Option, - network: bitcoin::Network, -) -> Result { - let mut path = get_datadir_path(datadir)?; +fn settings_path(datadir: &Path, network: bitcoin::Network) -> PathBuf { + let mut path = datadir.to_path_buf(); path.push(network.to_string()); path.push(settings::DEFAULT_FILE_NAME); - Ok(path) + path } diff --git a/gui/src/logger.rs b/gui/src/logger.rs new file mode 100644 index 00000000..eb1bd120 --- /dev/null +++ b/gui/src/logger.rs @@ -0,0 +1,115 @@ +use liana::miniscript::bitcoin::Network; +use std::path::PathBuf; +use std::{fs::File, sync::Arc}; +use tracing::error; +use tracing_subscriber::{ + filter, + fmt::{format, writer::BoxMakeWriter, Layer}, + prelude::*, + reload, Registry, +}; + +const INSTALLER_LOG_FILE_NAME: &str = "installer.log"; +const GUI_LOG_FILE_NAME: &str = "liana-gui.log"; + +#[derive(Debug)] +pub enum LoggerError { + Io(std::io::Error), + Reload(reload::Error), +} + +impl From for LoggerError { + fn from(e: std::io::Error) -> LoggerError { + LoggerError::Io(e) + } +} + +impl From for LoggerError { + fn from(e: reload::Error) -> LoggerError { + LoggerError::Reload(e) + } +} + +pub struct Logger { + file_handle: reload::Handle< + Layer, + Registry, + >, + level_handle: reload::Handle, +} + +impl Logger { + pub fn setup(log_level: filter::LevelFilter) -> Logger { + let (log_level, level_handle) = reload::Layer::new(log_level); + let writer = BoxMakeWriter::new(std::io::stderr); + let file_log = tracing_subscriber::fmt::layer() + .with_writer(writer) + .with_file(false); + let (file_log, file_handle) = reload::Layer::new(file_log); + let stdout_log = tracing_subscriber::fmt::layer().pretty().with_file(false); + tracing_subscriber::registry() + .with( + stdout_log + .and_then(file_log) + .with_filter(log_level) + // Add a filter to *both* layers that rejects spans and + // events whose targets start with ``. + .with_filter(filter::filter_fn(|metadata| { + !metadata.target().starts_with("iced_wgpu") + && !metadata.target().starts_with("iced_winit") + && !metadata.target().starts_with("wgpu_core") + && !metadata.target().starts_with("wgpu_hal") + && !metadata.target().starts_with("gfx_backend_vulkan") + && !metadata.target().starts_with("iced_glutin") + && !metadata.target().starts_with("iced_glow") + && !metadata.target().starts_with("glow_glyph") + && !metadata.target().starts_with("naga") + && !metadata.target().starts_with("mio") + })), + ) + .init(); + Self { + file_handle, + level_handle, + } + } + + pub fn set_installer_mode(&self, mut datadir: PathBuf, log_level: filter::LevelFilter) { + datadir.push(INSTALLER_LOG_FILE_NAME); + if let Err(e) = self.set_layer(datadir, log_level) { + error!("Failed to change logger settings: {:#?}", e); + } + } + + pub fn set_running_mode( + &self, + mut datadir: PathBuf, + network: Network, + log_level: filter::LevelFilter, + ) { + datadir.push(network.to_string()); + datadir.push(GUI_LOG_FILE_NAME); + if let Err(e) = self.set_layer(datadir, log_level) { + error!("Failed to change logger settings: {:#?}", e); + } + } + + pub fn remove_install_log_file(&self, mut datadir: PathBuf) { + datadir.push(INSTALLER_LOG_FILE_NAME); + if let Err(e) = std::fs::remove_file(datadir) { + error!("Failed to remove installer log file: {:#?}", e); + } + } + + pub fn set_layer( + &self, + destination_path: PathBuf, + log_level: filter::LevelFilter, + ) -> Result<(), LoggerError> { + let file = File::create(destination_path)?; + self.file_handle + .modify(|layer| *layer.writer_mut() = BoxMakeWriter::new(Arc::new(file)))?; + self.level_handle.modify(|filter| *filter = log_level)?; + Ok(()) + } +} diff --git a/gui/src/main.rs b/gui/src/main.rs index 5f67a7cf..914a10e6 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -1,8 +1,10 @@ #![windows_subsystem = "windows"] -use std::{error::Error, path::PathBuf, str::FromStr}; +use std::{error::Error, io::Write, path::PathBuf, str::FromStr}; use iced::{executor, Application, Command, Element, Settings, Subscription}; +use tracing::{error, info}; +use tracing_subscriber::filter::LevelFilter; extern crate serde; extern crate serde_json; @@ -17,6 +19,7 @@ use liana_gui::{ installer::{self, Installer}, launcher::{self, Launcher}, loader::{self, Loader}, + logger::Logger, }; #[derive(Debug, PartialEq)] @@ -50,23 +53,9 @@ fn parse_args(args: Vec) -> Result, Box> { Ok(res) } -fn log_level_from_config(config: &app::Config) -> Result> { - if let Some(level) = &config.log_level { - match level.as_ref() { - "info" => Ok(log::LevelFilter::Info), - "debug" => Ok(log::LevelFilter::Debug), - "trace" => Ok(log::LevelFilter::Trace), - _ => Err(format!("Unknown loglevel '{:?}'.", level).into()), - } - } else if let Some(true) = config.debug { - Ok(log::LevelFilter::Debug) - } else { - Ok(log::LevelFilter::Info) - } -} - pub struct GUI { state: State, + logger: Logger, } enum State { @@ -88,9 +77,9 @@ pub enum Message { async fn ctrl_c() -> Result<(), ()> { if let Err(e) = tokio::signal::ctrl_c().await { - log::error!("{}", e); + error!("{}", e); }; - log::info!("Signal received, exiting"); + info!("Signal received, exiting"); Ok(()) } @@ -108,21 +97,25 @@ impl Application for GUI { } fn new(config: Config) -> (GUI, Command) { + let logger = Logger::setup(LevelFilter::INFO); match config { Config::Launcher(datadir_path) => { let launcher = Launcher::new(datadir_path); ( Self { state: State::Launcher(Box::new(launcher)), + logger, }, Command::perform(ctrl_c(), |_| Message::CtrlC), ) } Config::Install(datadir_path, network) => { + logger.set_installer_mode(datadir_path.clone(), LevelFilter::INFO); let (install, command) = Installer::new(datadir_path, network); ( Self { state: State::Installer(Box::new(install)), + logger, }, Command::batch(vec![ command.map(|msg| Message::Install(Box::new(msg))), @@ -133,10 +126,16 @@ impl Application for GUI { Config::Run(datadir_path, cfg) => { let daemon_cfg = DaemonConfig::from_file(Some(cfg.daemon_config_path.clone())).unwrap(); + logger.set_running_mode( + datadir_path.clone(), + daemon_cfg.bitcoin_config.network, + cfg.log_level().unwrap_or(LevelFilter::INFO), + ); let (loader, command) = Loader::new(datadir_path, cfg, daemon_cfg); ( Self { state: State::Loader(Box::new(loader)), + logger, }, Command::batch(vec![ command.map(|msg| Message::Load(Box::new(msg))), @@ -166,13 +165,20 @@ impl Application for GUI { } (State::Launcher(l), Message::Launch(msg)) => match *msg { launcher::Message::Install(datadir_path) => { + self.logger + .set_installer_mode(datadir_path.clone(), LevelFilter::INFO); let (install, command) = Installer::new(datadir_path, bitcoin::Network::Bitcoin); self.state = State::Installer(Box::new(install)); command.map(|msg| Message::Install(Box::new(msg))) } launcher::Message::Run(datadir_path, cfg, daemon_cfg) => { - let (loader, command) = Loader::new(Some(datadir_path), cfg, daemon_cfg); + self.logger.set_running_mode( + datadir_path.clone(), + daemon_cfg.bitcoin_config.network, + cfg.log_level().unwrap_or(LevelFilter::INFO), + ); + let (loader, command) = Loader::new(datadir_path, cfg, daemon_cfg); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } @@ -183,7 +189,19 @@ impl Application for GUI { let cfg = app::Config::from_file(&path).unwrap(); let daemon_cfg = DaemonConfig::from_file(Some(cfg.daemon_config_path.clone())).unwrap(); - let (loader, command) = Loader::new(None, cfg, daemon_cfg); + let datadir_path = daemon_cfg + .data_dir + .as_ref() + .expect("Installer must have set it") + .clone(); + + self.logger.set_running_mode( + datadir_path.clone(), + daemon_cfg.bitcoin_config.network, + cfg.log_level().unwrap_or(LevelFilter::INFO), + ); + self.logger.remove_install_log_file(datadir_path.clone()); + let (loader, command) = Loader::new(datadir_path, cfg, daemon_cfg); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } else { @@ -192,9 +210,8 @@ impl Application for GUI { } (State::Loader(loader), Message::Load(msg)) => match *msg { loader::Message::View(loader::ViewMessage::SwitchNetwork) => { - self.state = State::Launcher(Box::new(Launcher::new( - loader.datadir_path.clone().unwrap(), - ))); + self.state = + State::Launcher(Box::new(Launcher::new(loader.datadir_path.clone()))); Command::none() } loader::Message::Synced(Ok((wallet, cache, daemon))) => { @@ -238,8 +255,7 @@ impl Application for GUI { } pub enum Config { - /// Datadir is optional because app can run with the config path only. - Run(Option, app::Config), + Run(PathBuf, app::Config), Launcher(PathBuf), Install(PathBuf, bitcoin::Network), } @@ -254,7 +270,7 @@ impl Config { path.push(network.to_string()); path.push(app::config::DEFAULT_FILE_NAME); match app::Config::from_file(&path) { - Ok(cfg) => Ok(Config::Run(Some(datadir_path), cfg)), + Ok(cfg) => Ok(Config::Run(datadir_path, cfg)), Err(ConfigError::NotFound) => Ok(Config::Install(datadir_path, network)), Err(e) => Err(format!("Failed to read configuration file: {}", e).into()), } @@ -277,7 +293,14 @@ fn main() -> Result<(), Box> { let datadir_path = default_datadir().unwrap(); Config::new(datadir_path, Some(*network)) } - [Arg::ConfigPath(path)] => Ok(Config::Run(None, app::Config::from_file(path)?)), + [Arg::ConfigPath(path)] => { + let cfg = app::Config::from_file(path)?; + let daemon_cfg = DaemonConfig::from_file(Some(cfg.daemon_config_path.clone()))?; + let datadir_path = daemon_cfg + .data_dir + .unwrap_or_else(|| default_datadir().unwrap()); + Ok(Config::Run(datadir_path, cfg)) + } [Arg::DatadirPath(datadir_path)] => Config::new(datadir_path.clone(), None), [Arg::DatadirPath(datadir_path), Arg::Network(network)] | [Arg::Network(network), Arg::DatadirPath(datadir_path)] => { @@ -288,12 +311,6 @@ fn main() -> Result<(), Box> { } }?; - let level = if let Config::Run(_, cfg) = &config { - log_level_from_config(cfg)? - } else { - log::LevelFilter::Info - }; - setup_logger(level)?; setup_panic_hook(); let mut settings = Settings::with_flags(config); @@ -323,62 +340,16 @@ fn setup_panic_hook() { .downcast_ref::<&str>() .map(|s| s.to_string()) .or_else(|| panic_info.payload().downcast_ref::().cloned()); - log::error!( + error!( "panic occurred at line {} of file {}: {:?}\n{:?}", - line, - file, - info, - bt + line, file, info, bt ); + std::io::stdout().flush().expect("Flushing stdout"); std::process::exit(1); })); } -// This creates the log file automagically if it doesn't exist, and logs on stdout -// if None is given -pub fn setup_logger(log_level: log::LevelFilter) -> Result<(), fern::InitError> { - let dispatcher = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "[{}][{}][{}] {}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_else(|e| { - println!("Can't get time since epoch: '{}'. Using a dummy value.", e); - std::time::Duration::from_secs(0) - }) - .as_secs(), - record.target(), - record.level(), - message - )) - }) - .level(log_level) - .level_for("iced_wgpu", log::LevelFilter::Off) - .level_for("iced_winit", log::LevelFilter::Off) - .level_for("wgpu_core", log::LevelFilter::Off) - .level_for("wgpu_hal", log::LevelFilter::Off) - .level_for("gfx_backend_vulkan", log::LevelFilter::Off) - .level_for("iced_glutin", log::LevelFilter::Off) - .level_for("iced_glow", log::LevelFilter::Off) - .level_for("glow_glyph", log::LevelFilter::Off) - .level_for("naga", log::LevelFilter::Off) - .level_for( - "ledger_transport_hid", - if log_level == log::LevelFilter::Info { - log::LevelFilter::Off - } else { - log_level - }, - ) - .level_for("mio", log::LevelFilter::Off); - - dispatcher.chain(std::io::stdout()).apply()?; - - Ok(()) -} - #[cfg(test)] mod tests { use super::*;