Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ pub struct SSHConnection {
sftp: Option<Sftp>,
}

#[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 {
Expand Down Expand Up @@ -214,4 +220,67 @@ impl SSHConnection {
Err("SFTP subsystem not initialized.".to_string())
}
}

fn run_command(session: &Session, cmd: &str) -> Result<String, String> {
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<ServerStats, String> {
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,
}
}
}
41 changes: 40 additions & 1 deletion src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
localization::{Language, Localizer},
ssh::SSHConnection,
ssh::{SSHConnection, ServerStats},
};
use eframe::egui;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -67,6 +67,7 @@ enum Task {
WriteFile(String, String),
/// Disconnect the active connection
Disconnect,
FetchStats,
}

/// Represents the result of executing a Task.
Expand Down Expand Up @@ -95,6 +96,7 @@ enum TaskResult {
WriteFileResult(Result<(), String>),
/// The result of disconnecting
DisconnectResult,
FetchStatsResult(Result<ServerStats, String>),
}

/// BackgroundWorker handles asynchronous tasks to avoid blocking the UI.
Expand Down Expand Up @@ -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())));
}
}
}
}
});
Expand Down Expand Up @@ -299,6 +311,7 @@ pub struct UIState {
pub language: Language,
/// The localizer that holds translations
pub localizer: Localizer,
pub server_stats: Option<ServerStats>,
}

impl Default for UIState {
Expand All @@ -325,6 +338,7 @@ impl Default for UIState {
language: Language::English,

localizer: Localizer::new(),
server_stats: None,
}
}
}
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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;
}
},
}
}
}
Loading