diff --git a/Cargo.lock b/Cargo.lock index 2568bf3..2feae6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -521,7 +521,7 @@ checksum = "031718ddb8f78aa5def78a09e90defe30151d1f6c672f937af4dd916429ed996" dependencies = [ "semver", "serde", - "toml", + "toml 0.5.11", "url", ] @@ -1718,7 +1718,9 @@ dependencies = [ "openssl-sys", "pollster 0.3.0", "rust-i18n", + "serde", "sys-locale", + "toml 0.7.4", "wgpu", "winit", ] @@ -1791,7 +1793,7 @@ dependencies = [ "rand 0.6.5", "serde", "serde_derive", - "toml", + "toml 0.5.11", ] [[package]] @@ -3541,7 +3543,7 @@ dependencies = [ "rust-i18n-macro", "serde", "serde_derive", - "toml", + "toml 0.5.11", ] [[package]] @@ -3790,6 +3792,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "serde", +] + [[package]] name = "serde_yaml" version = "0.8.26" @@ -4234,11 +4245,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4247,6 +4273,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 4dbfd45..ff48609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ rust-i18n = "1.1.4" sys-locale = "0.3.0" chrono = "0.4.23" lazy_static = "1.4.0" +toml = "0.7.4" +serde = "1.0.164" [patch.crates-io] winit = { git = "https://github.com/rib/winit", branch = "android-activity" } diff --git a/src/grim.rs b/src/grim.rs index fc8feb5..b5e3a0b 100644 --- a/src/grim.rs +++ b/src/grim.rs @@ -13,13 +13,12 @@ // limitations under the License. use eframe::{AppCreator, NativeOptions, Renderer, Theme}; -use grin_core::global::ChainTypes; -use log::LevelFilter::Info; #[cfg(target_os = "android")] use winit::platform::android::activity::AndroidApp; use crate::gui::{App, PlatformApp}; use crate::node::Node; +use crate::Settings; #[allow(dead_code)] #[cfg(target_os = "android")] @@ -28,9 +27,10 @@ fn android_main(app: AndroidApp) { #[cfg(debug_assertions)] { std::env::set_var("RUST_BACKTRACE", "full"); - android_logger::init_once( - android_logger::Config::default().with_max_level(Info).with_tag("grim"), - ); + let log_config = android_logger::Config::default() + .with_max_level(log::LevelFilter::Info) + .with_tag("grim"); + android_logger::init_once(log_config); } use crate::gui::platform::Android; @@ -73,7 +73,10 @@ fn start(mut options: NativeOptions, app_creator: AppCreator) { options.renderer = Renderer::Wgpu; setup_i18n(); - Node::start(ChainTypes::Mainnet); + + if Settings::get_app_config().auto_start_node { + Node::start(); + } eframe::run_native("Grim", options, app_creator); } diff --git a/src/lib.rs b/src/lib.rs index 248882b..a46b71f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,4 +20,7 @@ mod node; mod wallet; mod gui; -pub mod grim; \ No newline at end of file +pub mod grim; + +mod settings; +pub use settings::{Settings, AppConfig}; \ No newline at end of file diff --git a/src/node/config.rs b/src/node/config.rs new file mode 100644 index 0000000..8065c14 --- /dev/null +++ b/src/node/config.rs @@ -0,0 +1,102 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fs, thread}; +use std::fs::File; +use std::io::Write; +use std::sync::atomic::{AtomicBool, Ordering}; + +use grin_config::{config, ConfigError, ConfigMembers, GlobalConfig}; +use grin_config::config::{API_SECRET_FILE_NAME, FOREIGN_API_SECRET_FILE_NAME, SERVER_CONFIG_FILE_NAME}; +use grin_core::global::ChainTypes; +use serde::{Deserialize, Serialize}; + +use crate::Settings; + +/// Node config that contains [`GlobalConfig`] to be used by [`grin_servers::Server`]. +#[derive(Serialize, Deserialize)] +pub struct NodeConfig { + pub global_config: GlobalConfig, + update_needed: AtomicBool, + updating: AtomicBool +} + +impl NodeConfig { + /// Initialize node config with provided chain type from the disk. + pub fn init(chain_type: &ChainTypes) -> Self { + let _ = Self::check_api_secret_files(chain_type, API_SECRET_FILE_NAME); + let _ = Self::check_api_secret_files(chain_type, FOREIGN_API_SECRET_FILE_NAME); + + let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type)); + + // Create default config if it doesn't exist or has wrong format. + if !config_path.exists() || toml::from_str::( + fs::read_to_string(config_path.clone()).unwrap().as_str() + ).is_err() { + let mut default_config = GlobalConfig::for_chain(chain_type); + default_config.update_paths(&Settings::get_working_path(Some(chain_type))); + let _ = default_config.write_to_file(config_path.to_str().unwrap()); + } + + let config = GlobalConfig::new(config_path.to_str().unwrap()); + + Self { + global_config: config.unwrap(), + update_needed: AtomicBool::new(false), + updating: AtomicBool::new(false) + } + } + + /// Write node config on disk. + pub fn save_config(&self) { + if self.updating.load(Ordering::Relaxed) { + self.update_needed.store(true, Ordering::Relaxed); + return; + } + + thread::spawn(move || loop { + let config = Settings::get_node_config(); + config.update_needed.store(false, Ordering::Relaxed); + config.updating.store(true, Ordering::Relaxed); + + let chain_type = &config.global_config.members.clone().unwrap().server.chain_type; + let config_path = Settings::get_config_path(SERVER_CONFIG_FILE_NAME, Some(chain_type)); + + // Write config to file. + let conf_out = toml::to_string(&config.global_config.members).unwrap(); + let mut file = File::create(config_path.to_str().unwrap()).unwrap(); + file.write_all(conf_out.as_bytes()).unwrap(); + + if !config.update_needed.load(Ordering::Relaxed) { + config.updating.store(false, Ordering::Relaxed); + break; + } + }); + } + + /// Check that the api secret files exist and are valid. + fn check_api_secret_files( + chain_type: &ChainTypes, + secret_file_name: &str, + ) -> Result<(), ConfigError> { + let grin_path = Settings::get_working_path(Some(chain_type)); + let mut api_secret_path = grin_path; + api_secret_path.push(secret_file_name); + if !api_secret_path.exists() { + config::init_api_secret(&api_secret_path) + } else { + config::check_api_secret(&api_secret_path) + } + } +} \ No newline at end of file diff --git a/src/node/mod.rs b/src/node/mod.rs index ecac403..d0bea2f 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -13,5 +13,7 @@ // limitations under the License. mod node; +pub use node::Node; -pub use node::Node; \ No newline at end of file +mod config; +pub use config::NodeConfig; \ No newline at end of file diff --git a/src/node/node.rs b/src/node/node.rs index bbd6ab1..69516b9 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -16,12 +16,10 @@ use std::{fs, thread}; use std::path::PathBuf; use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::thread::JoinHandle; use std::time::Duration; use futures::channel::oneshot; use grin_chain::SyncStatus; -use grin_config::config; use grin_core::global; use grin_core::global::ChainTypes; use grin_servers::{Server, ServerStats}; @@ -29,6 +27,8 @@ use jni::sys::{jboolean, jstring}; use lazy_static::lazy_static; use log::info; +use crate::Settings; + lazy_static! { /// Static thread-aware state of [`Node`] to be updated from another thread. static ref NODE_STATE: Arc = Arc::new(Node::default()); @@ -38,8 +38,6 @@ lazy_static! { pub struct Node { /// Statistics data for UI. stats: Arc>>, - /// Chain type of launched server. - chain_type: Arc>, /// Indicator if server is starting. starting: AtomicBool, /// Thread flag to stop the server and start it again. @@ -54,7 +52,6 @@ impl Default for Node { fn default() -> Self { Self { stats: Arc::new(RwLock::new(None)), - chain_type: Arc::new(RwLock::new(ChainTypes::Mainnet)), starting: AtomicBool::new(false), restart_needed: AtomicBool::new(false), stop_needed: AtomicBool::new(false), @@ -70,52 +67,48 @@ impl Node { NODE_STATE.exit_after_stop.store(exit_after_stop, Ordering::Relaxed); } - /// Start [`Server`] with provided chain type. - pub fn start(chain_type: ChainTypes) { + /// Start the node. + pub fn start() { if !Self::is_running() { - let mut w_chain_type = NODE_STATE.chain_type.write().unwrap(); - *w_chain_type = chain_type; Self::start_server_thread(); } } - /// Restart [`Server`] with provided chain type. - pub fn restart(chain_type: ChainTypes) { + /// Restart the node. + pub fn restart() { if Self::is_running() { - let mut w_chain_type = NODE_STATE.chain_type.write().unwrap(); - *w_chain_type = chain_type; NODE_STATE.restart_needed.store(true, Ordering::Relaxed); } else { - Node::start(chain_type); + Node::start(); } } - /// Check if [`Server`] is starting. + /// Check if node is starting. pub fn is_starting() -> bool { NODE_STATE.starting.load(Ordering::Relaxed) } - /// Check if [`Server`] is running. + /// Check if node is running. pub fn is_running() -> bool { Self::get_sync_status().is_some() } - /// Check if [`Server`] is stopping. + /// Check if node is stopping. pub fn is_stopping() -> bool { NODE_STATE.stop_needed.load(Ordering::Relaxed) } - /// Check if [`Server`] is restarting. + /// Check if node is restarting. pub fn is_restarting() -> bool { NODE_STATE.restart_needed.load(Ordering::Relaxed) } - /// Get [`Server`] statistics. + /// Get node [`Server`] statistics. pub fn get_stats() -> RwLockReadGuard<'static, Option> { NODE_STATE.stats.read().unwrap() } - /// Get [`Server`] synchronization status, empty when Server is not running. + /// Get synchronization status, empty when [`Server`] is not running. pub fn get_sync_status() -> Option { // Return Shutdown status when node is stopping. if Self::is_stopping() { @@ -135,13 +128,13 @@ impl Node { None } - /// Start a thread to launch [`Server`] and update [`NODE_STATE`] with server statistics. + /// Start the node [`Server`] at separate thread to update [`NODE_STATE`] with [`ServerStats`]. fn start_server_thread() { thread::spawn(move || { NODE_STATE.starting.store(true, Ordering::Relaxed); // Start the server. - let mut server = start_server(&NODE_STATE.chain_type.read().unwrap()); + let mut server = start_server(); let mut first_start = true; loop { @@ -155,8 +148,8 @@ impl Node { // Stop the server. server.stop(); - // Create new server with current chain type. - server = start_server(&NODE_STATE.chain_type.read().unwrap()); + // Create new server. + server = start_server(); NODE_STATE.restart_needed.store(false, Ordering::Relaxed); } else if Self::is_stopping() { @@ -310,27 +303,12 @@ impl Node { SyncStatus::Shutdown => t!("sync_status.shutdown"), } } - } -/// Start the [`Server`] with provided chain type. -fn start_server(chain_type: &ChainTypes) -> Server { - // Initialize config - let mut node_config_result = config::initial_setup_server(chain_type); - if node_config_result.is_err() { - // Remove config file on init error - let mut grin_path = dirs::home_dir().unwrap(); - grin_path.push(".grin"); - grin_path.push(chain_type.shortname()); - grin_path.push(config::SERVER_CONFIG_FILE_NAME); - fs::remove_file(grin_path).unwrap(); - - // Reinit config - node_config_result = config::initial_setup_server(chain_type); - } - - let node_config = node_config_result.ok(); - let config = node_config.clone().unwrap(); +/// Start the node [`Server`]. +fn start_server() -> Server { + // Get current global config + let config = &Settings::get_node_config().global_config; let server_config = config.members.as_ref().unwrap().server.clone(); // Remove temporary file dir @@ -367,24 +345,19 @@ fn start_server(chain_type: &ChainTypes) -> Server { } } if !global::GLOBAL_ACCEPT_FEE_BASE.is_init() { - let afb = config - .members - .as_ref() - .unwrap() - .server - .pool_config - .accept_fee_base; + let afb = config.members.as_ref().unwrap().server.pool_config.accept_fee_base; global::init_global_accept_fee_base(afb); info!("Accept Fee Base: {:?}", global::get_accept_fee_base()); } if !global::GLOBAL_FUTURE_TIME_LIMIT.is_init() { - global::init_global_future_time_limit(config.members.unwrap().server.future_time_limit); + let future_time_limit = config.members.as_ref().unwrap().server.future_time_limit; + global::init_global_future_time_limit(future_time_limit); info!("Future Time Limit: {:?}", global::get_future_time_limit()); } let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = Box::leak(Box::new(oneshot::channel::<()>())); - let mut server_result = Server::new(server_config.clone(), None, api_chan); + let server_result = Server::new(server_config, None, api_chan); //TODO: handle server errors // // if server_result.is_err() { diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..840e72e --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,139 @@ +// Copyright 2023 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs::{self, File}; +use std::io::Write; +use std::path::PathBuf; +use std::sync::{Arc, RwLock, RwLockReadGuard}; + +use grin_config::ConfigError; +use grin_core::global::ChainTypes; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use serde::de::DeserializeOwned; + +use crate::node::NodeConfig; + +lazy_static! { + /// Static settings state to be accessible globally. + static ref SETTINGS_STATE: Arc = Arc::new(Settings::init()); +} + +const APP_CONFIG_FILE_NAME: &'static str = "app.toml"; + +/// Application settings config. +#[derive(Serialize, Deserialize)] +pub struct AppConfig { + /// Run node server on startup. + pub auto_start_node: bool, + /// Chain type for node server. + pub chain_type: ChainTypes +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + auto_start_node: false, + chain_type: ChainTypes::default(), + } + } +} + +impl AppConfig { + /// Initialize application config from the disk. + pub fn init() -> Self { + let config_path = Settings::get_config_path(APP_CONFIG_FILE_NAME, None); + let parsed = Settings::read_from_file::(config_path.clone()); + if !config_path.exists() || parsed.is_err() { + let default_config = AppConfig::default(); + Settings::write_to_file(&default_config, config_path.to_str().unwrap()); + default_config + } else { + parsed.unwrap() + } + } +} + +pub struct Settings { + app_config: Arc>, + node_config: Arc> +} + +impl Settings { + /// Initialize settings with app and node configs from the disk. + fn init() -> Self { + let app_config = AppConfig::init(); + let chain_type = app_config.chain_type; + Self { + app_config: Arc::new(RwLock::new(app_config)), + node_config: Arc::new(RwLock::new(NodeConfig::init(&chain_type))) + } + } + + pub fn get_node_config() -> RwLockReadGuard<'static, NodeConfig> { + SETTINGS_STATE.node_config.read().unwrap() + } + + pub fn get_app_config() -> RwLockReadGuard<'static, AppConfig> { + SETTINGS_STATE.app_config.read().unwrap() + } + + /// Get working directory path for application. + pub fn get_working_path(chain_type: Option<&ChainTypes>) -> PathBuf { + // Check if dir exists + let mut path = match dirs::home_dir() { + Some(p) => p, + None => PathBuf::new(), + }; + path.push(".grim"); + if chain_type.is_some() { + path.push(chain_type.unwrap().shortname()); + } + // Create if the default path doesn't exist + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + path + } + + /// Get config file path from provided name and [`ChainTypes`] if needed. + pub fn get_config_path(config_name: &str, chain_type: Option<&ChainTypes>) -> PathBuf { + let main_path = Self::get_working_path(chain_type); + let mut settings_path = main_path.clone(); + settings_path.push(config_name); + settings_path + } + + /// Read config from file + pub fn read_from_file(config_path: PathBuf) -> Result { + let file_content = fs::read_to_string(config_path.clone())?; + let parsed = toml::from_str::(file_content.as_str()); + match parsed { + Ok(cfg) => { Ok(cfg) } + Err(e) => { + return Err(ConfigError::ParseError( + config_path.to_str().unwrap().to_string(), + format!("{}", e), + )); + } + } + } + + /// Write config to a file + pub fn write_to_file(config: &T, name: &str) { + let conf_out = toml::to_string(config).unwrap(); + let mut file = File::create(name).unwrap(); + file.write_all(conf_out.as_bytes()).unwrap(); + } +} \ No newline at end of file