From 66b9c1f3db809943363118820e9634dfd15a3b90 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 21 Dec 2024 04:15:31 -0800 Subject: [PATCH 1/2] Update ssh.rs --- src/ssh.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/ssh.rs b/src/ssh.rs index f19be6a..f408d47 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -15,6 +15,12 @@ pub struct SSHConnection { sftp: Option, } +#[derive(Debug, Clone)] +pub struct ServerStats { + pub cpu_usage: String, + pub memory_usage: String, + pub disk_usage: String, +} impl SSHConnection { pub fn new(hostname: &str, username: &str, password: &str, port: u16) -> Self { Self { @@ -214,4 +220,67 @@ impl SSHConnection { Err("SFTP subsystem not initialized.".to_string()) } } + + fn run_command(session: &Session, cmd: &str) -> Result { + let mut channel = session + .channel_session() + .map_err(|e| format!("Failed to open channel: {}", e))?; + channel + .exec(cmd) + .map_err(|e| format!("Failed to exec command {}: {}", cmd, e))?; + + let mut stdout = String::new(); + channel + .read_to_string(&mut stdout) + .map_err(|e| format!("Failed to read command output: {}", e))?; + + channel + .wait_close() + .map_err(|e| format!("Failed to close channel: {}", e))?; + + Ok(stdout) + } + + pub fn fetch_stats(&self) -> Result { + let session = self + .session + .as_ref() + .ok_or_else(|| "Session not initialized.".to_string())?; + + let cpu_cmd = r#"top -bn1 | grep "Cpu(s)""#; + let mem_cmd = r#"free -h | grep "Mem:""#; + let disk_cmd = r#"df -h / | tail -1"#; + + let raw_cpu = Self::run_command(session, cpu_cmd)?; + let raw_mem = Self::run_command(session, mem_cmd)?; + let raw_disk = Self::run_command(session, disk_cmd)?; + + Ok(Self::process_stats(&raw_cpu, &raw_mem, &raw_disk)) + } + + fn process_stats(raw_cpu: &str, raw_mem: &str, raw_disk: &str) -> ServerStats { + let cpu_parts: Vec<&str> = raw_cpu.split_whitespace().collect(); + let cpu_usage = format!( + "User: {}%, System: {}%, Idle: {}%, Steal: {}%", + cpu_parts[1], cpu_parts[3], cpu_parts[7], cpu_parts[15] + ); + + let mem_parts: Vec<&str> = raw_mem.split_whitespace().collect(); + let memory_usage = format!( + "Total: {}, Used: {}, Free: {}, Buffers/Cache: {}", + mem_parts[1], mem_parts[2], mem_parts[3], mem_parts[5] + ); + + let disk_parts: Vec<&str> = raw_disk.split_whitespace().collect(); + let disk_usage = format!( + "Filesystem: {}, Total: {}, Used: {}, Available: {}, Usage: {}", + disk_parts[0], disk_parts[1], disk_parts[2], disk_parts[3], disk_parts[4] + ); + + ServerStats { + cpu_usage, + memory_usage, + disk_usage, + } + } } From d1810b0c524170b76effb2677bfccba2a69cbbf1 Mon Sep 17 00:00:00 2001 From: 0xb-s <145866191+0xb-s@users.noreply.github.com> Date: Sat, 21 Dec 2024 04:16:00 -0800 Subject: [PATCH 2/2] Update ui.rs --- src/ui.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index 9028454..496cd80 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,6 @@ use crate::{ localization::{Language, Localizer}, - ssh::SSHConnection, + ssh::{SSHConnection, ServerStats}, }; use eframe::egui; use serde::{Deserialize, Serialize}; @@ -67,6 +67,7 @@ enum Task { WriteFile(String, String), /// Disconnect the active connection Disconnect, + FetchStats, } /// Represents the result of executing a Task. @@ -95,6 +96,7 @@ enum TaskResult { WriteFileResult(Result<(), String>), /// The result of disconnecting DisconnectResult, + FetchStatsResult(Result), } /// BackgroundWorker handles asynchronous tasks to avoid blocking the UI. @@ -239,6 +241,16 @@ impl BackgroundWorker { } let _ = result_sender.send(TaskResult::DisconnectResult); } + + Task::FetchStats => { + if let Some(conn) = connection.as_ref() { + let result = conn.fetch_stats(); + let _ = result_sender.send(TaskResult::FetchStatsResult(result)); + } else { + let _ = result_sender + .send(TaskResult::FetchStatsResult(Err("Not connected".into()))); + } + } } } }); @@ -299,6 +311,7 @@ pub struct UIState { pub language: Language, /// The localizer that holds translations pub localizer: Localizer, + pub server_stats: Option, } impl Default for UIState { @@ -325,6 +338,7 @@ impl Default for UIState { language: Language::English, localizer: Localizer::new(), + server_stats: None, } } } @@ -460,6 +474,21 @@ pub fn render_ui(ui: &mut egui::Ui, state: &mut UIState, _connection: &mut Optio ui.colored_label(egui::Color32::RED, error); } } else { + ui.collapsing("Dashboard", |ui| { + if ui.button("Refresh Stats").clicked() { + state.operation_in_progress = true; + let worker = state.worker.clone(); + worker.lock().unwrap().send_task(Task::FetchStats); + } + + if let Some(stats) = &state.server_stats { + ui.label(format!("CPU Usage:\n {}", stats.cpu_usage)); + ui.label(format!("Memory Usage:\n {}", stats.memory_usage)); + ui.label(format!("Disk Usage:\n {}", stats.disk_usage)); + } else { + ui.label("No stats available. Click 'Refresh Stats' to fetch."); + } + }); ui.heading(state.localizer.t(state.language, "ssh_file_manager")); ui.horizontal(|ui| { @@ -852,6 +881,16 @@ fn poll_worker(state: &mut UIState) { state.current_path = "/".to_string(); state.error_message = Some("Disconnected".to_string()); } + TaskResult::FetchStatsResult(res) => match res { + Ok(stats) => { + state.server_stats = Some(stats); + state.error_message = None; + } + Err(e) => { + state.error_message = Some(e); + state.server_stats = None; + } + }, } } }