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
This commit is contained in:
Yeastplume 2018-03-09 17:16:31 +00:00 committed by GitHub
parent ab4b2a19e3
commit 23ac36a834
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 630 additions and 72 deletions

View file

@ -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" }

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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<RwLock<MiningStats>>,
) -> Option<Proof> {
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<RwLock<MiningStats>>,
) -> Option<Proof> {
// 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<T: MiningWorker>(
&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<RwLock<MiningStats>>,
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(),
);
}
}

View file

@ -45,19 +45,33 @@ pub struct Server {
pub chain: Arc<chain::Chain>,
/// in-memory transaction pool
tx_pool: Arc<RwLock<pool::TransactionPool<PoolToChainAdapter>>>,
/// Whether we're currently syncing
currently_syncing: Arc<AtomicBool>,
/// To be passed around to collect stats and info
state_info: ServerStateInfo,
/// Stop flag
stop: Arc<AtomicBool>,
}
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<F>(config: ServerConfig, mut info_callback: F) -> Result<(), Error>
where
F: FnMut(Arc<Server>),
{
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<ServerStats, Error> {
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,
})
}

View file

@ -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<AtomicBool>,
awaiting_peers: Arc<AtomicBool>,
peers: Arc<p2p::Peers>,
chain: Arc<chain::Chain>,
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:

View file

@ -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<bool>,
/// Whether to run the TUI
/// if enabled, this will disable logging to stdout
pub run_tui: Option<bool>,
}
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<AtomicBool>,
/// Mining stats
pub mining_stats: Arc<RwLock<MiningStats>>,
}
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,
}
}
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -105,7 +105,7 @@ pub fn mine_genesis_block(
let proof_size = global::proofsize();
let mut miner: Box<MiningWorker> = 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)

View file

@ -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() {

View file

@ -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<bool>,
pub miner_async_mode: Option<bool>,
/// plugin dir
pub cuckoo_miner_plugin_dir: Option<String>,
pub miner_plugin_dir: Option<String>,
/// Cuckoo miner plugin configuration, one for each plugin
pub cuckoo_miner_plugin_config: Option<Vec<CuckooMinerPluginConfig>>,
pub miner_plugin_config: Option<Vec<CuckooMinerPluginConfig>>,
/// 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),

View file

@ -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<grin::Server>| {
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);

394
src/bin/ui.rs Normal file
View file

@ -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<UIMessage>,
ui_tx: mpsc::Sender<UIMessage>,
controller_tx: mpsc::Sender<ControllerMessage>,
}
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<ControllerMessage>) -> UI {
let (ui_tx, ui_rx) = mpsc::channel::<UIMessage>();
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(
"<TAB> Toggle Basic / Advanced view",
)))
.child(Panel::new(TextView::new("<Q> 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::<Panel<BoxView<TextView>>>("basic_status").unwrap();
//let mut root_stack = s.find_id::<StackView>("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<ControllerMessage>,
ui: UI,
}
pub enum ControllerMessage {
Shutdown,
}
impl Controller {
/// Create a new controller
pub fn new() -> Result<Controller, String> {
let (tx, rx) = mpsc::channel::<ControllerMessage>();
Ok(Controller {
rx: rx,
ui: UI::new(tx.clone()),
})
}
/// Run the controller
pub fn run(&mut self, server: Arc<Server>) {
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<Server>) {
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();
}
}