From 6d20208c3876e5705c0f31946e5fe4548f92d82b Mon Sep 17 00:00:00 2001 From: dormant-user Date: Wed, 25 Sep 2024 00:00:53 -0500 Subject: [PATCH] Fix incorrect CPU usage metrics Remove dedicated in house module for processor and disks Upgrade `sysinfo` dependency --- Cargo.toml | 2 +- src/resources/disks.rs | 238 ------------------------------------- src/resources/info.rs | 86 ++++++++++++-- src/resources/mod.rs | 4 - src/resources/processor.rs | 98 --------------- src/resources/stream.rs | 11 +- src/routes/monitor.rs | 10 +- 7 files changed, 83 insertions(+), 366 deletions(-) delete mode 100644 src/resources/disks.rs delete mode 100644 src/resources/processor.rs diff --git a/Cargo.toml b/Cargo.toml index 20a0cdc..b59b516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ base64 = "0.22.1" sha2 = "0.10.8" rand = "0.8.5" fernet = "0.2.2" -sysinfo = "0.26.7" +sysinfo = "0.31.4" reqwest = { version = "0.12.7", features = ["blocking", "json"] } minijinja = { version = "2.3.1", features = ["loader"] } url = "2.5.2" diff --git a/src/resources/disks.rs b/src/resources/disks.rs deleted file mode 100644 index b8d6bd8..0000000 --- a/src/resources/disks.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::collections::HashMap; -use std::str; -use regex::Regex; -use serde_json::Value; -use crate::squire; - -/// Function to convert byte size to human-readable format. -/// -/// # Arguments -/// -/// * `byte_size` - The size in bytes to convert -/// -/// # Returns -/// -/// A `String` containing the human-readable format of the byte size -fn size_converter(byte_size: f64) -> String { - let size_name = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - let index = byte_size.log(1024.0).floor() as usize; - format!("{:.2} {}", byte_size / 1024.0_f64.powi(index as i32), size_name[index]) -} - -/// Function to parse size string for Linux. -/// -/// # Arguments -/// -/// * `size_str` - The size string to parse -/// -/// # Returns -/// -/// A `String` containing the parsed size string. -fn parse_size(size_str: &str) -> String { - let re = Regex::new(r"([\d.]+)([KMGTP]?)").unwrap(); - if let Some(caps) = re.captures(size_str.trim()) { - let value: f64 = caps[1].parse().unwrap(); - let unit = &caps[2]; - let unit_multipliers = HashMap::from([ - ("K", 2_f64.powi(10)), - ("M", 2_f64.powi(20)), - ("G", 2_f64.powi(30)), - ("T", 2_f64.powi(40)), - ("P", 2_f64.powi(50)), - ]); - let multiplier = unit_multipliers.get(unit).unwrap_or(&1.0); - return size_converter(value * multiplier); - } - size_str.replace("K", " KB") - .replace("M", " MB") - .replace("G", " GB") - .replace("T", " TB") - .replace("P", " PB") -} - -/// Function to check if a disk is physical/virtual for macOS. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library -/// * `device_id` - The device ID -/// -/// # Returns -/// -/// A `bool` indicating if the disk is physical. -fn is_physical_disk(lib_path: &str, device_id: &str) -> bool { - let result = squire::util::run_command(lib_path, &["info", device_id]); - let output = match result { - Ok(output) => output, - Err(_) => { - log::error!("Failed to get disk info"); - return false; - }, - }; - for line in output.split("\n") { - if line.contains("Virtual:") && line.contains("Yes") { - return false; - } - } - true -} - -/// Function to get disk information on Linux. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get disks' information. -/// -/// # Returns -/// -/// A `Vec` of `HashMap` containing the disk information. -fn linux_disks(lib_path: &str) -> Vec> { - let result = squire::util::run_command(lib_path, &["-o", "NAME,SIZE,TYPE,MODEL", "-d"]); - let output = match result { - Ok(output) => output, - Err(_) => { - log::error!("Failed to get disk info"); - return Vec::new(); - }, - }; - let disks: Vec<&str> = output.lines().collect(); - let filtered_disks: Vec<&str> = disks.into_iter().filter(|&disk| !disk.contains("loop")).collect(); - let keys_raw = filtered_disks[0].to_lowercase() - .replace("name", "DeviceID") - .replace("model", "Name") - .replace("size", "Size"); - let keys: Vec<&str> = keys_raw.split_whitespace().collect(); - let mut disk_info = Vec::new(); - for line in &filtered_disks[1..] { - let values: Vec<&str> = line.split_whitespace().collect(); - let mut disk_map = HashMap::new(); - for (key, value) in keys.iter().zip(values.iter()) { - disk_map.insert(key.to_string(), value.to_string()); - } - disk_map.insert("Size".to_string(), parse_size(disk_map.get("Size").unwrap())); - disk_map.remove("type"); - disk_info.push(disk_map); - } - disk_info -} - -/// Function to get disk information on macOS. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get disks' information. -/// -/// # Returns -/// -/// A `Vec` of `HashMap` containing the disk information. -fn darwin_disks(lib_path: &str) -> Vec> { - let result = squire::util::run_command(lib_path, &["list"]); - let output = match result { - Ok(output) => output, - Err(_) => { - log::error!("Failed to get disk info"); - return Vec::new(); - }, - }; - let disks: Vec<&str> = output.lines().collect(); - let disk_lines: Vec<&str> = disks.into_iter().filter(|&line| line.starts_with("/dev/disk")).collect(); - let mut disk_info = Vec::new(); - for line in disk_lines { - let device_id = line.split_whitespace().next().unwrap(); - if !is_physical_disk(lib_path, device_id) { - continue; - } - let result = squire::util::run_command(lib_path, &["info", device_id]); - let disk_info_output = match result { - Ok(output) => output, - Err(_) => { - log::error!("Failed to get disk info"); - return Vec::new(); - }, - }; - let info_lines: Vec<&str> = disk_info_output.lines().collect(); - let mut disk_data = HashMap::new(); - for info_line in info_lines { - if info_line.contains("Device / Media Name:") { - disk_data.insert("Name".to_string(), info_line.split(":").nth(1).unwrap().trim().to_string()); - } - if info_line.contains("Disk Size:") { - let size_info = info_line.split(":").nth(1).unwrap().split("(").next().unwrap().trim().to_string(); - disk_data.insert("Size".to_string(), size_info); - } - } - disk_data.insert("DeviceID".to_string(), device_id.to_string()); - disk_info.push(disk_data); - } - disk_info -} - -/// Function to reformat disk information on Windows. -/// -/// # Arguments -/// -/// * `data` - A mutable reference to the disk information. -/// -/// # Returns -/// -/// A `HashMap` containing the reformatted disk information. -fn reformat_windows(data: &mut HashMap) -> HashMap { - let size = data.get("Size").unwrap().as_f64().unwrap(); - let model = data.get("Model").unwrap().as_str().unwrap().to_string(); - let mut reformatted_data = HashMap::new(); - reformatted_data.insert("Size".to_string(), size_converter(size)); - reformatted_data.insert("Name".to_string(), model); - reformatted_data.insert("DeviceID".to_string(), data.get("DeviceID").unwrap().as_str().unwrap().to_string()); - reformatted_data -} - -/// Function to get disk information on Windows. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get disks' information. -/// -/// # Returns -/// -/// A `Vec` of `HashMap` containing the disk information. -fn windows_disks(lib_path: &str) -> Vec> { - let ps_command = "Get-CimInstance Win32_DiskDrive | Select-Object Caption, DeviceID, Model, Partitions, Size | ConvertTo-Json"; - let result = squire::util::run_command(lib_path, &["-Command", ps_command]); - let output = match result { - Ok(output) => output, - Err(_) => { - log::error!("Failed to get disk info"); - return Vec::new(); - }, - }; - let disks_info: Value = serde_json::from_str(&output).unwrap(); - let mut disk_info = Vec::new(); - if let Some(disks) = disks_info.as_array() { - for disk in disks { - let mut disk_map: HashMap = serde_json::from_value(disk.clone()).unwrap(); - disk_info.push(reformat_windows(&mut disk_map)); - } - } else { - let mut disk_map: HashMap = serde_json::from_value(disks_info).unwrap(); - disk_info.push(reformat_windows(&mut disk_map)); - } - disk_info -} - -/// Function to get all disks' information. -/// -/// # Returns -/// -/// A `Vec` of `HashMap` containing the disk information. -pub fn get_all_disks() -> Vec> { - let operating_system = std::env::consts::OS; - match operating_system { - "windows" => windows_disks("C:\\Program Files\\PowerShell\\7\\pwsh.exe"), - "macos" => darwin_disks("/usr/sbin/diskutil"), - "linux" => linux_disks("/usr/bin/lsblk"), - _ => { - log::error!("Unsupported operating system"); - Vec::new() - } - } -} diff --git a/src/resources/info.rs b/src/resources/info.rs index 0deadb2..650474d 100644 --- a/src/resources/info.rs +++ b/src/resources/info.rs @@ -1,25 +1,84 @@ use crate::{resources, squire}; use chrono::Utc; use std::collections::HashMap; -use sysinfo::{DiskExt, System, SystemExt}; +use std::collections::HashSet; +use sysinfo::System; +use sysinfo::Disks; /// Function to get total disk usage. /// -/// # Arguments -/// -/// * `system` - A reference to the `System` struct. -/// /// # Returns /// /// A `u64` value containing the total disk usage. -pub fn get_disk_usage(system: &System) -> u64 { +pub fn get_disk_usage() -> u64 { let mut disks_space = vec![]; - for disk in system.disks() { + let disks = Disks::new_with_refreshed_list(); + for disk in disks.list() { disks_space.push(disk.total_space()); } disks_space.iter().sum() } +/// Function to get individual disk specs. +/// +/// # Returns +/// +/// A `vec` of . +pub fn get_disks() -> Vec> { + let mut disks_info = vec![]; + let disks = Disks::new_with_refreshed_list(); + for disk in disks.list() { + // todo: This is a redundant refresh, perhaps reuse `sys` from `get_sys_info` + // todo: Rename this as partitions instead (becuase that's what they are) + // todo: Check sysinfo source code for an easy way to get the `Model` information + // if disk.name().to_string_lossy().starts_with("/boot") { + // continue; + // } + disks_info.push( + HashMap::from([ + ("Name".to_string(), disk.name().to_string_lossy().to_string()), + ("Size".to_string(), squire::util::size_converter(disk.total_space()).to_string()), + ("Model".to_string(), disk.name().to_string_lossy().to_string()), + ("Kind".to_string(), disk.file_system().to_string_lossy().to_string()), + ("mount_point".to_string(), disk.mount_point().to_string_lossy().to_string()), + ]) + ); + } + disks_info +} + +/// Function to get CPU brand names as a comma separated string. +/// +/// # Arguments +/// +/// * `system` - A reference to the `System` struct. +/// +/// # Returns +/// +/// A `String` with CPU brand names. +fn get_cpu_brand(sys: &System) -> String { + let mut cpu_brands: HashSet = HashSet::new(); + let cpus = sys.cpus(); + for cpu in cpus { + cpu_brands.insert(cpu.brand().to_string()); + } + if cpu_brands.is_empty() { + log::error!("Unable to get brand information for all {} CPUs", cpus.len()); + return "Unknown".to_string() + } + let mut cpu_brand_list: Vec = cpu_brands.into_iter().collect(); + cpu_brand_list.sort_by_key(|brand| brand.len()); + match cpu_brand_list.len() { + 0 => String::new(), + 1 => cpu_brand_list[0].clone(), + 2 => format!("{} and {}", cpu_brand_list[0], cpu_brand_list[1]), + _ => { + let last = cpu_brand_list.pop().unwrap(); // Remove and store the last element + format!("{}, and {}", cpu_brand_list.join(", "), last) + } + } +} + /// Function to get system information /// /// This function retrieves system information such as basic system information and memory/storage information. @@ -32,22 +91,23 @@ pub fn get_sys_info() -> HashMap<&'static str, HashMap<&'static str, String>> { sys.refresh_all(); // Uptime - let boot_time = sys.boot_time(); + let boot_time = System::boot_time(); let uptime_duration = Utc::now().timestamp() - boot_time as i64; let uptime = squire::util::convert_seconds(uptime_duration); let total_memory = squire::util::size_converter(sys.total_memory()); // in bytes - let total_storage = squire::util::size_converter(get_disk_usage(&sys)); // in bytes + let total_storage = squire::util::size_converter(get_disk_usage()); // in bytes // Basic and Memory/Storage Info let os_arch = resources::system::os_arch(); let basic = HashMap::from_iter(vec![ - ("node", sys.host_name().unwrap_or("Unknown".to_string())), - ("system", squire::util::capwords(&os_arch.name, None)), + ("hostname", System::host_name().unwrap_or("Unknown".to_string())), + ("operating_system", squire::util::capwords(&os_arch.name, None)), ("architecture", os_arch.architecture), ("uptime", uptime), - ("CPU_cores_raw", sys.cpus().len().to_string() - )]); + ("CPU_cores_raw", sys.cpus().len().to_string()), + ("CPU_brand_raw", get_cpu_brand(&sys)) + ]); let mut hash_vec = vec![ ("memory", total_memory), ("storage", total_storage) diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 6a6c783..974d6c3 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -2,10 +2,6 @@ pub mod info; /// This module contains all the network related functions. pub mod network; -/// This module contains processor related functions. -pub mod processor; -/// This module contains disk related functions. -pub mod disks; /// This module contains system related functions. pub mod system; /// This module contains functions that are responsible to stream information via websockets. diff --git a/src/resources/processor.rs b/src/resources/processor.rs deleted file mode 100644 index 73d64db..0000000 --- a/src/resources/processor.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::{resources, squire}; -use std::fs::File; -use std::io::{self, BufRead}; - -/// Function to get processor information. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get processor information. -/// -/// # Returns -/// -/// A `Option` containing the processor information if successful, otherwise `None`. -fn get_processor_info_darwin(lib_path: &str) -> Result { - let result = squire::util::run_command(lib_path, &["-n", "machdep.cpu.brand_string"]); - if result.is_err() { - return Err("Failed to get processor info"); - } - Ok(result.unwrap()) -} - -/// Function to get processor information on Linux. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get processor information. -/// -/// # Returns -/// -/// A `Option` containing the processor information if successful, otherwise `None`. -fn get_processor_info_linux(lib_path: &str) -> Result { - let file = match File::open(lib_path) { - Ok(file) => file, - Err(_) => return Err("Failed to open file"), - }; - for line in io::BufReader::new(file).lines() { - match line { - Ok(line) => { - if line.contains("model name") { - let parts: Vec<&str> = line.split(':').collect(); - if parts.len() == 2 { - return Ok(parts[1].trim().to_string()); - } - } - } - Err(_) => return Err("Error reading line"), - } - } - Err("Model name not found") -} - -/// Function to get processor information on Windows. -/// -/// # Arguments -/// -/// * `lib_path` - The path to the library used to get processor information. -/// -/// # Returns -/// -/// A `Option` containing the processor information if successful, otherwise `None`. -fn get_processor_info_windows(lib_path: &str) -> Result { - let result = squire::util::run_command(lib_path, &["cpu", "get", "name"]); - let output = match result { - Ok(output) => output, - Err(_) => return Err("Failed to get processor info"), - }; - let lines: Vec<&str> = output.trim().split('\n').collect(); - if lines.len() > 1 { - Ok(lines[1].trim().to_string()) - } else { - Err("Invalid output from command") - } -} - -/// OS-agnostic function to get processor name. -/// -/// # Returns -/// -/// A `Option` containing the processor name if successful, otherwise `None`. -pub fn get_name() -> Option { - let os = resources::system::os_arch().name; - let result = match os.as_str() { - "darwin" => get_processor_info_darwin("/usr/sbin/sysctl"), - "linux" => get_processor_info_linux("/proc/cpuinfo"), - "windows" => get_processor_info_windows("C:\\Windows\\System32\\wbem\\wmic.exe"), - _ => { - log::error!("Unsupported operating system: {}", os); - Err("Unsupported operating system") - } - }; - match result { - Ok(info) => Some(info), - Err(err) => { - log::error!("{}", err); - None - } - } -} diff --git a/src/resources/stream.rs b/src/resources/stream.rs index 583b5ad..31a4cb9 100644 --- a/src/resources/stream.rs +++ b/src/resources/stream.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use sysinfo::{CpuExt, System, SystemExt}; +use sysinfo::{System, RefreshKind, CpuRefreshKind}; use crate::squire; @@ -50,8 +50,11 @@ fn get_docker_stats() -> Result, Box Vec { - let mut system = System::new_all(); - system.refresh_all(); + let mut system = System::new_with_specifics( + RefreshKind::new().with_cpu(CpuRefreshKind::everything()), + ); + std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); + system.refresh_cpu_all(); let mut cpu_usage = Vec::new(); for core in system.cpus() { cpu_usage.push(format!("{:.2}", core.cpu_usage())); @@ -68,7 +71,7 @@ fn get_system_metrics() -> HashMap { let mut system = System::new_all(); system.refresh_all(); - let load_avg = system.load_average(); + let load_avg = System::load_average(); let mem_total = system.total_memory(); // used_memory uses "mem_total - mem_free" but memory is set to available instead of free in macOS diff --git a/src/routes/monitor.rs b/src/routes/monitor.rs index 2bf5325..5d398ec 100644 --- a/src/routes/monitor.rs +++ b/src/routes/monitor.rs @@ -2,7 +2,6 @@ use crate::{constant, resources, routes, squire}; use actix_web::http::StatusCode; use actix_web::{web, HttpRequest, HttpResponse}; use fernet::Fernet; -use std::collections::HashMap; use std::sync::Arc; /// Handles the monitor endpoint and rendering the appropriate HTML page. @@ -36,18 +35,13 @@ pub async fn monitor(request: HttpRequest, log::debug!("Session Validation Response: {}", auth_response.detail); let sys_info_map = resources::info::get_sys_info(); - let sys_info_disks_vec = resources::disks::get_all_disks(); let sys_info_network = resources::network::get_network_info().await; - let mut sys_info_basic: HashMap<&str, String> = sys_info_map.get("basic").unwrap().clone(); + let sys_info_basic = sys_info_map.get("basic").unwrap(); let sys_info_mem_storage = sys_info_map.get("mem_storage").unwrap(); + let sys_info_disks = resources::info::get_disks(); - let sys_info_disks = sys_info_disks_vec; - - if let Some(processor_name) = resources::processor::get_name() { - sys_info_basic.insert("processor", processor_name); - } let rendered = monitor_template.render(minijinja::context!( version => metadata.pkg_version, logout => "/logout",