feat: TUI logs view (#3064)
* fix: add logs page to TUI * chore: print panic traces to TUI logs * chore: stop and start tui nicely and a bit of refactoring * chore: rustfmt * chore: typo * chore: use sync_channel for logs * chore: don't try to unwrap err on try_send log message * chore: fix compiler/lint warnings * fix: Only create logs channel if TUI is enabled and resovle other small review comments * fix: wrap logs in TUI to fix window size * fix: debug and trace logs appear white in the TUI logs
This commit is contained in:
committed by
Quentin Le Sceller
parent
38e6497919
commit
8ce2bfda58
+35
-27
@@ -27,46 +27,53 @@ use crate::core::global;
|
||||
use crate::p2p::{PeerAddr, Seeding};
|
||||
use crate::servers;
|
||||
use crate::tui::ui;
|
||||
use grin_util::logger::LogEntry;
|
||||
use std::sync::mpsc;
|
||||
|
||||
/// wrap below to allow UI to clean up on stop
|
||||
pub fn start_server(config: servers::ServerConfig) {
|
||||
start_server_tui(config);
|
||||
pub fn start_server(config: servers::ServerConfig, logs_rx: Option<mpsc::Receiver<LogEntry>>) {
|
||||
start_server_tui(config, logs_rx);
|
||||
// Just kill process for now, otherwise the process
|
||||
// hangs around until sigint because the API server
|
||||
// currently has no shutdown facility
|
||||
warn!("Shutting down...");
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
warn!("Shutdown complete.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fn start_server_tui(config: servers::ServerConfig) {
|
||||
fn start_server_tui(config: servers::ServerConfig, logs_rx: Option<mpsc::Receiver<LogEntry>>) {
|
||||
// Run the UI controller.. here for now for simplicity to access
|
||||
// everything it might need
|
||||
if config.run_tui.unwrap_or(false) {
|
||||
warn!("Starting GRIN in UI mode...");
|
||||
servers::Server::start(config, |serv: servers::Server| {
|
||||
let mut controller = ui::Controller::new().unwrap_or_else(|e| {
|
||||
panic!("Error loading UI controller: {}", e);
|
||||
});
|
||||
controller.run(serv);
|
||||
})
|
||||
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);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
warn!("Starting GRIN w/o UI...");
|
||||
servers::Server::start(config, |serv: servers::Server| {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
r.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.expect("Error setting handler for both SIGINT (Ctrl+C) and SIGTERM (kill)");
|
||||
while running.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
warn!("Received SIGINT (Ctrl+C) or SIGTERM (kill).");
|
||||
serv.stop();
|
||||
})
|
||||
servers::Server::start(
|
||||
config,
|
||||
logs_rx,
|
||||
|serv: servers::Server, _: Option<mpsc::Receiver<LogEntry>>| {
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
r.store(false, Ordering::SeqCst);
|
||||
})
|
||||
.expect("Error setting handler for both SIGINT (Ctrl+C) and SIGTERM (kill)");
|
||||
while running.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
warn!("Received SIGINT (Ctrl+C) or SIGTERM (kill).");
|
||||
serv.stop();
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -78,6 +85,7 @@ fn start_server_tui(config: servers::ServerConfig) {
|
||||
pub fn server_command(
|
||||
server_args: Option<&ArgMatches<'_>>,
|
||||
mut global_config: GlobalConfig,
|
||||
logs_rx: Option<mpsc::Receiver<LogEntry>>,
|
||||
) -> i32 {
|
||||
global::set_mining_mode(
|
||||
global_config
|
||||
@@ -123,7 +131,7 @@ pub fn server_command(
|
||||
if let Some(a) = server_args {
|
||||
match a.subcommand() {
|
||||
("run", _) => {
|
||||
start_server(server_config);
|
||||
start_server(server_config, logs_rx);
|
||||
}
|
||||
("", _) => {
|
||||
println!("Subcommand required, use 'grin help server' for details");
|
||||
@@ -137,7 +145,7 @@ pub fn server_command(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
start_server(server_config);
|
||||
start_server(server_config, logs_rx);
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
+25
-21
@@ -30,6 +30,8 @@ use grin_core as core;
|
||||
use grin_p2p as p2p;
|
||||
use grin_servers as servers;
|
||||
use grin_util as util;
|
||||
use grin_util::logger::LogEntry;
|
||||
use std::sync::mpsc;
|
||||
|
||||
mod cmd;
|
||||
pub mod tui;
|
||||
@@ -136,26 +138,28 @@ fn real_main() -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut config) = node_config.clone() {
|
||||
let mut l = config.members.as_mut().unwrap().logging.clone().unwrap();
|
||||
let run_tui = config.members.as_mut().unwrap().server.run_tui;
|
||||
if let Some(true) = run_tui {
|
||||
l.log_to_stdout = false;
|
||||
l.tui_running = Some(true);
|
||||
}
|
||||
init_logger(Some(l));
|
||||
let mut config = node_config.clone().unwrap();
|
||||
let mut logging_config = config.members.as_mut().unwrap().logging.clone().unwrap();
|
||||
logging_config.tui_running = config.members.as_mut().unwrap().server.run_tui;
|
||||
|
||||
global::set_mining_mode(config.members.unwrap().server.clone().chain_type);
|
||||
let (logs_tx, logs_rx) = if logging_config.tui_running.unwrap() {
|
||||
let (logs_tx, logs_rx) = mpsc::sync_channel::<LogEntry>(200);
|
||||
(Some(logs_tx), Some(logs_rx))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
init_logger(Some(logging_config), logs_tx);
|
||||
|
||||
if let Some(file_path) = &config.config_file_path {
|
||||
info!(
|
||||
"Using configuration file at {}",
|
||||
file_path.to_str().unwrap()
|
||||
);
|
||||
} else {
|
||||
info!("Node configuration file not found, using default");
|
||||
}
|
||||
}
|
||||
global::set_mining_mode(config.members.unwrap().server.clone().chain_type);
|
||||
|
||||
if let Some(file_path) = &config.config_file_path {
|
||||
info!(
|
||||
"Using configuration file at {}",
|
||||
file_path.to_str().unwrap()
|
||||
);
|
||||
} else {
|
||||
info!("Node configuration file not found, using default");
|
||||
};
|
||||
|
||||
log_build_info();
|
||||
|
||||
@@ -163,7 +167,7 @@ fn real_main() -> i32 {
|
||||
match args.subcommand() {
|
||||
// server commands and options
|
||||
("server", Some(server_args)) => {
|
||||
cmd::server_command(Some(server_args), node_config.unwrap())
|
||||
cmd::server_command(Some(server_args), node_config.unwrap(), logs_rx)
|
||||
}
|
||||
|
||||
// client commands and options
|
||||
@@ -177,11 +181,11 @@ fn real_main() -> i32 {
|
||||
Ok(_) => 0,
|
||||
Err(_) => 1,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// If nothing is specified, try to just use the config file instead
|
||||
// this could possibly become the way to configure most things
|
||||
// with most command line options being phased out
|
||||
_ => cmd::server_command(None, node_config.unwrap()),
|
||||
_ => cmd::server_command(None, node_config.unwrap(), logs_rx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ pub const SUBMENU_MINING_BUTTON: &str = "mining_submenu_button";
|
||||
pub const TABLE_MINING_STATUS: &str = "mining_status_table";
|
||||
pub const TABLE_MINING_DIFF_STATUS: &str = "mining_diff_status_table";
|
||||
|
||||
// Logs View
|
||||
pub const VIEW_LOGS: &str = "logs_view";
|
||||
|
||||
// Mining View
|
||||
pub const VIEW_VERSION: &str = "version_view";
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
use cursive::theme::{BaseColor, Color, ColorStyle};
|
||||
use cursive::traits::Identifiable;
|
||||
use cursive::view::View;
|
||||
use cursive::views::BoxView;
|
||||
use cursive::{Cursive, Printer};
|
||||
|
||||
use crate::tui::constants::VIEW_LOGS;
|
||||
use cursive::utils::lines::spans::{LinesIterator, Row};
|
||||
use cursive::utils::markup::StyledString;
|
||||
use grin_util::logger::LogEntry;
|
||||
use log::Level;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct TUILogsView;
|
||||
|
||||
impl TUILogsView {
|
||||
pub fn create() -> Box<dyn View> {
|
||||
let logs_view = BoxView::with_full_screen(LogBufferView::new(200).with_id("logs"));
|
||||
Box::new(logs_view.with_id(VIEW_LOGS))
|
||||
}
|
||||
|
||||
pub fn update(c: &mut Cursive, entry: LogEntry) {
|
||||
c.call_on_id("logs", |t: &mut LogBufferView| {
|
||||
t.update(entry);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct LogBufferView {
|
||||
buffer: VecDeque<LogEntry>,
|
||||
}
|
||||
|
||||
impl LogBufferView {
|
||||
fn new(size: usize) -> Self {
|
||||
let mut buffer = VecDeque::new();
|
||||
buffer.resize(
|
||||
size,
|
||||
LogEntry {
|
||||
log: String::new(),
|
||||
level: Level::Info,
|
||||
},
|
||||
);
|
||||
|
||||
LogBufferView { buffer }
|
||||
}
|
||||
|
||||
fn update(&mut self, entry: LogEntry) {
|
||||
self.buffer.push_front(entry);
|
||||
self.buffer.pop_back();
|
||||
}
|
||||
|
||||
fn color(level: Level) -> ColorStyle {
|
||||
match level {
|
||||
Level::Info => ColorStyle::new(
|
||||
Color::Light(BaseColor::Green),
|
||||
Color::Dark(BaseColor::Black),
|
||||
),
|
||||
Level::Warn => ColorStyle::new(
|
||||
Color::Light(BaseColor::Yellow),
|
||||
Color::Dark(BaseColor::Black),
|
||||
),
|
||||
Level::Error => {
|
||||
ColorStyle::new(Color::Light(BaseColor::Red), Color::Dark(BaseColor::Black))
|
||||
}
|
||||
_ => ColorStyle::new(
|
||||
Color::Light(BaseColor::White),
|
||||
Color::Dark(BaseColor::Black),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LogBufferView {
|
||||
fn draw(&self, printer: &Printer) {
|
||||
let mut i = 0;
|
||||
for entry in self.buffer.iter().take(printer.size.y) {
|
||||
printer.with_color(LogBufferView::color(entry.level), |p| {
|
||||
let log_message = StyledString::plain(&entry.log);
|
||||
let mut rows: Vec<Row> = LinesIterator::new(&log_message, printer.size.x).collect();
|
||||
rows.reverse(); // So stack traces are in the right order.
|
||||
for row in rows {
|
||||
for span in row.resolve(&log_message) {
|
||||
p.print((0, p.size.y - 1 - i), span.content);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -25,8 +25,8 @@ use cursive::views::{
|
||||
use cursive::Cursive;
|
||||
|
||||
use crate::tui::constants::{
|
||||
MAIN_MENU, ROOT_STACK, SUBMENU_MINING_BUTTON, VIEW_BASIC_STATUS, VIEW_MINING, VIEW_PEER_SYNC,
|
||||
VIEW_VERSION,
|
||||
MAIN_MENU, ROOT_STACK, SUBMENU_MINING_BUTTON, VIEW_BASIC_STATUS, VIEW_LOGS, VIEW_MINING,
|
||||
VIEW_PEER_SYNC, VIEW_VERSION,
|
||||
};
|
||||
|
||||
pub fn create() -> Box<dyn View> {
|
||||
@@ -38,6 +38,7 @@ pub fn create() -> Box<dyn View> {
|
||||
.get_mut()
|
||||
.add_item("Peers and Sync", VIEW_PEER_SYNC);
|
||||
main_menu.get_mut().add_item("Mining", VIEW_MINING);
|
||||
main_menu.get_mut().add_item("Logs", VIEW_LOGS);
|
||||
main_menu.get_mut().add_item("Version Info", VIEW_VERSION);
|
||||
let change_view = |s: &mut Cursive, v: &&str| {
|
||||
if *v == "" {
|
||||
|
||||
@@ -17,6 +17,7 @@ use chrono;
|
||||
use humansize;
|
||||
//
|
||||
mod constants;
|
||||
mod logs;
|
||||
mod menu;
|
||||
mod mining;
|
||||
mod peers;
|
||||
|
||||
+23
-8
@@ -34,13 +34,15 @@ use crate::built_info;
|
||||
use crate::servers::Server;
|
||||
use crate::tui::constants::ROOT_STACK;
|
||||
use crate::tui::types::{TUIStatusListener, UIMessage};
|
||||
use crate::tui::{menu, mining, peers, status, version};
|
||||
use crate::tui::{logs, menu, mining, peers, status, version};
|
||||
use grin_util::logger::LogEntry;
|
||||
|
||||
pub struct UI {
|
||||
cursive: Cursive,
|
||||
ui_rx: mpsc::Receiver<UIMessage>,
|
||||
ui_tx: mpsc::Sender<UIMessage>,
|
||||
controller_tx: mpsc::Sender<ControllerMessage>,
|
||||
logs_rx: mpsc::Receiver<LogEntry>,
|
||||
}
|
||||
|
||||
fn modify_theme(theme: &mut Theme) {
|
||||
@@ -57,19 +59,25 @@ fn modify_theme(theme: &mut Theme) {
|
||||
|
||||
impl UI {
|
||||
/// Create a new UI
|
||||
pub fn new(controller_tx: mpsc::Sender<ControllerMessage>) -> UI {
|
||||
pub fn new(
|
||||
controller_tx: mpsc::Sender<ControllerMessage>,
|
||||
logs_rx: mpsc::Receiver<LogEntry>,
|
||||
) -> UI {
|
||||
let (ui_tx, ui_rx) = mpsc::channel::<UIMessage>();
|
||||
|
||||
let mut grin_ui = UI {
|
||||
cursive: Cursive::default(),
|
||||
ui_tx: ui_tx,
|
||||
ui_rx: ui_rx,
|
||||
controller_tx: controller_tx,
|
||||
ui_tx,
|
||||
ui_rx,
|
||||
controller_tx,
|
||||
logs_rx,
|
||||
};
|
||||
|
||||
// Create UI objects, etc
|
||||
let status_view = status::TUIStatusView::create();
|
||||
let mining_view = mining::TUIMiningView::create();
|
||||
let peer_view = peers::TUIPeerView::create();
|
||||
let logs_view = logs::TUILogsView::create();
|
||||
let version_view = version::TUIVersionView::create();
|
||||
|
||||
let main_menu = menu::create();
|
||||
@@ -78,6 +86,7 @@ impl UI {
|
||||
.layer(version_view)
|
||||
.layer(mining_view)
|
||||
.layer(peer_view)
|
||||
.layer(logs_view)
|
||||
.layer(status_view)
|
||||
.with_id(ROOT_STACK)
|
||||
.full_height();
|
||||
@@ -128,6 +137,10 @@ impl UI {
|
||||
return false;
|
||||
}
|
||||
|
||||
while let Some(message) = self.logs_rx.try_iter().next() {
|
||||
logs::TUILogsView::update(&mut self.cursive, message);
|
||||
}
|
||||
|
||||
// Process any pending UI messages
|
||||
while let Some(message) = self.ui_rx.try_iter().next() {
|
||||
match message {
|
||||
@@ -162,13 +175,14 @@ pub enum ControllerMessage {
|
||||
|
||||
impl Controller {
|
||||
/// Create a new controller
|
||||
pub fn new() -> Result<Controller, String> {
|
||||
pub fn new(logs_rx: mpsc::Receiver<LogEntry>) -> Result<Controller, String> {
|
||||
let (tx, rx) = mpsc::channel::<ControllerMessage>();
|
||||
Ok(Controller {
|
||||
rx: rx,
|
||||
ui: UI::new(tx),
|
||||
rx,
|
||||
ui: UI::new(tx, logs_rx),
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the controller
|
||||
pub fn run(&mut self, server: Server) {
|
||||
let stat_update_interval = 1;
|
||||
@@ -177,6 +191,7 @@ impl Controller {
|
||||
while let Some(message) = self.rx.try_iter().next() {
|
||||
match message {
|
||||
ControllerMessage::Shutdown => {
|
||||
warn!("Shutdown in progress, please wait");
|
||||
self.ui.stop();
|
||||
server.stop();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user