From 23ac36a8340c083bf6ac8ab0d6236213cbaef3e3 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Fri, 9 Mar 2018 17:16:31 +0000 Subject: [PATCH] Basic TUI Integration (#756) * first explorations at attempting to integrate more user-friendly status screen/ui * rustfmt * adding some logo and color for visual interest * formatting * better integration with stdout, cleaner looking startup and shutdown * rustfmt * update to framework, and first collection of stats from server * rustfmt * commit of basic stat screen, think it's in a good enough state to share * rustfmt * fix to automated tests * fix to automated tests * grin.toml setting * grin.toml setting * grin.toml setting --- Cargo.toml | 3 +- chain/tests/data_file_integrity.rs | 2 +- chain/tests/mine_simple_chain.rs | 2 +- chain/tests/test_coinbase_maturity.rs | 2 +- grin.toml | 36 +-- grin/src/miner.rs | 44 ++- grin/src/server.rs | 35 ++- grin/src/sync.rs | 3 + grin/src/types.rs | 71 ++++- grin/tests/framework/mod.rs | 7 +- grin/tests/simulnet.rs | 9 +- pow/src/lib.rs | 2 +- pow/src/plugin.rs | 8 +- pow/src/types.rs | 18 +- src/bin/grin.rs | 66 ++++- src/bin/ui.rs | 394 ++++++++++++++++++++++++++ 16 files changed, 630 insertions(+), 72 deletions(-) create mode 100644 src/bin/ui.rs diff --git a/Cargo.toml b/Cargo.toml index 86de1ad7d..e0116bb1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,8 @@ serde = "~1.0.8" serde_json = "~1.0.7" slog = { version = "^2.0.12", features = ["max_level_trace", "release_max_level_trace"] } term = "~0.4.6" - +time = "^0.1" +cursive = { git = "https://github.com/gyscos/Cursive" } # TODO - once "patch" is available we should be able to clean up the workspace dependencies # [patch.crate-io] # secp256k1zkp = { git = "https://github.com/mimblewimble/rust-secp256k1-zkp" } diff --git a/chain/tests/data_file_integrity.rs b/chain/tests/data_file_integrity.rs index f100c0ba5..d8fabadcf 100644 --- a/chain/tests/data_file_integrity.rs +++ b/chain/tests/data_file_integrity.rs @@ -77,7 +77,7 @@ fn data_files() { burn_reward: true, ..Default::default() }; - miner_config.cuckoo_miner_plugin_dir = Some(String::from("../target/debug/deps")); + miner_config.miner_plugin_dir = Some(String::from("../target/debug/deps")); let mut cuckoo_miner = cuckoo::Miner::new( consensus::EASINESS, diff --git a/chain/tests/mine_simple_chain.rs b/chain/tests/mine_simple_chain.rs index 90f7d7bb2..4a342aa06 100644 --- a/chain/tests/mine_simple_chain.rs +++ b/chain/tests/mine_simple_chain.rs @@ -65,7 +65,7 @@ fn mine_empty_chain() { burn_reward: true, ..Default::default() }; - miner_config.cuckoo_miner_plugin_dir = Some(String::from("../target/debug/deps")); + miner_config.miner_plugin_dir = Some(String::from("../target/debug/deps")); let mut cuckoo_miner = cuckoo::Miner::new( consensus::EASINESS, diff --git a/chain/tests/test_coinbase_maturity.rs b/chain/tests/test_coinbase_maturity.rs index 9ec945748..31ea4daab 100644 --- a/chain/tests/test_coinbase_maturity.rs +++ b/chain/tests/test_coinbase_maturity.rs @@ -60,7 +60,7 @@ fn test_coinbase_maturity() { burn_reward: true, ..Default::default() }; - miner_config.cuckoo_miner_plugin_dir = Some(String::from("../target/debug/deps")); + miner_config.miner_plugin_dir = Some(String::from("../target/debug/deps")); let mut cuckoo_miner = cuckoo::Miner::new( consensus::EASINESS, diff --git a/grin.toml b/grin.toml index 65d9a02a8..a16eb775e 100644 --- a/grin.toml +++ b/grin.toml @@ -52,6 +52,10 @@ capabilities = [7] #skip waiting for sync on startup, (optional param, mostly for testing) #skip_sync_wait = false +#whether to run the ncurses TUI. Ncurses must be installed and this +#will also disable logging to stdout +run_tui = false + #The P2P server details (i.e. the server that communicates with other #grin server nodes @@ -99,23 +103,19 @@ log_file_append = true #flag whether mining is enabled -enable_mining = false +enable_mining = true -#Whether to use cuckoo-miner, and related parameters - -use_cuckoo_miner = true - -#Whether to use async mode for cuckoo miner, if the plugin supports it. +#Whether to use async mode for the miner, if the plugin supports it. #this allows for many searches to be run in parallel, e.g. if the system #has multiple GPUs, or if you want to mine using multiple plugins -cuckoo_miner_async_mode = false +miner_async_mode = false -#If using cuckoo_miner, the directory in which plugins are installed +#The directory in which mining plugins are installed #if not specified, grin will look in the directory /deps relative #to the executable -#cuckoo_miner_plugin_dir = "target/debug/plugins" +#miner_plugin_dir = "target/debug/plugins" #The amount of time, in seconds, to attempt to mine on a particular #header before stopping and re-collecting transactions from the pool @@ -160,23 +160,23 @@ burn_reward = false #Also requires instructions that aren't available on #older processors. In this case, use mean_compat_cpu #instead -#[[mining.cuckoo_miner_plugin_config]] +#[[mining.miner_plugin_config]] #type_filter = "mean_cpu" #[mining.cuckoo_miner_plugin_config.device_parameters.0] #NUM_THREADS = 1 #As above, but for older processors -[[mining.cuckoo_miner_plugin_config]] +[[mining.miner_plugin_config]] type_filter = "mean_compat_cpu" -[mining.cuckoo_miner_plugin_config.device_parameters.0] +[mining.miner_plugin_config.device_parameters.0] NUM_THREADS = 1 #note lean_cpu currently has a bug which prevents it from #working with threads > 1 -#[[mining.cuckoo_miner_plugin_config]] +#[[mining.miner_plugin_config]] #type_filter = "lean_cpu" -#[mining.cuckoo_miner_plugin_config.device_parameters.0] +#[mining.miner_plugin_config.device_parameters.0] #NUM_THREADS = 1 #CUDA Miner (Included here for integration only, Not ready for use) @@ -193,9 +193,9 @@ NUM_THREADS = 1 #disabled unless explicitly enabled by setting the 'USE_DEVICE' #param to 1 on each device, as demonstrated below. -#[[mining.cuckoo_miner_plugin_config]] +#[[mining.miner_plugin_config]] #type_filter = "cuda" -#[mining.cuckoo_miner_plugin_config.device_parameters.0] +#[mining.miner_plugin_config.device_parameters.0] #USE_DEVICE = 1 # Below are advanced optional per-device tweakable params @@ -215,8 +215,8 @@ NUM_THREADS = 1 #TRIM_3_TPB = 32 #RENAME_3_TPB = 8 -#[mining.cuckoo_miner_plugin_config.device_parameters.1] +#[mining.miner_plugin_config.device_parameters.1] #USE_DEVICE = 1 -#[mining.cuckoo_miner_plugin_config.device_parameters.2] +#[mining.miner_plugin_config.device_parameters.2] #USE_DEVICE = 1 diff --git a/grin/src/miner.rs b/grin/src/miner.rs index dbbc8afc5..85610a748 100644 --- a/grin/src/miner.rs +++ b/grin/src/miner.rs @@ -26,16 +26,16 @@ use adapters::PoolToChainAdapter; use core::consensus; use core::core; use core::core::Proof; -use pow::cuckoo; use core::core::target::Difficulty; use core::core::{Block, BlockHeader, Transaction}; use core::core::hash::{Hash, Hashed}; -use pow::MiningWorker; +use pow::{cuckoo, MiningWorker}; use pow::types::MinerConfig; use core::ser; +use core::global; use core::ser::AsFixedBytes; use util::LOGGER; -use types::Error; +use types::{Error, MiningStats}; use chain; use pool; @@ -158,6 +158,7 @@ impl Miner { head: &BlockHeader, latest_hash: &Hash, attempt_time_per_block: u32, + mining_stats: Arc>, ) -> Option { debug!( LOGGER, @@ -250,6 +251,10 @@ impl Miner { } } info!(LOGGER, "Mining at {} graphs per second", sps_total); + if sps_total.is_finite() { + let mut mining_stats = mining_stats.write().unwrap(); + mining_stats.combined_gps = sps_total; + } next_stat_output = time::get_time().sec + stat_output_interval; } // avoid busy wait @@ -277,6 +282,7 @@ impl Miner { head: &BlockHeader, attempt_time_per_block: u32, latest_hash: &mut Hash, + mining_stats: Arc>, ) -> Option { // look for a pow for at most attempt_time_per_block sec on the same block (to // give a chance to new @@ -350,6 +356,10 @@ impl Miner { LOGGER, "Mining at {} graphs per second", last_hashes_per_sec ); + if last_hashes_per_sec.is_finite() { + let mut mining_stats = mining_stats.write().unwrap(); + mining_stats.combined_gps = last_hashes_per_sec; + } } next_stat_check = time::get_time().sec + stat_check_interval; } @@ -381,6 +391,7 @@ impl Miner { } /// The inner part of mining loop for the internal miner + /// kept around mostly for automated testing purposes pub fn inner_loop_sync_internal( &self, miner: &mut T, @@ -453,14 +464,21 @@ impl Miner { /// Starts the mining loop, building a new block on top of the existing /// chain anytime required and looking for PoW solution. - pub fn run_loop(&self, miner_config: MinerConfig, cuckoo_size: u32, proof_size: usize) { + pub fn run_loop( + &self, + miner_config: MinerConfig, + mining_stats: Arc>, + cuckoo_size: u32, + proof_size: usize, + ) { info!( LOGGER, "(Server ID: {}) Starting miner loop.", self.debug_output_id ); + let mut plugin_miner = None; let mut miner = None; - if miner_config.use_cuckoo_miner { + if !global::is_automated_testing_mode() { plugin_miner = Some(PluginMiner::new( consensus::EASINESS, cuckoo_size, @@ -475,11 +493,16 @@ impl Miner { )); } - // to prevent the wallet from generating a new HD key derivation for each // iteration, we keep the returned derivation to provide it back when // nothing has changed let mut key_id = None; + { + let mut mining_stats = mining_stats.write().unwrap(); + mining_stats.is_mining = true; + mining_stats.cuckoo_size = cuckoo_size as u16; + } + loop { debug!(LOGGER, "in miner loop..."); trace!(LOGGER, "key_id: {:?}", key_id); @@ -503,10 +526,15 @@ impl Miner { } let (mut b, block_fees) = result.unwrap(); + { + let mut mining_stats = mining_stats.write().unwrap(); + mining_stats.block_height = b.header.height; + mining_stats.network_difficulty = b.header.difficulty.into_num(); + } let mut sol = None; let mut use_async = false; - if let Some(c) = self.config.cuckoo_miner_async_mode { + if let Some(c) = self.config.miner_async_mode { if c { use_async = true; } @@ -521,6 +549,7 @@ impl Miner { &head, &latest_hash, miner_config.attempt_time_per_block, + mining_stats.clone(), ); } else { sol = self.inner_loop_sync_plugin( @@ -530,6 +559,7 @@ impl Miner { &head, miner_config.attempt_time_per_block, &mut latest_hash, + mining_stats.clone(), ); } } diff --git a/grin/src/server.rs b/grin/src/server.rs index 3468158e8..44bcfb1ab 100644 --- a/grin/src/server.rs +++ b/grin/src/server.rs @@ -45,19 +45,33 @@ pub struct Server { pub chain: Arc, /// in-memory transaction pool tx_pool: Arc>>, + /// Whether we're currently syncing currently_syncing: Arc, + /// To be passed around to collect stats and info + state_info: ServerStateInfo, + /// Stop flag stop: Arc, } impl Server { - /// Instantiates and starts a new server. - pub fn start(config: ServerConfig) -> Result<(), Error> { + /// Instantiates and starts a new server. Optionally takes a callback + /// for the server to send an ARC copy of itself, to allow another process + /// to poll info about the server status + pub fn start(config: ServerConfig, mut info_callback: F) -> Result<(), Error> + where + F: FnMut(Arc), + { let mut mining_config = config.mining_config.clone(); - let serv = Server::new(config)?; + let serv = Arc::new(Server::new(config)?); if mining_config.as_mut().unwrap().enable_mining { + { + let mut mining_stats = serv.state_info.mining_stats.write().unwrap(); + mining_stats.is_enabled = true; + } serv.start_miner(mining_config.unwrap()); } + info_callback(serv.clone()); loop { thread::sleep(time::Duration::from_secs(1)); if serv.stop.load(Ordering::Relaxed) { @@ -97,6 +111,7 @@ impl Server { pool_adapter.set_chain(Arc::downgrade(&shared_chain)); let currently_syncing = Arc::new(AtomicBool::new(true)); + let awaiting_peers = Arc::new(AtomicBool::new(false)); let net_adapter = Arc::new(NetToChainAdapter::new( currently_syncing.clone(), @@ -154,6 +169,7 @@ impl Server { sync::run_sync( currently_syncing.clone(), + awaiting_peers.clone(), p2p_server.peers.clone(), shared_chain.clone(), skip_sync_wait, @@ -182,6 +198,10 @@ impl Server { chain: shared_chain, tx_pool: tx_pool, currently_syncing: currently_syncing, + state_info: ServerStateInfo { + awaiting_peers: awaiting_peers, + ..Default::default() + }, stop: stop, }) } @@ -210,6 +230,7 @@ impl Server { self.tx_pool.clone(), self.stop.clone(), ); + let mining_stats = self.state_info.mining_stats.clone(); miner.set_debug_output_id(format!("Port {}", self.config.p2p_config.port)); let _ = thread::Builder::new() .name("miner".to_string()) @@ -220,7 +241,7 @@ impl Server { while currently_syncing.load(Ordering::Relaxed) { thread::sleep(secs_5); } - miner.run_loop(config.clone(), cuckoo_size as u32, proof_size); + miner.run_loop(config.clone(), mining_stats, cuckoo_size as u32, proof_size); }); } @@ -240,9 +261,15 @@ impl Server { /// other /// consumers pub fn get_server_stats(&self) -> Result { + let mining_stats = self.state_info.mining_stats.read().unwrap().clone(); + let awaiting_peers = self.state_info.awaiting_peers.load(Ordering::Relaxed); Ok(ServerStats { peer_count: self.peer_count(), head: self.head(), + header_head: self.header_head(), + is_syncing: self.currently_syncing.load(Ordering::Relaxed), + awaiting_peers: awaiting_peers, + mining_stats: mining_stats, }) } diff --git a/grin/src/sync.rs b/grin/src/sync.rs index dac9f209c..27a465b49 100644 --- a/grin/src/sync.rs +++ b/grin/src/sync.rs @@ -29,6 +29,7 @@ use util::LOGGER; /// Starts the syncing loop, just spawns two threads that loop forever pub fn run_sync( currently_syncing: Arc, + awaiting_peers: Arc, peers: Arc, chain: Arc, skip_sync_wait: bool, @@ -45,7 +46,9 @@ pub fn run_sync( // initial sleep to give us time to peer with some nodes if !skip_sync_wait { + awaiting_peers.store(true, Ordering::Relaxed); thread::sleep(Duration::from_secs(30)); + awaiting_peers.store(false, Ordering::Relaxed); } // fast sync has 3 states: diff --git a/grin/src/types.rs b/grin/src/types.rs index a4a9cc8d7..9c538e65e 100644 --- a/grin/src/types.rs +++ b/grin/src/types.rs @@ -13,6 +13,8 @@ // limitations under the License. use std::convert::From; +use std::sync::{Arc, RwLock}; +use std::sync::atomic::AtomicBool; use api; use chain; @@ -145,6 +147,10 @@ pub struct ServerConfig { /// Whether to skip the sync timeout on startup /// (To assist testing on solo chains) pub skip_sync_wait: Option, + + /// Whether to run the TUI + /// if enabled, this will disable logging to stdout + pub run_tui: Option, } impl Default for ServerConfig { @@ -161,19 +167,74 @@ impl Default for ServerConfig { archive_mode: None, pool_config: pool::PoolConfig::default(), skip_sync_wait: None, + run_tui: None, } } } -/// Thread-safe container to return all server related stats that other -/// consumers might be interested in, such as test results -/// -/// -/// +/// Server state info collection struct, to be passed around into internals +/// and populated when required +#[derive(Clone)] +pub struct ServerStateInfo { + /// whether we're in a state of waiting for peers at startup + pub awaiting_peers: Arc, + /// Mining stats + pub mining_stats: Arc>, +} + +impl Default for ServerStateInfo { + fn default() -> ServerStateInfo { + ServerStateInfo { + awaiting_peers: Arc::new(AtomicBool::new(false)), + mining_stats: Arc::new(RwLock::new(MiningStats::default())), + } + } +} +/// Simpler thread-unware version of above to be populated and retured to +/// consumers might be interested in, such as test results or UI #[derive(Clone)] pub struct ServerStats { /// Number of peers pub peer_count: u32, /// Chain head pub head: chain::Tip, + /// sync header head + pub header_head: chain::Tip, + /// Whether we're currently syncing + pub is_syncing: bool, + /// Whether we're awaiting peers + pub awaiting_peers: bool, + /// Handle to current mining stats + pub mining_stats: MiningStats, +} + +/// Struct to return relevant information about the mining process +/// back to interested callers (such as the TUI) +#[derive(Clone)] +pub struct MiningStats { + /// whether mining is enabled + pub is_enabled: bool, + /// whether we're currently mining + pub is_mining: bool, + /// combined graphs per second + pub combined_gps: f64, + /// what block height we're mining at + pub block_height: u64, + /// current network difficulty we're working on + pub network_difficulty: u64, + /// cuckoo size used for mining + pub cuckoo_size: u16, +} + +impl Default for MiningStats { + fn default() -> MiningStats { + MiningStats { + is_enabled: false, + is_mining: false, + combined_gps: 0.0, + block_height: 0, + network_difficulty: 0, + cuckoo_size: 0, + } + } } diff --git a/grin/tests/framework/mod.rs b/grin/tests/framework/mod.rs index 546dc3dae..062a13de6 100644 --- a/grin/tests/framework/mod.rs +++ b/grin/tests/framework/mod.rs @@ -219,10 +219,9 @@ impl LocalServerContainer { let miner_config = pow::types::MinerConfig { enable_mining: self.config.start_miner, burn_reward: self.config.burn_mining_rewards, - use_cuckoo_miner: false, - cuckoo_miner_async_mode: Some(false), - cuckoo_miner_plugin_dir: Some(String::from("../target/debug/deps")), - cuckoo_miner_plugin_config: Some(plugin_config_vec), + miner_async_mode: Some(false), + miner_plugin_dir: None, + miner_plugin_config: Some(plugin_config_vec), wallet_listener_url: self.config.coinbase_wallet_address.clone(), slow_down_in_millis: Some(self.config.miner_slowdown_in_millis.clone()), ..Default::default() diff --git a/grin/tests/simulnet.rs b/grin/tests/simulnet.rs index e0a3f44fd..07cbbb715 100644 --- a/grin/tests/simulnet.rs +++ b/grin/tests/simulnet.rs @@ -48,7 +48,7 @@ fn basic_genesis_mine() { // Create a server pool let mut pool_config = LocalServerContainerPoolConfig::default(); pool_config.base_name = String::from(test_name_dir); - pool_config.run_length_in_seconds = 5; + pool_config.run_length_in_seconds = 10; pool_config.base_api_port = 30000; pool_config.base_p2p_port = 31000; @@ -338,10 +338,9 @@ fn miner_config() -> pow::types::MinerConfig { pow::types::MinerConfig { enable_mining: true, burn_reward: true, - use_cuckoo_miner: false, - cuckoo_miner_async_mode: Some(false), - cuckoo_miner_plugin_dir: Some(String::from("../target/debug/deps")), - cuckoo_miner_plugin_config: Some(plugin_config_vec), + miner_async_mode: Some(false), + miner_plugin_dir: None, + miner_plugin_config: Some(plugin_config_vec), ..Default::default() } } diff --git a/pow/src/lib.rs b/pow/src/lib.rs index 608ff0e87..645116636 100644 --- a/pow/src/lib.rs +++ b/pow/src/lib.rs @@ -105,7 +105,7 @@ pub fn mine_genesis_block( let proof_size = global::proofsize(); let mut miner: Box = match miner_config { - Some(c) => if c.use_cuckoo_miner { + Some(c) => if c.enable_mining { let mut p = plugin::PluginMiner::new(consensus::EASINESS, sz, proof_size); p.init(c.clone()); Box::new(p) diff --git a/pow/src/plugin.rs b/pow/src/plugin.rs index c5b2c23ce..706bc642d 100644 --- a/pow/src/plugin.rs +++ b/pow/src/plugin.rs @@ -69,16 +69,16 @@ impl PluginMiner { let mut exe_path = env::current_exe().unwrap(); exe_path.pop(); let exe_path = exe_path.to_str().unwrap(); - let plugin_install_path = match miner_config.cuckoo_miner_plugin_dir.clone() { + let plugin_install_path = match miner_config.miner_plugin_dir.clone() { Some(s) => s, None => String::from(format!("{}/plugins", exe_path)), }; let mut plugin_vec_filters = Vec::new(); - if let None = miner_config.cuckoo_miner_plugin_config { + if let None = miner_config.miner_plugin_config { plugin_vec_filters.push(String::from("simple")); } else { - for p in miner_config.clone().cuckoo_miner_plugin_config.unwrap() { + for p in miner_config.clone().miner_plugin_config.unwrap() { plugin_vec_filters.push(p.type_filter); } } @@ -132,7 +132,7 @@ impl PluginMiner { caps[0].full_path.clone() ); config.plugin_full_path = caps[0].full_path.clone(); - if let Some(l) = miner_config.clone().cuckoo_miner_plugin_config { + if let Some(l) = miner_config.clone().miner_plugin_config { if let Some(dp) = l[index].device_parameters.clone() { for (device, param_map) in dp.into_iter() { for (param_name, param_value) in param_map.into_iter() { diff --git a/pow/src/types.rs b/pow/src/types.rs index 1ae1a546b..04b41adaf 100644 --- a/pow/src/types.rs +++ b/pow/src/types.rs @@ -37,20 +37,17 @@ impl Default for CuckooMinerPluginConfig { /// Mining configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MinerConfig { - /// Whether to start the miner with the server + /// Whether to start the miner with the server (requires using cuckoo-miner) pub enable_mining: bool, - /// Whether to use the cuckoo-miner crate and plugin for mining - pub use_cuckoo_miner: bool, - /// Whether to use the async version of mining - pub cuckoo_miner_async_mode: Option, + pub miner_async_mode: Option, /// plugin dir - pub cuckoo_miner_plugin_dir: Option, + pub miner_plugin_dir: Option, /// Cuckoo miner plugin configuration, one for each plugin - pub cuckoo_miner_plugin_config: Option>, + pub miner_plugin_config: Option>, /// How long to wait before stopping the miner, recollecting transactions /// and starting again @@ -72,10 +69,9 @@ impl Default for MinerConfig { fn default() -> MinerConfig { MinerConfig { enable_mining: false, - use_cuckoo_miner: false, - cuckoo_miner_async_mode: None, - cuckoo_miner_plugin_dir: None, - cuckoo_miner_plugin_config: None, + miner_async_mode: None, + miner_plugin_dir: None, + miner_plugin_config: None, wallet_listener_url: "http://localhost:13415".to_string(), burn_reward: false, slow_down_in_millis: Some(0), diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 7ed5ce24b..b2d289f1b 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -16,11 +16,13 @@ extern crate blake2_rfc as blake2; extern crate clap; +extern crate cursive; extern crate daemonize; extern crate serde; extern crate serde_json; #[macro_use] extern crate slog; +extern crate time; extern crate grin_api as api; extern crate grin_config as config; @@ -32,10 +34,13 @@ extern crate grin_util as util; extern crate grin_wallet as wallet; mod client; +mod ui; use std::thread; +use std::sync::Arc; use std::time::Duration; use std::env::current_dir; +use std::process::exit; use clap::{App, Arg, ArgMatches, SubCommand}; use daemonize::Daemonize; @@ -45,6 +50,38 @@ use core::global; use core::core::amount_to_hr_string; use util::{init_logger, LoggingConfig, LOGGER}; +/// wrap below to allow UI to clean up on stop +fn start_server(config: grin::ServerConfig) { + start_server_tui(config); + // Just kill process for now, otherwise the process + // hangs around until sigint because the API server + // currently has no shutdown facility + println!("Shutting down..."); + thread::sleep(Duration::from_millis(1000)); + println!("Shutdown complete."); + exit(0); +} + +fn start_server_tui(config: grin::ServerConfig) { + // Run the UI controller.. here for now for simplicity to access + // everything it might need + if config.run_tui.is_some() && config.run_tui.unwrap() { + println!("Starting GRIN in UI mode..."); + grin::Server::start(config, |serv: Arc| { + let _ = thread::Builder::new() + .name("ui".to_string()) + .spawn(move || { + let mut controller = ui::Controller::new().unwrap_or_else(|e| { + panic!("Error loading UI controller: {}", e); + }); + controller.run(serv.clone()); + }); + }).unwrap(); + } else { + grin::Server::start(config, |_| {}).unwrap(); + } +} + fn start_from_config_file(mut global_config: GlobalConfig) { info!( LOGGER, @@ -62,7 +99,7 @@ fn start_from_config_file(mut global_config: GlobalConfig) { .chain_type, ); - grin::Server::start(global_config.members.as_mut().unwrap().server.clone()).unwrap(); + start_server(global_config.members.as_mut().unwrap().server.clone()); loop { thread::sleep(Duration::from_secs(60)); } @@ -85,7 +122,18 @@ fn main() { if global_config.using_config_file { // initialise the logger - init_logger(global_config.members.as_mut().unwrap().logging.clone()); + let mut log_conf = global_config + .members + .as_mut() + .unwrap() + .logging + .clone() + .unwrap(); + let run_tui = global_config.members.as_mut().unwrap().server.run_tui; + if run_tui.is_some() && run_tui.unwrap() { + log_conf.log_to_stdout = false; + } + init_logger(Some(log_conf)); info!( LOGGER, "Using configuration file at: {}", @@ -351,7 +399,7 @@ fn server_command(server_args: &ArgMatches, global_config: GlobalConfig) { // start the server in the different run modes (interactive or daemon) match server_args.subcommand() { ("run", _) => { - grin::Server::start(server_config).unwrap(); + start_server(server_config); } ("start", _) => { let daemonize = Daemonize::new() @@ -359,7 +407,7 @@ fn server_command(server_args: &ArgMatches, global_config: GlobalConfig) { .chown_pid_file(true) .working_directory(current_dir().unwrap()) .privileged_action(move || { - grin::Server::start(server_config.clone()).unwrap(); + start_server(server_config.clone()); loop { thread::sleep(Duration::from_secs(60)); } @@ -532,11 +580,11 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { recipient_fee, } => { error!( - LOGGER, - "Recipient rejected the transfer because transaction fee ({}) exceeded amount ({}).", - amount_to_hr_string(recipient_fee), - amount_to_hr_string(sender_amount) - ); + LOGGER, + "Recipient rejected the transfer because transaction fee ({}) exceeded amount ({}).", + amount_to_hr_string(recipient_fee), + amount_to_hr_string(sender_amount) + ); } _ => { error!(LOGGER, "Tx not sent: {:?}", e); diff --git a/src/bin/ui.rs b/src/bin/ui.rs new file mode 100644 index 000000000..3b4f2000b --- /dev/null +++ b/src/bin/ui.rs @@ -0,0 +1,394 @@ +// Copyright 2018 The Grin 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. + +//! Basic TUI to better output the overall system status and status +//! of various subsystems + +use std::sync::{mpsc, Arc}; +use time; + +use cursive::Cursive; +use cursive::theme::{BaseColor, BorderStyle, Color}; +use cursive::theme::PaletteColor::*; +use cursive::theme::Color::*; +use cursive::theme::BaseColor::*; +use cursive::utils::markup::StyledString; +use cursive::align::{HAlign, VAlign}; +use cursive::event::Key; +use cursive::views::{BoxView, LayerPosition, LinearLayout, Panel, StackView, TextView}; +use cursive::direction::Orientation; +use cursive::traits::*; + +use grin::Server; + +const WELCOME_LOGO: &str = " GGGGG GGGGGGG + GGGGGGG GGGGGGGGG + GGGGGGGGG GGGG GGGGGGGGGG + GGGGGGGGGGG GGGGGGGG GGGGGGGGGGG + GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG + GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG + GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG + GGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGG + GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG + GGGGGGGGGGGGGGG GGGGGGGGGGGGGGGGGGGGGGGGGGGGGG + GGGGGG + GGGGGGG + GGGGGGGG + GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG + GGGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG + GGGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGGG + GGGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGGG + GGGGGGGGGGGG GGGGGGGG GGGGGGGGGGGGG + GGGGGGGGGGG GGGGGGGG GGGGGGGGGGGG + GGGGGGGGGG GGGGGGGG GGGGGGGGGGG + GGGGGGGG GGGGGGGG GGGGGGGGG + GGGGGGG GGGGGGGG GGGGGGG + GGGG GGGGGGGG GGGG + GG GGGGGGGG GG + GGGGGGGG "; + +pub struct UI { + cursive: Cursive, + ui_rx: mpsc::Receiver, + ui_tx: mpsc::Sender, + controller_tx: mpsc::Sender, +} + +pub struct StatusUpdates { + pub basic_status: String, + pub peer_count: String, + pub chain_height: String, + pub basic_mining_config_status: String, + pub basic_mining_status: String, + pub basic_network_info: String, +} + +pub enum UIMessage { + UpdateStatus(StatusUpdates), +} + +impl UI { + /// Create a new UI + pub fn new(controller_tx: mpsc::Sender) -> UI { + let (ui_tx, ui_rx) = mpsc::channel::(); + let mut grin_ui = UI { + cursive: Cursive::new(), + ui_tx: ui_tx, + ui_rx: ui_rx, + controller_tx: controller_tx, + }; + + let mut logo_string = StyledString::new(); + logo_string.append(StyledString::styled( + WELCOME_LOGO, + Color::Dark(BaseColor::Green), + )); + + let mut title_string = StyledString::new(); + title_string.append(StyledString::styled( + "Grin Version 0.0.1", + Color::Dark(BaseColor::Green), + )); + let mut logo_view = TextView::new(logo_string) + .v_align(VAlign::Center) + .h_align(HAlign::Center); + logo_view.set_scrollable(false); + + // Create UI objects, etc + let basic_status_view = BoxView::with_full_screen( + LinearLayout::new(Orientation::Horizontal) + .child(BoxView::with_full_screen(logo_view)) + .child(BoxView::with_full_screen( + LinearLayout::new(Orientation::Vertical) + .child(TextView::new(title_string)) + .child(TextView::new("------------------------")) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("Current Status: ")) + .child(TextView::new("Starting").with_id("basic_current_status")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("Connected Peers: ")) + .child(TextView::new("0").with_id("connected_peers")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("Chain Height: ")) + .child(TextView::new("").with_id("chain_height")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("------------------------")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("").with_id("basic_mining_config_status")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("").with_id("basic_mining_status")), + ) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(TextView::new("").with_id("basic_network_info")), + ), + )), + ).with_id("basic_status_view"); + + let advanced_status_view = BoxView::with_full_screen(TextView::new( + "Advanced Status Display will go here and should contain detailed readouts for: +--Latest Blocks +--Sync Info +--Chain Info +--Peer Info +--Mining Info + ", + )).with_id("advanced_status"); + + let root_stack = StackView::new() + .layer(advanced_status_view) + .layer(basic_status_view) + .with_id("root_stack"); + + /* + let mut basic_button = Button::new("", |s| { + // + }); + basic_button.set_label_raw("1 - Basic Status"); + let mut advanced_button = Button::new("2 - Advanced Status", |s| { + // + }); + //advanced_button.set_label_raw("2 - Advanced Status"); + let mut config_button = Button::new("", |s| { + // + }); + config_button.set_label_raw("3 - Config"); + let mut quit_button = Button::new("", |s| { + //s.quit(); + }); + quit_button.set_label_raw("Quit");*/ + + let top_layer = LinearLayout::new(Orientation::Vertical) + .child(Panel::new(root_stack)) + .child( + LinearLayout::new(Orientation::Horizontal) + .child(Panel::new(TextView::new( + " Toggle Basic / Advanced view", + ))) + .child(Panel::new(TextView::new(" Quit"))), + ); + /*.child( + LinearLayout::new(Orientation::Horizontal) + .child(Panel::new(basic_button)) + .child(Panel::new(advanced_button)) + .child(Panel::new(config_button)) + .child(Panel::new(quit_button)) + );*/ + + grin_ui.cursive.add_global_callback(Key::Tab, |s| { + //let bas_sta = s.find_id::>>("basic_status").unwrap(); + //let mut root_stack = s.find_id::("root_stack").unwrap(); + //root_stack.add_layer(adv_sta); + + s.call_on_id("root_stack", |sv: &mut StackView| { + /*if let FunctionalState::BasicStatus = cur_state { + return; + }*/ + sv.move_to_front(LayerPosition::FromBack(0)); + //sv.add_layer(advanced_status); + /*sv.pop_layer(); + sv.add_layer(bas_sta);*/ }); + }); + + //set theme + let mut theme = grin_ui.cursive.current_theme().clone(); + theme.shadow = false; + theme.borders = BorderStyle::Simple; + theme.palette[Background] = Dark(Black); + theme.palette[Shadow] = Dark(Black); + theme.palette[View] = Dark(Black); + theme.palette[Primary] = Dark(White); + theme.palette[Highlight] = Dark(Cyan); + theme.palette[HighlightInactive] = Dark(Blue); + // also secondary, tertiary, TitlePrimary, TitleSecondary + grin_ui.cursive.set_theme(theme); + + grin_ui.cursive.add_layer(top_layer); + + // Configure a callback (shutdown, for the first test) + let controller_tx_clone = grin_ui.controller_tx.clone(); + grin_ui.cursive.add_global_callback('q', move |_| { + controller_tx_clone + .send(ControllerMessage::Shutdown) + .unwrap(); + }); + grin_ui.cursive.set_fps(4); + grin_ui + } + + /// Step the UI by calling into Cursive's step function, then + /// processing any UI messages + pub fn step(&mut self) -> bool { + if !self.cursive.is_running() { + return false; + } + + // Process any pending UI messages + while let Some(message) = self.ui_rx.try_iter().next() { + match message { + UIMessage::UpdateStatus(update) => { + //find and update here as needed + self.cursive + .call_on_id("basic_current_status", |t: &mut TextView| { + t.set_content(update.basic_status.clone()); + }); + self.cursive + .call_on_id("connected_peers", |t: &mut TextView| { + t.set_content(update.peer_count.clone()); + }); + self.cursive.call_on_id("chain_height", |t: &mut TextView| { + t.set_content(update.chain_height.clone()); + }); + self.cursive + .call_on_id("basic_mining_config_status", |t: &mut TextView| { + t.set_content(update.basic_mining_config_status.clone()); + }); + self.cursive + .call_on_id("basic_mining_status", |t: &mut TextView| { + t.set_content(update.basic_mining_status.clone()); + }); + self.cursive + .call_on_id("basic_network_info", |t: &mut TextView| { + t.set_content(update.basic_network_info.clone()); + }); + } + } + } + + // Step the UI + self.cursive.step(); + + true + } + + /// Stop the UI + pub fn stop(&mut self) { + self.cursive.quit(); + } +} + +pub struct Controller { + rx: mpsc::Receiver, + ui: UI, +} + +pub enum ControllerMessage { + Shutdown, +} + +impl Controller { + /// Create a new controller + pub fn new() -> Result { + let (tx, rx) = mpsc::channel::(); + Ok(Controller { + rx: rx, + ui: UI::new(tx.clone()), + }) + } + /// Run the controller + pub fn run(&mut self, server: Arc) { + let stat_update_interval = 1; + let mut next_stat_update = time::get_time().sec + stat_update_interval; + while self.ui.step() { + while let Some(message) = self.rx.try_iter().next() { + match message { + ControllerMessage::Shutdown => { + server.stop(); + self.ui.stop(); + /*self.ui + .ui_tx + .send(UIMessage::UpdateOutput("update".to_string())) + .unwrap();*/ + } + } + } + if time::get_time().sec > next_stat_update { + self.update_status(server.clone()); + next_stat_update = time::get_time().sec + stat_update_interval; + } + } + } + /// update the UI with server status at given intervals (should be + /// once a second at present + pub fn update_status(&mut self, server: Arc) { + let stats = server.get_server_stats().unwrap(); + let basic_status = { + if stats.is_syncing { + if stats.awaiting_peers { + "Waiting for peers".to_string() + } else { + format!("Syncing - Latest header: {}", stats.header_head.height).to_string() + } + } else { + "Running".to_string() + } + }; + let basic_mining_config_status = { + if stats.mining_stats.is_enabled { + "Configured as mining node" + } else { + "Configured as validating node only (not mining)" + } + }; + let (basic_mining_status, basic_network_info) = { + if stats.mining_stats.is_enabled { + if stats.is_syncing { + ( + "Mining Status: Paused while syncing".to_string(), + "".to_string(), + ) + } else if stats.mining_stats.combined_gps == 0.0 { + ( + "Mining Status: Starting miner and awating first solution...".to_string(), + "".to_string(), + ) + } else { + ( + format!( + "Mining Status: Mining at height {} at {:.*} GPS", + stats.mining_stats.block_height, 4, stats.mining_stats.combined_gps + ), + format!( + "Cuckoo {} - Network Difficulty {}", + stats.mining_stats.cuckoo_size, + stats.mining_stats.network_difficulty.to_string() + ), + ) + } + } else { + ("".to_string(), "".to_string()) + } + }; + let update = StatusUpdates { + basic_status: basic_status, + peer_count: stats.peer_count.to_string(), + chain_height: stats.head.height.to_string(), + basic_mining_config_status: basic_mining_config_status.to_string(), + basic_mining_status: basic_mining_status, + basic_network_info: basic_network_info, + }; + self.ui.ui_tx.send(UIMessage::UpdateStatus(update)).unwrap(); + } +}