tui: show server initialization status and error (#3836)

* tui: show server initialization status and error

* fix: compilation issues

* fix: add documenting to status, remove unused imports

* fix: do not empty server value

* fix: server ref

* tui: do not quit on q when another dialog is showing (progress or error)

* fix: server stop on tui shutdown

* fix: stop server if tui was stopped after start

* server: panic on error at non-tui mode like before with unwrap

* fix: pop dialog

* fix: do not return result on tx after server start

* tui: close current dialog before quit

* tui: pass stop state to server creation after tui quit

* tui: exit code 1 after error, also for non-tui

* tui: better exit code
This commit is contained in:
ardocrat
2026-06-01 14:47:29 +03:00
committed by GitHub
parent 7332c742d6
commit 151e74c860
4 changed files with 174 additions and 65 deletions
+15 -1
View File
@@ -19,7 +19,6 @@ use std::sync::Arc;
use chrono::prelude::Utc;
use rand::prelude::*;
use crate::api;
use crate::chain;
use crate::core::global::{ChainTypes, DEFAULT_FUTURE_TIME_LIMIT};
use crate::core::{core, libtx, pow};
@@ -28,6 +27,7 @@ use crate::p2p;
use crate::pool;
use crate::pool::types::DandelionConfig;
use crate::store;
use crate::{api, Server};
/// Error type wrapping underlying module errors.
#[derive(Debug)]
@@ -405,3 +405,17 @@ impl DandelionEpoch {
self.relay_peer.clone()
}
}
/// Server initialization status.
pub enum ServerInitStatus {
/// Database loading.
LoadDatabase,
/// P2P server initialization.
StartSync,
/// API server initialization.
StartAPI,
/// Server instance after successful initialization.
FinishedLoading(Server),
/// Error on initialization.
ErrorLoading(Error),
}
+19 -12
View File
@@ -39,7 +39,7 @@ use crate::common::hooks::{init_chain_hooks, init_net_hooks};
use crate::common::stats::{
ChainStats, DiffBlock, DiffStats, PeerStats, ServerStateInfo, ServerStats, TxStats,
};
use crate::common::types::{Error, ServerConfig, StratumServerConfig};
use crate::common::types::{Error, ServerConfig, ServerInitStatus, StratumServerConfig};
use crate::core::core::hash::{Hashed, ZERO_HASH};
use crate::core::ser::ProtocolVersion;
use crate::core::{consensus, genesis, global, pow};
@@ -52,7 +52,6 @@ use crate::pool;
use crate::util::file::get_first_line;
use crate::util::{RwLock, StopState};
use futures::channel::oneshot;
use grin_util::logger::LogEntry;
/// Arcified thread-safe TransactionPool with type parameters used by server components
pub type ServerTxPool = Arc<RwLock<pool::TransactionPool<PoolToChainAdapter, PoolToNetAdapter>>>;
@@ -84,20 +83,16 @@ impl Server {
/// 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>(
pub fn start(
config: ServerConfig,
logs_rx: Option<mpsc::Receiver<LogEntry>>,
mut info_callback: F,
stop_state: Option<Arc<StopState>>,
server_tx: Option<mpsc::Sender<ServerInitStatus>>,
api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>),
) -> Result<(), Error>
where
F: FnMut(Server, Option<mpsc::Receiver<LogEntry>>),
{
) -> Result<Server, Error> {
let mining_config = config.stratum_mining_config.clone();
let enable_test_miner = config.run_test_miner;
let test_miner_wallet_url = config.test_miner_wallet_url.clone();
let serv = Server::new(config, stop_state, api_chan)?;
let serv = Server::new(config, stop_state, server_tx, api_chan)?;
if let Some(c) = mining_config {
let enable_stratum_server = c.enable_stratum_server;
@@ -118,8 +113,7 @@ impl Server {
}
}
info_callback(serv, logs_rx);
Ok(())
Ok(serv)
}
// Exclusive (advisory) lock_file to ensure we do not run multiple
@@ -151,6 +145,7 @@ impl Server {
pub fn new(
config: ServerConfig,
stop_state: Option<Arc<StopState>>,
server_tx: Option<mpsc::Sender<ServerInitStatus>>,
api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>),
) -> Result<Server, Error> {
// Obtain our lock_file or fail immediately with an error.
@@ -193,6 +188,10 @@ impl Server {
info!("Starting server, genesis block: {}", genesis.hash());
if let Some(ref server_tx) = server_tx {
let _ = server_tx.send(ServerInitStatus::LoadDatabase);
}
let shared_chain = Arc::new(chain::Chain::init(
config.db_root.clone(),
chain_adapter.clone(),
@@ -220,6 +219,10 @@ impl Server {
};
debug!("Capabilities: {:?}", capabilities);
if let Some(ref server_tx) = server_tx {
let _ = server_tx.send(ServerInitStatus::StartSync);
}
let p2p_server = Arc::new(p2p::Server::new(
&config.db_root,
capabilities,
@@ -265,6 +268,10 @@ impl Server {
}
})?;
if let Some(ref server_tx) = server_tx {
let _ = server_tx.send(ServerInitStatus::StartAPI);
}
info!("Starting rest apis at: {}", &config.api_http_addr);
let api_secret = get_first_line(config.api_secret_path.clone());
let foreign_api_secret = get_first_line(config.foreign_api_secret_path.clone());
+45 -28
View File
@@ -28,7 +28,10 @@ use crate::tui::ui;
use futures::channel::oneshot;
use grin_p2p::msg::PeerAddrs;
use grin_p2p::PeerAddr;
use grin_servers::common::types::ServerInitStatus;
use grin_servers::Server;
use grin_util::logger::LogEntry;
use grin_util::StopState;
use std::sync::mpsc;
/// wrap below to allow UI to clean up on stop
@@ -37,38 +40,50 @@ pub fn start_server(
logs_rx: Option<mpsc::Receiver<LogEntry>>,
api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>),
) {
start_server_tui(config, logs_rx, api_chan);
exit(0);
exit(start_server_tui(config, logs_rx, api_chan));
}
fn start_server_tui(
config: servers::ServerConfig,
logs_rx: Option<mpsc::Receiver<LogEntry>>,
api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>),
) {
// Run the UI controller.. here for now for simplicity to access
// everything it might need
) -> i32 {
if config.run_tui.unwrap_or(false) {
warn!("Starting GRIN in UI mode...");
servers::Server::start(
config,
logs_rx,
|serv: servers::Server, logs_rx: Option<mpsc::Receiver<LogEntry>>| {
let mut controller = ui::Controller::new(logs_rx.unwrap()).unwrap_or_else(|e| {
panic!("Error loading UI controller: {}", e);
});
controller.run(serv);
},
None,
api_chan,
)
.unwrap();
// Run the UI controller.
let (serv_tx, serv_rx) = mpsc::channel::<ServerInitStatus>();
let mut controller = ui::Controller::new(logs_rx, serv_rx).unwrap_or_else(|e| {
panic!("Error loading UI controller: {}", e);
});
let serv_tx_clone = serv_tx.clone();
let stop_state = Arc::new(StopState::new());
let stop_state_clone = stop_state.clone();
thread::spawn(move || {
match Server::start(
config,
Some(stop_state_clone.clone()),
Some(serv_tx_clone.clone()),
api_chan,
) {
Ok(s) => {
if stop_state_clone.is_stopped() {
s.stop();
return;
}
let _ = serv_tx_clone.send(ServerInitStatus::FinishedLoading(s));
}
Err(e) => {
let _ = serv_tx_clone.send(ServerInitStatus::ErrorLoading(e));
}
}
});
let exit_code = controller.run();
stop_state.stop();
exit_code
} else {
warn!("Starting GRIN w/o UI...");
servers::Server::start(
config,
logs_rx,
|serv: servers::Server, _: Option<mpsc::Receiver<LogEntry>>| {
match Server::start(config, None, None, api_chan) {
Ok(s) => {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
@@ -79,12 +94,14 @@ fn start_server_tui(
thread::sleep(Duration::from_secs(1));
}
warn!("Received SIGINT (Ctrl+C) or SIGTERM (kill).");
serv.stop();
},
None,
api_chan,
)
.unwrap();
s.stop();
0
}
Err(e) => {
error!("Error starting GRIN: {:?}", e);
1
}
}
}
}
+95 -24
View File
@@ -15,6 +15,12 @@
//! Basic TUI to better output the overall system status and status
//! of various subsystems
use super::constants::MAIN_MENU;
use crate::built_info;
use crate::servers::Server;
use crate::tui::constants::{ROOT_STACK, VIEW_BASIC_STATUS, VIEW_MINING, VIEW_PEER_SYNC};
use crate::tui::types::{TUIStatusListener, UIMessage};
use crate::tui::{logs, menu, mining, peers, status, version};
use chrono::prelude::Utc;
use cursive::direction::Orientation;
use cursive::theme::BaseColor::{Black, Blue, Cyan, White};
@@ -29,24 +35,20 @@ use cursive::views::{
CircularFocus, Dialog, LinearLayout, Panel, SelectView, StackView, TextView, ViewRef,
};
use cursive::{CursiveRunnable, CursiveRunner};
use std::sync::mpsc;
use std::{thread, time};
use super::constants::MAIN_MENU;
use crate::built_info;
use crate::servers::Server;
use crate::tui::constants::{ROOT_STACK, VIEW_BASIC_STATUS, VIEW_MINING, VIEW_PEER_SYNC};
use crate::tui::types::{TUIStatusListener, UIMessage};
use crate::tui::{logs, menu, mining, peers, status, version};
use grin_core::global;
use grin_servers::common::types::{Error, ServerInitStatus};
use grin_util::logger::LogEntry;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc};
use std::{thread, time};
pub struct UI {
cursive: CursiveRunner<CursiveRunnable>,
ui_rx: mpsc::Receiver<UIMessage>,
ui_tx: mpsc::Sender<UIMessage>,
controller_tx: mpsc::Sender<ControllerMessage>,
logs_rx: mpsc::Receiver<LogEntry>,
logs_rx: Option<mpsc::Receiver<LogEntry>>,
show_dialog: Arc<AtomicBool>,
}
fn modify_theme(theme: &mut Theme) {
@@ -65,7 +67,7 @@ impl UI {
/// Create a new UI
pub fn new(
controller_tx: mpsc::Sender<ControllerMessage>,
logs_rx: mpsc::Receiver<LogEntry>,
logs_rx: Option<mpsc::Receiver<LogEntry>>,
) -> UI {
let (ui_tx, ui_rx) = mpsc::channel::<UIMessage>();
@@ -75,6 +77,7 @@ impl UI {
ui_rx,
controller_tx,
logs_rx,
show_dialog: Arc::new(AtomicBool::new(false)),
};
// Create UI objects, etc
@@ -102,7 +105,7 @@ impl UI {
built_info::PKG_VERSION,
global::get_chain_type()
),
Color::Dark(BaseColor::Green),
Dark(BaseColor::Green),
));
let main_layer = LinearLayout::new(Orientation::Vertical)
@@ -117,17 +120,21 @@ impl UI {
let mut theme = grin_ui.cursive.current_theme().clone();
modify_theme(&mut theme);
grin_ui.cursive.set_theme(theme);
grin_ui.cursive.add_fullscreen_layer(main_layer);
// Configure a callback (shutdown, for the first test)
let controller_tx_clone = grin_ui.controller_tx.clone();
let show_dialog_clone = grin_ui.show_dialog.clone();
grin_ui.cursive.add_global_callback('q', move |c| {
if show_dialog_clone.load(Ordering::Relaxed) {
c.pop_layer();
}
let content = StyledString::styled("Shutting down...", Color::Light(BaseColor::Yellow));
c.add_layer(CircularFocus::new(Dialog::around(TextView::new(content))).wrap_tab());
controller_tx_clone
.send(ControllerMessage::Shutdown)
.unwrap();
let _ = controller_tx_clone.send(ControllerMessage::Shutdown);
});
grin_ui.cursive.set_fps(3);
grin_ui
}
@@ -139,8 +146,10 @@ impl UI {
return false;
}
while let Some(message) = self.logs_rx.try_iter().next() {
logs::TUILogsView::update(&mut self.cursive, message);
if let Some(logs_rx) = &self.logs_rx {
while let Some(message) = logs_rx.try_iter().next() {
logs::TUILogsView::update(&mut self.cursive, message);
}
}
// Process any pending UI messages
@@ -174,6 +183,8 @@ impl UI {
pub struct Controller {
rx: mpsc::Receiver<ControllerMessage>,
ui: UI,
serv_rx: mpsc::Receiver<ServerInitStatus>,
server: Option<Server>,
}
pub enum ControllerMessage {
@@ -182,39 +193,99 @@ pub enum ControllerMessage {
impl Controller {
/// Create a new controller
pub fn new(logs_rx: mpsc::Receiver<LogEntry>) -> Result<Controller, String> {
pub fn new(
logs_rx: Option<mpsc::Receiver<LogEntry>>,
serv_rx: mpsc::Receiver<ServerInitStatus>,
) -> Result<Controller, String> {
let (tx, rx) = mpsc::channel::<ControllerMessage>();
Ok(Controller {
rx,
ui: UI::new(tx, logs_rx),
serv_rx,
server: None,
})
}
/// Server initialization status.
pub fn init_status(&mut self, text: &str, pop: bool) {
if pop {
self.ui.cursive.pop_layer();
}
let content = StyledString::styled(text, Color::Light(BaseColor::Green));
self.ui
.cursive
.add_layer(CircularFocus::new(Dialog::around(TextView::new(content))).wrap_tab());
self.ui.show_dialog.store(true, Ordering::Relaxed);
}
/// Server initialization error.
pub fn init_error(&mut self, e: Error) {
let content = StyledString::styled(format!("{:?}", e), Color::Light(BaseColor::Red));
self.ui.cursive.add_layer(
CircularFocus::new(Dialog::around(TextView::new(content)).button("Exit", |s| {
s.quit();
}))
.wrap_tab(),
);
self.ui.show_dialog.store(true, Ordering::Relaxed);
}
/// Server UI after initialization.
pub fn server(&mut self, server: &Server) {
if let Ok(stats) = server.get_server_stats() {
self.ui.ui_tx.send(UIMessage::UpdateStatus(stats)).unwrap();
}
}
/// Run the controller
pub fn run(&mut self, server: Server) {
pub fn run(&mut self) -> i32 {
self.init_status("Starting server...", false);
let stat_update_interval = 1;
let mut next_stat_update = Utc::now().timestamp() + stat_update_interval;
let delay = time::Duration::from_millis(50);
let mut exit_code = 0;
while self.ui.step() {
if let Some(message) = self.rx.try_iter().next() {
match message {
ControllerMessage::Shutdown => {
warn!("Shutdown in progress, please wait");
self.ui.stop();
server.stop();
return;
if let Some(s) = self.server.take() {
s.stop();
}
return exit_code;
}
}
}
if let Some(m) = self.serv_rx.try_iter().next() {
match m {
ServerInitStatus::LoadDatabase => self.init_status("Loading database...", true),
ServerInitStatus::StartSync => self.init_status("Start syncing...", true),
ServerInitStatus::StartAPI => self.init_status("Starting API...", true),
ServerInitStatus::FinishedLoading(s) => {
self.ui.cursive.pop_layer();
self.ui.show_dialog.store(false, Ordering::Relaxed);
self.server = Some(s)
}
ServerInitStatus::ErrorLoading(e) => {
exit_code = 1;
self.init_error(e);
}
}
}
if Utc::now().timestamp() > next_stat_update {
next_stat_update = Utc::now().timestamp() + stat_update_interval;
if let Ok(stats) = server.get_server_stats() {
self.ui.ui_tx.send(UIMessage::UpdateStatus(stats)).unwrap();
if let Some(server) = &self.server {
if let Ok(stats) = server.get_server_stats() {
self.ui.ui_tx.send(UIMessage::UpdateStatus(stats)).unwrap();
}
}
}
thread::sleep(delay);
}
server.stop();
exit_code
}
}