From e59a1513f3c75c4c6e9b8c6de82bd3b91c2e61ff Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Fri, 26 Jan 2024 02:04:02 +0200 Subject: [PATCH] Monitor free disk space of the node path and show warning below 10GiB --- Cargo.lock | 1 + Cargo.toml | 1 + res/app.css | 8 ++ src/frontend/running.rs | 1 + src/frontend/running/node.rs | 178 +++++++++++++++++++++++++++++------ 5 files changed, 161 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a16285e..707299d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10709,6 +10709,7 @@ dependencies = [ "event-listener-primitives", "file-rotate", "frame-system", + "fs4 0.7.0", "futures", "gtk4", "hex", diff --git a/Cargo.toml b/Cargo.toml index 2551cbc..f55cf4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ duct = "0.13.6" event-listener-primitives = "2.0.1" file-rotate = "0.7.5" frame-system = { git = "https://github.com/subspace/polkadot-sdk", rev = "c63a8b28a9fd26d42116b0dcef1f2a5cefb9cd1c", default-features = false } +fs4 = "0.7.0" futures = "0.3.29" gtk = { version = "0.7.3", package = "gtk4" } hex = "0.4.3" diff --git a/res/app.css b/res/app.css index 29cb062..094a974 100644 --- a/res/app.css +++ b/res/app.css @@ -19,6 +19,14 @@ progressbar progress { color: #ff3800; } +.free-disk-space > trough > block.low { + background-color: #ff3800; +} + +.free-disk-space > trough > block.high { + background-color: #ffA400; +} + farm-sectors > child { padding: 0; } diff --git a/src/frontend/running.rs b/src/frontend/running.rs index faedbb7..7dcd97e 100644 --- a/src/frontend/running.rs +++ b/src/frontend/running.rs @@ -231,6 +231,7 @@ impl RunningView { self.node_view.emit(NodeInput::Initialize { best_block_number, chain_info, + node_path: raw_config.node_path().clone(), }); } RunningInput::NodeNotification(node_notification) => { diff --git a/src/frontend/running/node.rs b/src/frontend/running/node.rs index cc91af1..3b5c7eb 100644 --- a/src/frontend/running/node.rs +++ b/src/frontend/running/node.rs @@ -1,32 +1,47 @@ use crate::backend::node::{ChainInfo, SyncKind, SyncState}; use crate::backend::NodeNotification; +use bytesize::ByteSize; use gtk::prelude::*; +use parking_lot::Mutex; use relm4::prelude::*; +use relm4::{Sender, ShutdownReceiver}; +use relm4_icons::icon_name; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; use subspace_core_primitives::BlockNumber; +use tracing::error; /// Maximum blocks to store in the import queue. // HACK: This constant comes from Substrate's sync, but it is not public in there const MAX_IMPORTING_BLOCKS: BlockNumber = 2048; +/// How frequently to check for free disk space +const FREE_DISK_SPACE_CHECK_INTERVAL: Duration = Duration::from_secs(5); +/// Free disk space below which warning must be shown +const FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD: u64 = 10 * 1024 * 1024 * 1024; #[derive(Debug)] pub enum NodeInput { Initialize { best_block_number: BlockNumber, chain_info: ChainInfo, + node_path: PathBuf, }, NodeNotification(NodeNotification), } -#[derive(Debug, Default)] -struct NodeState { - best_block_number: BlockNumber, - sync_state: SyncState, +#[derive(Debug)] +pub enum NodeCommandOutput { + FreeDiskSpace(ByteSize), } #[derive(Debug)] pub struct NodeView { - node_state: NodeState, + best_block_number: BlockNumber, + sync_state: SyncState, + free_disk_space: Option, chain_name: String, + node_path: Arc>, } #[relm4::component(pub)] @@ -34,7 +49,7 @@ impl Component for NodeView { type Init = (); type Input = NodeInput; type Output = (); - type CommandOutput = (); + type CommandOutput = NodeCommandOutput; view! { #[root] @@ -43,21 +58,60 @@ impl Component for NodeView { set_orientation: gtk::Orientation::Vertical, set_spacing: 10, - gtk::Label { - add_css_class: "heading", - set_halign: gtk::Align::Start, - #[watch] - set_label: &model.chain_name, + gtk::Box { + gtk::Label { + add_css_class: "heading", + set_halign: gtk::Align::Start, + #[watch] + set_label: &model.chain_name, + }, + + gtk::Box { + set_halign: gtk::Align::End, + set_hexpand: true, + + gtk::Box { + set_spacing: 10, + #[watch] + set_tooltip: &format!( + "Free disk space: {} remaining", + model.free_disk_space + .map(|bytes| bytes.to_string_as(true)) + .unwrap_or_default() + ), + #[watch] + set_visible: model.free_disk_space + .map(|bytes| bytes.as_u64() <= FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD) + .unwrap_or_default(), + + gtk::Image { + set_icon_name: Some(icon_name::SSD), + }, + + gtk::LevelBar { + add_css_class: "free-disk-space", + set_min_value: 0.1, + #[watch] + set_value: { + let free_space = model.free_disk_space + .map(|bytes| bytes.as_u64()) + .unwrap_or_default(); + free_space as f64 / FREE_DISK_SPACE_CHECK_WARNING_THRESHOLD as f64 + }, + set_width_request: 100, + }, + }, + }, }, #[transition = "SlideUpDown"] - match model.node_state.sync_state { + match model.sync_state { SyncState::Unknown => gtk::Box { gtk::Label { #[watch] set_label: &format!( "Connecting to the network, best block #{}", - model.node_state.best_block_number + model.best_block_number ), } }, @@ -81,7 +135,7 @@ impl Component for NodeView { format!( "{} #{}/{}{}", kind, - model.node_state.best_block_number, + model.best_block_number, target, speed .map(|speed| format!(", {:.2} blocks/s", speed)) @@ -97,13 +151,13 @@ impl Component for NodeView { gtk::ProgressBar { #[watch] - set_fraction: model.node_state.best_block_number as f64 / target as f64, + set_fraction: model.best_block_number as f64 / target as f64, }, }, SyncState::Idle => gtk::Box { gtk::Label { #[watch] - set_label: &format!("Synced, best block #{}", model.node_state.best_block_number), + set_label: &format!("Synced, best block #{}", model.best_block_number), } }, }, @@ -113,21 +167,38 @@ impl Component for NodeView { fn init( _init: Self::Init, _root: Self::Root, - _sender: ComponentSender, + sender: ComponentSender, ) -> ComponentParts { + let node_path = Arc::>::default(); let model = Self { - node_state: NodeState::default(), + best_block_number: 0, + sync_state: SyncState::default(), + free_disk_space: None, chain_name: String::new(), + node_path: node_path.clone(), }; let widgets = view_output!(); + sender.command(move |sender, shutdown_receiver| async move { + Self::check_free_disk_space(sender, shutdown_receiver, node_path).await; + }); + ComponentParts { model, widgets } } fn update(&mut self, input: Self::Input, _sender: ComponentSender, _root: &Self::Root) { self.process_input(input); } + + fn update_cmd( + &mut self, + input: Self::CommandOutput, + _sender: ComponentSender, + _root: &Self::Root, + ) { + self.process_command(input); + } } impl NodeView { @@ -136,11 +207,9 @@ impl NodeView { NodeInput::Initialize { best_block_number, chain_info, + node_path, } => { - self.node_state = NodeState { - best_block_number, - sync_state: SyncState::default(), - }; + self.best_block_number = best_block_number; self.chain_name = format!( "{} consensus node", chain_info @@ -148,6 +217,7 @@ impl NodeView { .strip_prefix("Subspace ") .unwrap_or(&chain_info.chain_name) ); + *self.node_path.lock() = node_path; } NodeInput::NodeNotification(node_notification) => match node_notification { NodeNotification::SyncStateUpdate(mut sync_state) => { @@ -155,12 +225,12 @@ impl NodeView { target: new_target, .. } = &mut sync_state { - *new_target = (*new_target).max(self.node_state.best_block_number); + *new_target = (*new_target).max(self.best_block_number); // Ensure target is never below current block if let SyncState::Syncing { target: old_target, .. - } = &self.node_state.sync_state + } = &self.sync_state { // If old target was within `MAX_IMPORTING_BLOCKS` from new target, keep old target if old_target @@ -172,16 +242,68 @@ impl NodeView { } } } - self.node_state.sync_state = sync_state; + self.sync_state = sync_state; } NodeNotification::BlockImported(imported_block) => { - self.node_state.best_block_number = imported_block.number; + self.best_block_number = imported_block.number; // Ensure target is never below current block - if let SyncState::Syncing { target, .. } = &mut self.node_state.sync_state { - *target = (*target).max(self.node_state.best_block_number); + if let SyncState::Syncing { target, .. } = &mut self.sync_state { + *target = (*target).max(self.best_block_number); } } }, } } + + fn process_command(&mut self, command_output: NodeCommandOutput) { + match command_output { + NodeCommandOutput::FreeDiskSpace(bytes) => { + self.free_disk_space.replace(bytes); + } + } + } + + async fn check_free_disk_space( + sender: Sender, + shutdown_receiver: ShutdownReceiver, + node_path: Arc>, + ) { + shutdown_receiver + .register(async move { + loop { + let node_path = node_path.lock().clone(); + + if node_path == PathBuf::default() { + tokio::time::sleep(FREE_DISK_SPACE_CHECK_INTERVAL).await; + continue; + } + + match tokio::task::spawn_blocking(move || fs4::available_space(node_path)).await + { + Ok(Ok(free_disk_space)) => { + if sender + .send(NodeCommandOutput::FreeDiskSpace(ByteSize::b( + free_disk_space, + ))) + .is_err() + { + break; + } + } + Ok(Err(error)) => { + error!(%error, "Failed to check free disk space"); + break; + } + Err(error) => { + error!(%error, "Free disk space task panicked"); + break; + } + } + + tokio::time::sleep(FREE_DISK_SPACE_CHECK_INTERVAL).await; + } + }) + .drop_on_shutdown() + .await + } }