diff --git a/Cargo.lock b/Cargo.lock index 7580d47..f5cdb05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,6 +1121,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -4422,7 +4431,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.8.9" +version = "0.8.10" dependencies = [ "anyhow", "bincode", @@ -4459,7 +4468,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.8.9" +version = "0.8.10" dependencies = [ "anyhow", "base64 0.21.7", @@ -4489,12 +4498,13 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "tracing-appender", "tracing-subscriber", ] [[package]] name = "thunder_app_cli" -version = "0.8.9" +version = "0.8.10" dependencies = [ "anyhow", "bip300301", @@ -4508,7 +4518,7 @@ dependencies = [ [[package]] name = "thunder_app_rpc_api" -version = "0.8.9" +version = "0.8.10" dependencies = [ "bip300301", "jsonrpsee", @@ -4522,10 +4532,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -4534,6 +4546,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-bip39" version = "1.0.0" @@ -4727,6 +4749,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" diff --git a/Cargo.toml b/Cargo.toml index 45b2092..b95e9bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ authors = [ "Nikita Chashchinskii " ] edition = "2021" -version = "0.8.9" +version = "0.8.10" [workspace.dependencies.bip300301] git = "https://github.com/Ash-L2L/bip300301.git" diff --git a/app/Cargo.toml b/app/Cargo.toml index 2dfd09a..9182022 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -38,6 +38,7 @@ tiny-bip39 = "1.0.0" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } tokio-util = { version = "0.7.10", features = ["rt"] } tracing = "0.1.40" +tracing-appender = "0.2.3" tracing-subscriber = "0.3.18" [[bin]] diff --git a/app/cli.rs b/app/cli.rs index aa2713d..0599eed 100644 --- a/app/cli.rs +++ b/app/cli.rs @@ -1,43 +1,145 @@ -use clap::Parser; -use std::{net::SocketAddr, path::PathBuf}; +use clap::{Arg, Parser}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + ops::Deref, + path::PathBuf, + sync::LazyLock, +}; + +const fn ipv4_socket_addr(ipv4_octets: [u8; 4], port: u16) -> SocketAddr { + let [a, b, c, d] = ipv4_octets; + let ipv4 = Ipv4Addr::new(a, b, c, d); + SocketAddr::new(IpAddr::V4(ipv4), port) +} + +static DEFAULT_DATA_DIR: LazyLock> = + LazyLock::new(|| match dirs::data_dir() { + None => { + tracing::warn!("Failed to resolve default data dir"); + None + } + Some(data_dir) => Some(data_dir.join("thunder")), + }); + +const DEFAULT_MAIN_ADDR: SocketAddr = ipv4_socket_addr([127, 0, 0, 1], 18443); + +const DEFAULT_MAIN_USER: &str = "user"; + +const DEFAULT_MAIN_PASS: &str = "password"; + +const DEFAULT_NET_ADDR: SocketAddr = ipv4_socket_addr([0, 0, 0, 0], 4000); + +const DEFAULT_RPC_ADDR: SocketAddr = ipv4_socket_addr([127, 0, 0, 1], 2020); + +/// Implement arg manually so that there is only a default if we can resolve +/// the default data dir +#[derive(Clone, Debug)] +#[repr(transparent)] +struct DatadirArg(PathBuf); + +impl clap::FromArgMatches for DatadirArg { + fn from_arg_matches( + matches: &clap::ArgMatches, + ) -> Result { + let mut matches = matches.clone(); + Self::from_arg_matches_mut(&mut matches) + } + + fn from_arg_matches_mut( + matches: &mut clap::ArgMatches, + ) -> Result { + let datadir = matches + .remove_one::("DATADIR") + .expect("`datadir` is required"); + Ok(Self(datadir)) + } + + fn update_from_arg_matches( + &mut self, + matches: &clap::ArgMatches, + ) -> Result<(), clap::Error> { + let mut matches = matches.clone(); + self.update_from_arg_matches_mut(&mut matches) + } + + fn update_from_arg_matches_mut( + &mut self, + matches: &mut clap::ArgMatches, + ) -> Result<(), clap::Error> { + if let Some(datadir) = matches.remove_one("DATADIR") { + self.0 = datadir; + } + Ok(()) + } +} + +impl clap::Args for DatadirArg { + fn augment_args(cmd: clap::Command) -> clap::Command { + cmd.arg({ + let arg = Arg::new("DATADIR") + .value_parser(clap::builder::PathBufValueParser::new()) + .long("datadir") + .short('d') + .help("Data directory for storing blockchain and wallet data"); + match DEFAULT_DATA_DIR.deref() { + None => arg.required(true), + Some(datadir) => { + arg.required(false).default_value(datadir.as_os_str()) + } + } + }) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + Self::augment_args(cmd) + } +} #[derive(Clone, Debug, Parser)] #[command(author, version, about, long_about = None)] -pub struct Cli { - /// data directory for storing blockchain data and wallet, defaults to ~/.local/share - #[arg(short, long)] - pub datadir: Option, +pub(super) struct Cli { + /// Data directory for storing blockchain and wallet data + #[command(flatten)] + datadir: DatadirArg, /// If specified, the gui will not launch. #[arg(long)] - pub headless: bool, - /// Log level, defaults to [`tracing::Level::Debug`] + headless: bool, + /// Directory in which to store log files. + /// Defaults to `/logs/v`, where `` is thunder's data + /// directory, and `` is the thunder app version. + /// By default, only logs at the WARN level and above are logged to file. + /// If set to the empty string, logging to file will be disabled. + #[arg(long)] + log_dir: Option, + /// Log level #[arg(default_value_t = tracing::Level::DEBUG, long)] - pub log_level: tracing::Level, - /// address to use for P2P networking, defaults to 0.0.0.0:4000 - #[arg(short, long)] - pub net_addr: Option, - /// address to connect to mainchain node RPC server, defaults to 127.0.0.1:18443 - #[arg(short, long)] - pub main_addr: Option, + log_level: tracing::Level, + /// Socket address to connect to mainchain node RPC server + #[arg(default_value_t = DEFAULT_MAIN_ADDR, long, short)] + main_addr: SocketAddr, /// Path to a mnemonic seed phrase #[arg(long)] - pub mnemonic_seed_phrase_path: Option, - /// address for use by the RPC server exposing getblockcount and stop commands, defaults to - /// 127.0.0.1:2020 - #[arg(short, long)] - pub rpc_addr: Option, - /// mainchain node RPC user, defaults to "user" - #[arg(short, long)] - pub user_main: Option, - /// mainchain node RPC password, defaults to "password" - #[arg(short, long)] - pub password_main: Option, + mnemonic_seed_phrase_path: Option, + /// Socket address to use for P2P networking + #[arg(default_value_t = DEFAULT_NET_ADDR, long, short)] + net_addr: SocketAddr, + /// Socket address to host the RPC server + #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] + rpc_addr: SocketAddr, + /// Mainchain node RPC user + #[arg(default_value_t = DEFAULT_MAIN_USER.to_owned(), long, short)] + user_main: String, + /// Mainchain node RPC password + #[arg(default_value_t = DEFAULT_MAIN_PASS.to_owned(), long, short)] + password_main: String, } #[derive(Clone, Debug)] pub struct Config { pub datadir: PathBuf, pub headless: bool, + /// If None, logging to file should be disabled. + pub log_dir: Option, pub log_level: tracing::Level, pub main_addr: SocketAddr, pub main_password: String, @@ -49,47 +151,33 @@ pub struct Config { impl Cli { pub fn get_config(self) -> anyhow::Result { - let datadir = self - .datadir - .clone() - .unwrap_or_else(|| { - dirs::data_dir() - .expect("couldn't get default datadir, specify --datadir") - }) - .join("thunder"); - const DEFAULT_MAIN_ADDR: &str = "127.0.0.1:18443"; - let main_addr: SocketAddr = self - .main_addr - .clone() - .unwrap_or(DEFAULT_MAIN_ADDR.to_string()) - .parse()?; - let main_password = self - .password_main - .clone() - .unwrap_or_else(|| "password".into()); - let main_user = self.user_main.clone().unwrap_or_else(|| "user".into()); - const DEFAULT_NET_ADDR: &str = "0.0.0.0:4000"; - let net_addr: SocketAddr = self - .net_addr - .clone() - .unwrap_or(DEFAULT_NET_ADDR.to_string()) - .parse()?; - const DEFAULT_RPC_ADDR: &str = "127.0.0.1:2020"; - let rpc_addr: SocketAddr = self - .rpc_addr - .clone() - .unwrap_or(DEFAULT_RPC_ADDR.to_string()) - .parse()?; + let log_dir = match self.log_dir { + None => { + let version_dir_name = + format!("v{}", env!("CARGO_PKG_VERSION")); + let log_dir = + self.datadir.0.join("logs").join(version_dir_name); + Some(log_dir) + } + Some(log_dir) => { + if log_dir.as_os_str().is_empty() { + None + } else { + Some(log_dir) + } + } + }; Ok(Config { - datadir, + datadir: self.datadir.0, headless: self.headless, + log_dir, log_level: self.log_level, - main_addr, - main_password, - main_user, + main_addr: self.main_addr, + main_password: self.password_main, + main_user: self.user_main, mnemonic_seed_phrase_path: self.mnemonic_seed_phrase_path, - net_addr, - rpc_addr, + net_addr: self.net_addr, + rpc_addr: self.rpc_addr, }) } } diff --git a/app/main.rs b/app/main.rs index 302c5f8..3ab4ea4 100644 --- a/app/main.rs +++ b/app/main.rs @@ -1,10 +1,12 @@ #![feature(let_chains)] -use std::sync::mpsc; +use std::{path::Path, sync::mpsc}; use clap::Parser as _; -use tracing_subscriber::{filter as tracing_filter, layer::SubscriberExt}; +use tracing_subscriber::{ + filter as tracing_filter, layer::SubscriberExt, Layer, +}; mod app; mod cli; @@ -15,8 +17,45 @@ mod util; use line_buffer::{LineBuffer, LineBufferWriter}; -// Configure logger -fn set_tracing_subscriber(log_level: tracing::Level) -> LineBuffer { +/// Must be held for the lifetime of the program in order to keep the file +/// logger alive. +type RollingLoggerGuard = tracing_appender::non_blocking::WorkerGuard; + +/// Rolling file logger. +/// Returns a guard that must be held for the lifetime of the program in order +/// to keep the file logger alive. +fn rolling_logger( + log_dir: &Path, +) -> anyhow::Result<(impl Layer, RollingLoggerGuard)> +where + S: tracing::Subscriber + + for<'s> tracing_subscriber::registry::LookupSpan<'s>, +{ + const DEFAULT_LEVEL: tracing::Level = tracing::Level::WARN; + const LOG_FILE_SUFFIX: &str = "log"; + let rolling_log_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .filename_suffix(LOG_FILE_SUFFIX) + .build(log_dir)?; + let (non_blocking_rolling_log_writer, rolling_log_guard) = + tracing_appender::non_blocking(rolling_log_appender); + let level_filter = + tracing_filter::Targets::new().with_default(DEFAULT_LEVEL); + let rolling_log_layer = tracing_subscriber::fmt::layer() + .compact() + .with_ansi(false) + .with_writer(non_blocking_rolling_log_writer) + .with_filter(level_filter); + Ok((rolling_log_layer, rolling_log_guard)) +} + +// Configure loggers. +// If the file logger is set, returns a guard that must be held for the +// lifetime of the program in order to keep the file logger alive. +fn set_tracing_subscriber( + log_dir: Option<&Path>, + log_level: tracing::Level, +) -> anyhow::Result<(LineBuffer, Option)> { let targets_filter = tracing_filter::Targets::new().with_targets([ ("bip300301", log_level), ("jsonrpsee_core::tracing", log_level), @@ -27,6 +66,13 @@ fn set_tracing_subscriber(log_level: tracing::Level) -> LineBuffer { let stdout_layer = tracing_subscriber::fmt::layer() .compact() .with_line_number(true); + let (rolling_log_layer, rolling_log_guard) = match log_dir { + None => (None, None), + Some(log_dir) => { + let (layer, guard) = rolling_logger(log_dir)?; + (Some(layer), Some(guard)) + } + }; let capture_layer = tracing_subscriber::fmt::layer() .compact() .with_line_number(true) @@ -35,16 +81,18 @@ fn set_tracing_subscriber(log_level: tracing::Level) -> LineBuffer { let tracing_subscriber = tracing_subscriber::registry() .with(targets_filter) .with(stdout_layer) - .with(capture_layer); + .with(capture_layer) + .with(rolling_log_layer); tracing::subscriber::set_global_default(tracing_subscriber) .expect("setting default subscriber failed"); - line_buffer + Ok((line_buffer, rolling_log_guard)) } fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); let config = cli.get_config()?; - let line_buffer = set_tracing_subscriber(config.log_level); + let (line_buffer, _rolling_log_guard) = + set_tracing_subscriber(config.log_dir.as_deref(), config.log_level)?; let app = app::App::new(&config)?; // spawn rpc server app.runtime.spawn({