diff --git a/Cargo.lock b/Cargo.lock index 178c42a4..cd794dfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,7 @@ dependencies = [ "battery", "beef", "cargo-husky", + "cfg-if 1.0.0", "chrono", "clap", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 11e26521..13947761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ chrono = "0.4.19" crossterm = "0.18.2" ctrlc = { version = "3.1.9", features = ["termination"] } clap = "2.33" +cfg-if = "1.0" dirs-next = "2.0.0" futures = "0.3.14" futures-timer = "3.0.2" diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index c58e4f1b..73830842 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -16,9 +16,8 @@ use once_cell::sync::Lazy; use std::{time::Instant, vec::Vec}; -use crate::app::data_harvester::load_avg::LoadAvgHarvest; use crate::{ - data_harvester::{batteries, cpu, disks, load_avg, mem, network, processes, temperature, Data}, + data_harvester::{batteries, cpu, disks, memory, network, processes, temperature, Data}, utils::gen_util::{get_decimal_bytes, GIGA_LIMIT}, }; use regex::Regex; @@ -51,10 +50,10 @@ pub struct DataCollection { pub frozen_instant: Option, pub timed_data_vec: Vec<(Instant, TimedData)>, pub network_harvest: network::NetworkHarvest, - pub memory_harvest: mem::MemHarvest, - pub swap_harvest: mem::MemHarvest, + pub memory_harvest: memory::MemHarvest, + pub swap_harvest: memory::MemHarvest, pub cpu_harvest: cpu::CpuHarvest, - pub load_avg_harvest: load_avg::LoadAvgHarvest, + pub load_avg_harvest: cpu::LoadAvgHarvest, pub process_harvest: Vec, pub disk_harvest: Vec, pub io_harvest: disks::IoHarvest, @@ -71,10 +70,10 @@ impl Default for DataCollection { frozen_instant: None, timed_data_vec: Vec::default(), network_harvest: network::NetworkHarvest::default(), - memory_harvest: mem::MemHarvest::default(), - swap_harvest: mem::MemHarvest::default(), + memory_harvest: memory::MemHarvest::default(), + swap_harvest: memory::MemHarvest::default(), cpu_harvest: cpu::CpuHarvest::default(), - load_avg_harvest: load_avg::LoadAvgHarvest::default(), + load_avg_harvest: cpu::LoadAvgHarvest::default(), process_harvest: Vec::default(), disk_harvest: Vec::default(), io_harvest: disks::IoHarvest::default(), @@ -90,8 +89,8 @@ impl DataCollection { pub fn reset(&mut self) { self.timed_data_vec = Vec::default(); self.network_harvest = network::NetworkHarvest::default(); - self.memory_harvest = mem::MemHarvest::default(); - self.swap_harvest = mem::MemHarvest::default(); + self.memory_harvest = memory::MemHarvest::default(); + self.swap_harvest = memory::MemHarvest::default(); self.cpu_harvest = cpu::CpuHarvest::default(); self.process_harvest = Vec::default(); self.disk_harvest = Vec::default(); @@ -180,7 +179,7 @@ impl DataCollection { } fn eat_memory_and_swap( - &mut self, memory: mem::MemHarvest, swap: mem::MemHarvest, new_entry: &mut TimedData, + &mut self, memory: memory::MemHarvest, swap: memory::MemHarvest, new_entry: &mut TimedData, ) { // trace!("Eating mem and swap."); // Memory @@ -230,7 +229,7 @@ impl DataCollection { self.cpu_harvest = cpu.to_vec(); } - fn eat_load_avg(&mut self, load_avg: LoadAvgHarvest, new_entry: &mut TimedData) { + fn eat_load_avg(&mut self, load_avg: cpu::LoadAvgHarvest, new_entry: &mut TimedData) { new_entry.load_avg_data = load_avg; self.load_avg_harvest = load_avg; diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 60ff5a2a..feab6005 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -19,8 +19,7 @@ use super::DataFilters; pub mod batteries; pub mod cpu; pub mod disks; -pub mod load_avg; -pub mod mem; +pub mod memory; pub mod network; pub mod processes; pub mod temperature; @@ -29,9 +28,9 @@ pub mod temperature; pub struct Data { pub last_collection_time: Instant, pub cpu: Option, - pub load_avg: Option, - pub memory: Option, - pub swap: Option, + pub load_avg: Option, + pub memory: Option, + pub swap: Option, pub temperature_sensors: Option>, pub network: Option, pub list_of_processes: Option>, @@ -232,7 +231,7 @@ impl DataCollector { #[cfg(target_family = "unix")] { // Load Average - if let Ok(load_avg_data) = load_avg::get_load_avg().await { + if let Ok(load_avg_data) = cpu::get_load_avg().await { self.data.load_avg = Some(load_avg_data); } } @@ -299,7 +298,7 @@ impl DataCollector { ) } }; - let mem_data_fut = mem::get_mem_data(self.widgets_to_harvest.use_mem); + let mem_data_fut = memory::get_mem_data(self.widgets_to_harvest.use_mem); let disk_data_fut = disks::get_disk_usage( self.widgets_to_harvest.use_disk, &self.filters.disk_filter, diff --git a/src/app/data_harvester/batteries.rs b/src/app/data_harvester/batteries/battery.rs similarity index 85% rename from src/app/data_harvester/batteries.rs rename to src/app/data_harvester/batteries/battery.rs index 98cf6ae6..7e4644e9 100644 --- a/src/app/data_harvester/batteries.rs +++ b/src/app/data_harvester/batteries/battery.rs @@ -1,3 +1,14 @@ +//! Uses the battery crate from svartalf. +//! Covers battery usage for: +//! - Linux 2.6.39+ +//! - MacOS 10.10+ +//! - iOS +//! - Windows 7+ +//! - FreeBSD +//! - DragonFlyBSD +//! +//! For more information, see https://github.com/svartalf/rust-battery + use battery::{ units::{power::watt, ratio::percent, time::second}, Battery, Manager, diff --git a/src/app/data_harvester/batteries/mod.rs b/src/app/data_harvester/batteries/mod.rs new file mode 100644 index 00000000..8c0e4a92 --- /dev/null +++ b/src/app/data_harvester/batteries/mod.rs @@ -0,0 +1,10 @@ +//! Data collection for batteries. +//! +//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by the battery crate. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] { + pub mod battery; + pub use self::battery::*; + } +} diff --git a/src/app/data_harvester/cpu/heim/linux.rs b/src/app/data_harvester/cpu/heim/linux.rs new file mode 100644 index 00000000..542685d4 --- /dev/null +++ b/src/app/data_harvester/cpu/heim/linux.rs @@ -0,0 +1,16 @@ +//! Linux-specific functions regarding CPU usage. + +use heim::cpu::os::linux::CpuTimeExt; +pub fn convert_cpu_times(cpu_time: &heim::cpu::CpuTime) -> (f64, f64) { + let working_time: f64 = (cpu_time.user() + + cpu_time.nice() + + cpu_time.system() + + cpu_time.irq() + + cpu_time.soft_irq() + + cpu_time.steal()) + .get::(); + ( + working_time, + working_time + (cpu_time.idle() + cpu_time.io_wait()).get::(), + ) +} diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu/heim/mod.rs similarity index 83% rename from src/app/data_harvester/cpu.rs rename to src/app/data_harvester/cpu/heim/mod.rs index 61e79e28..73a97b5b 100644 --- a/src/app/data_harvester/cpu.rs +++ b/src/app/data_harvester/cpu/heim/mod.rs @@ -1,3 +1,23 @@ +//! CPU stats through heim. +//! Supports macOS, Linux, and Windows. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use linux::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod windows_macos; + pub use windows_macos::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(target_family = "unix")] { + pub mod unix; + pub use unix::*; + } +} + #[derive(Default, Debug, Clone)] pub struct CpuData { pub cpu_prefix: String, @@ -10,42 +30,13 @@ pub type CpuHarvest = Vec; pub type PastCpuWork = f64; pub type PastCpuTotal = f64; +use futures::StreamExt; +use std::collections::VecDeque; + pub async fn get_cpu_data_list( show_average_cpu: bool, previous_cpu_times: &mut Vec<(PastCpuWork, PastCpuTotal)>, previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>, ) -> crate::error::Result { - use futures::StreamExt; - #[cfg(target_os = "linux")] - use heim::cpu::os::linux::CpuTimeExt; - use std::collections::VecDeque; - - fn convert_cpu_times(cpu_time: &heim::cpu::CpuTime) -> (f64, f64) { - #[cfg(not(target_os = "linux"))] - { - let working_time: f64 = - (cpu_time.user() + cpu_time.system()).get::(); - ( - working_time, - working_time + cpu_time.idle().get::(), - ) - } - #[cfg(target_os = "linux")] - { - let working_time: f64 = (cpu_time.user() - + cpu_time.nice() - + cpu_time.system() - + cpu_time.irq() - + cpu_time.soft_irq() - + cpu_time.steal()) - .get::(); - ( - working_time, - working_time - + (cpu_time.idle() + cpu_time.io_wait()).get::(), - ) - } - } - fn calculate_cpu_usage_percentage( (previous_working_time, previous_total_time): (f64, f64), (current_working_time, current_total_time): (f64, f64), diff --git a/src/app/data_harvester/load_avg.rs b/src/app/data_harvester/cpu/heim/unix.rs similarity index 75% rename from src/app/data_harvester/load_avg.rs rename to src/app/data_harvester/cpu/heim/unix.rs index 0f58ea8b..74340951 100644 --- a/src/app/data_harvester/load_avg.rs +++ b/src/app/data_harvester/cpu/heim/unix.rs @@ -1,6 +1,7 @@ -pub type LoadAvgHarvest = [f32; 3]; +//! Unix-specific functions regarding CPU usage. + +use crate::app::data_harvester::cpu::LoadAvgHarvest; -#[cfg(target_family = "unix")] pub async fn get_load_avg() -> crate::error::Result { let (one, five, fifteen) = heim::cpu::os::unix::loadavg().await?; diff --git a/src/app/data_harvester/cpu/heim/windows_macos.rs b/src/app/data_harvester/cpu/heim/windows_macos.rs new file mode 100644 index 00000000..34abc818 --- /dev/null +++ b/src/app/data_harvester/cpu/heim/windows_macos.rs @@ -0,0 +1,10 @@ +//! Windows and macOS-specific functions regarding CPU usage. + +pub fn convert_cpu_times(cpu_time: &heim::cpu::CpuTime) -> (f64, f64) { + let working_time: f64 = + (cpu_time.user() + cpu_time.system()).get::(); + ( + working_time, + working_time + cpu_time.idle().get::(), + ) +} diff --git a/src/app/data_harvester/cpu/mod.rs b/src/app/data_harvester/cpu/mod.rs new file mode 100644 index 00000000..81a0db4c --- /dev/null +++ b/src/app/data_harvester/cpu/mod.rs @@ -0,0 +1,14 @@ +//! Data collection for CPU usage and load average. +//! +//! For CPU usage, Linux, macOS, and Windows are handled by Heim. +//! +//! For load average, macOS and Linux are supported through Heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } +} + +pub type LoadAvgHarvest = [f32; 3]; diff --git a/src/app/data_harvester/disks/heim/linux.rs b/src/app/data_harvester/disks/heim/linux.rs new file mode 100644 index 00000000..cbc99d9f --- /dev/null +++ b/src/app/data_harvester/disks/heim/linux.rs @@ -0,0 +1,34 @@ +//! Linux-specific things for Heim disk data collection. + +use heim::disk::Partition; + +pub fn get_device_name(partition: &Partition) -> String { + if let Some(device) = partition.device() { + // See if this disk is actually mounted elsewhere on Linux... + // This is a workaround to properly map I/O in some cases (i.e. disk encryption), see + // https://github.com/ClementTsang/bottom/issues/419 + if let Ok(path) = std::fs::read_link(device) { + if path.is_absolute() { + path.into_os_string() + } else { + let mut combined_path = std::path::PathBuf::new(); + combined_path.push(device); + combined_path.pop(); // Pop the current file... + combined_path.push(path); + + if let Ok(canon_path) = std::fs::canonicalize(combined_path) { + // Resolve the local path into an absolute one... + canon_path.into_os_string() + } else { + device.to_os_string() + } + } + } else { + device.to_os_string() + } + .into_string() + .unwrap_or_else(|_| "Name Unavailable".to_string()) + } else { + "Name Unavailable".to_string() + } +} diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks/heim/mod.rs similarity index 74% rename from src/app/data_harvester/disks.rs rename to src/app/data_harvester/disks/heim/mod.rs index 103bb701..a79d00db 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks/heim/mod.rs @@ -1,5 +1,15 @@ use crate::app::Filter; +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use linux::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod windows_macos; + pub use windows_macos::*; + } +} + #[derive(Debug, Clone, Default)] pub struct DiskHarvest { pub name: String, @@ -62,44 +72,7 @@ pub async fn get_disk_usage( while let Some(part) = partitions_stream.next().await { if let Ok(partition) = part { - let symlink: std::ffi::OsString; - - let name = (if let Some(device) = partition.device() { - // See if this disk is actually mounted elsewhere on Linux... - // This is a workaround to properly map I/O in some cases (i.e. disk encryption), see - // https://github.com/ClementTsang/bottom/issues/419 - if cfg!(target_os = "linux") { - if let Ok(path) = std::fs::read_link(device) { - if path.is_absolute() { - symlink = path.into_os_string(); - symlink.as_os_str() - } else { - let mut combined_path = std::path::PathBuf::new(); - combined_path.push(device); - combined_path.pop(); // Pop the current file... - combined_path.push(path.clone()); - - if let Ok(path) = std::fs::canonicalize(combined_path) { - // Resolve the local path into an absolute one... - symlink = path.into_os_string(); - symlink.as_os_str() - } else { - symlink = path.into_os_string(); - symlink.as_os_str() - } - } - } else { - device - } - } else { - device - } - } else { - std::ffi::OsStr::new("Name Unavailable") - } - .to_str() - .unwrap_or("Name Unavailable")) - .to_string(); + let name = get_device_name(&partition); let mount_point = (partition .mount_point() diff --git a/src/app/data_harvester/disks/heim/windows_macos.rs b/src/app/data_harvester/disks/heim/windows_macos.rs new file mode 100644 index 00000000..428733bf --- /dev/null +++ b/src/app/data_harvester/disks/heim/windows_macos.rs @@ -0,0 +1,14 @@ +//! macOS and Windows-specific things for Heim disk data collection. + +use heim::disk::Partition; + +pub fn get_device_name(partition: &Partition) -> String { + if let Some(device) = partition.device() { + device + .to_os_string() + .into_string() + .unwrap_or_else(|_| "Name Unavailable".to_string()) + } else { + "Name Unavailable".to_string() + } +} diff --git a/src/app/data_harvester/disks/mod.rs b/src/app/data_harvester/disks/mod.rs new file mode 100644 index 00000000..e5a52336 --- /dev/null +++ b/src/app/data_harvester/disks/mod.rs @@ -0,0 +1,10 @@ +//! Data collection for disks (IO, usage, space, etc.). +//! +//! For Linux, macOS, and Windows, this is handled by heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } +} diff --git a/src/app/data_harvester/mem.rs b/src/app/data_harvester/memory/heim.rs similarity index 97% rename from src/app/data_harvester/mem.rs rename to src/app/data_harvester/memory/heim.rs index 014cbcdc..5319b1b3 100644 --- a/src/app/data_harvester/mem.rs +++ b/src/app/data_harvester/memory/heim.rs @@ -1,3 +1,5 @@ +//! Data collection for memory via heim. + #[derive(Debug, Clone)] pub struct MemHarvest { pub mem_total_in_kib: u64, diff --git a/src/app/data_harvester/memory/mod.rs b/src/app/data_harvester/memory/mod.rs new file mode 100644 index 00000000..588a3c3b --- /dev/null +++ b/src/app/data_harvester/memory/mod.rs @@ -0,0 +1,10 @@ +//! Data collection for memory. +//! +//! For Linux, macOS, and Windows, this is handled by Heim. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } +} diff --git a/src/app/data_harvester/network.rs b/src/app/data_harvester/network/heim.rs similarity index 53% rename from src/app/data_harvester/network.rs rename to src/app/data_harvester/network/heim.rs index 650a68e3..d18287c8 100644 --- a/src/app/data_harvester/network.rs +++ b/src/app/data_harvester/network/heim.rs @@ -1,81 +1,9 @@ +//! Gets network data via heim. + +use super::NetworkHarvest; use std::time::Instant; -#[derive(Default, Clone, Debug)] -/// All units in bits. -pub struct NetworkHarvest { - pub rx: u64, - pub tx: u64, - pub total_rx: u64, - pub total_tx: u64, -} - -impl NetworkHarvest { - pub fn first_run_cleanup(&mut self) { - self.rx = 0; - self.tx = 0; - } -} - -/// Separate Windows implementation required due to https://github.com/heim-rs/heim/issues/26. -#[cfg(target_os = "windows")] -pub async fn get_network_data( - sys: &sysinfo::System, prev_net_access_time: Instant, prev_net_rx: &mut u64, - prev_net_tx: &mut u64, curr_time: Instant, actually_get: bool, - filter: &Option, -) -> crate::utils::error::Result> { - use sysinfo::{NetworkExt, SystemExt}; - - if !actually_get { - return Ok(None); - } - - let mut total_rx: u64 = 0; - let mut total_tx: u64 = 0; - - let networks = sys.get_networks(); - for (name, network) in networks { - let to_keep = if let Some(filter) = filter { - let mut ret = filter.is_list_ignored; - for r in &filter.list { - if r.is_match(&name) { - ret = !filter.is_list_ignored; - break; - } - } - ret - } else { - true - }; - - if to_keep { - total_rx += network.get_total_received() * 8; - total_tx += network.get_total_transmitted() * 8; - } - } - - let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64(); - - let (rx, tx) = if elapsed_time == 0.0 { - (0, 0) - } else { - ( - ((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64, - ((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64, - ) - }; - - *prev_net_rx = total_rx; - *prev_net_tx = total_tx; - Ok(Some(NetworkHarvest { - rx, - tx, - total_rx, - total_tx, - })) -} - // FIXME: Eventually make it so that this thing also takes individual usage into account, so we can allow for showing per-interface! -#[cfg(not(target_os = "windows"))] pub async fn get_network_data( prev_net_access_time: Instant, prev_net_rx: &mut u64, prev_net_tx: &mut u64, curr_time: Instant, actually_get: bool, filter: &Option, diff --git a/src/app/data_harvester/network/mod.rs b/src/app/data_harvester/network/mod.rs new file mode 100644 index 00000000..c717e6ac --- /dev/null +++ b/src/app/data_harvester/network/mod.rs @@ -0,0 +1,30 @@ +//! Data collection for network usage/IO. +//! +//! For Linux and macOS, this is handled by Heim. +//! For Windows, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos"))] { + pub mod heim; + pub use self::heim::*; + } else if #[cfg(target_os = "windows")] { + pub mod sysinfo; + pub use self::sysinfo::*; + } +} + +#[derive(Default, Clone, Debug)] +/// All units in bits. +pub struct NetworkHarvest { + pub rx: u64, + pub tx: u64, + pub total_rx: u64, + pub total_tx: u64, +} + +impl NetworkHarvest { + pub fn first_run_cleanup(&mut self) { + self.rx = 0; + self.tx = 0; + } +} diff --git a/src/app/data_harvester/network/sysinfo.rs b/src/app/data_harvester/network/sysinfo.rs new file mode 100644 index 00000000..c7a7db00 --- /dev/null +++ b/src/app/data_harvester/network/sysinfo.rs @@ -0,0 +1,60 @@ +//! Gets network data via sysinfo. + +use super::NetworkHarvest; +use std::time::Instant; + +pub async fn get_network_data( + sys: &sysinfo::System, prev_net_access_time: Instant, prev_net_rx: &mut u64, + prev_net_tx: &mut u64, curr_time: Instant, actually_get: bool, + filter: &Option, +) -> crate::utils::error::Result> { + use sysinfo::{NetworkExt, SystemExt}; + + if !actually_get { + return Ok(None); + } + + let mut total_rx: u64 = 0; + let mut total_tx: u64 = 0; + + let networks = sys.get_networks(); + for (name, network) in networks { + let to_keep = if let Some(filter) = filter { + let mut ret = filter.is_list_ignored; + for r in &filter.list { + if r.is_match(&name) { + ret = !filter.is_list_ignored; + break; + } + } + ret + } else { + true + }; + + if to_keep { + total_rx += network.get_total_received() * 8; + total_tx += network.get_total_transmitted() * 8; + } + } + + let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64(); + + let (rx, tx) = if elapsed_time == 0.0 { + (0, 0) + } else { + ( + ((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64, + ((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64, + ) + }; + + *prev_net_rx = total_rx; + *prev_net_tx = total_tx; + Ok(Some(NetworkHarvest { + rx, + tx, + total_rx, + total_tx, + })) +} diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes/linux.rs similarity index 51% rename from src/app/data_harvester/processes.rs rename to src/app/data_harvester/processes/linux.rs index 94f6fb62..d6778ba3 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes/linux.rs @@ -1,99 +1,20 @@ +//! Process data collection for Linux. + +use crate::utils::error::{self, BottomError}; use crate::Pid; +use super::ProcessHarvest; + use sysinfo::ProcessStatus; -#[cfg(target_family = "unix")] -use crate::utils::error; - -#[cfg(target_os = "linux")] use procfs::process::{Process, Stat}; -#[cfg(target_os = "linux")] -use crate::utils::error::BottomError; - -#[cfg(target_os = "linux")] use fxhash::{FxHashMap, FxHashSet}; -#[cfg(not(target_os = "linux"))] -use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; - /// Maximum character length of a /proc//stat process name. /// If it's equal or greater, then we instead refer to the command for the name. -#[cfg(target_os = "linux")] const MAX_STAT_NAME_LEN: usize = 15; -// TODO: Add value so we know if it's sorted ascending or descending by default? -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub enum ProcessSorting { - CpuPercent, - Mem, - MemPercent, - Pid, - ProcessName, - Command, - ReadPerSecond, - WritePerSecond, - TotalRead, - TotalWrite, - State, - User, - Count, -} - -impl std::fmt::Display for ProcessSorting { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match &self { - ProcessSorting::CpuPercent => "CPU%", - ProcessSorting::MemPercent => "Mem%", - ProcessSorting::Mem => "Mem", - ProcessSorting::ReadPerSecond => "R/s", - ProcessSorting::WritePerSecond => "W/s", - ProcessSorting::TotalRead => "T.Read", - ProcessSorting::TotalWrite => "T.Write", - ProcessSorting::State => "State", - ProcessSorting::ProcessName => "Name", - ProcessSorting::Command => "Command", - ProcessSorting::Pid => "PID", - ProcessSorting::Count => "Count", - ProcessSorting::User => "User", - } - ) - } -} - -impl Default for ProcessSorting { - fn default() -> Self { - ProcessSorting::CpuPercent - } -} - -#[derive(Debug, Clone, Default)] -pub struct ProcessHarvest { - pub pid: Pid, - pub parent_pid: Option, // Remember, parent_pid 0 is root... - pub cpu_usage_percent: f64, - pub mem_usage_percent: f64, - pub mem_usage_bytes: u64, - // pub rss_kb: u64, - // pub virt_kb: u64, - pub name: String, - pub command: String, - pub read_bytes_per_sec: u64, - pub write_bytes_per_sec: u64, - pub total_read_bytes: u64, - pub total_write_bytes: u64, - pub process_state: String, - pub process_state_char: char, - - /// This is the *effective* user ID. - #[cfg(target_family = "unix")] - pub uid: Option, -} - -#[cfg(target_os = "linux")] #[derive(Debug, Clone)] pub struct PrevProcDetails { pub total_read_bytes: u64, @@ -102,7 +23,6 @@ pub struct PrevProcDetails { pub process: Process, } -#[cfg(target_os = "linux")] impl PrevProcDetails { fn new(pid: Pid) -> error::Result { Ok(Self { @@ -114,36 +34,6 @@ impl PrevProcDetails { } } -#[cfg(target_family = "unix")] -#[derive(Debug, Default)] -pub struct UserTable { - pub uid_user_mapping: std::collections::HashMap, -} - -#[cfg(target_family = "unix")] -impl UserTable { - pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result { - if let Some(user) = self.uid_user_mapping.get(&uid) { - Ok(user.clone()) - } else { - // SAFETY: getpwuid returns a null pointer if no passwd entry is found for the uid - let passwd = unsafe { libc::getpwuid(uid) }; - - if passwd.is_null() { - return Err(error::BottomError::QueryError("Missing passwd".into())); - } - - let username = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) } - .to_str()? - .to_string(); - self.uid_user_mapping.insert(uid, username.clone()); - - Ok(username) - } - } -} - -#[cfg(target_os = "linux")] fn cpu_usage_calculation( prev_idle: &mut f64, prev_non_idle: &mut f64, ) -> error::Result<(f64, f64)> { @@ -204,7 +94,6 @@ fn cpu_usage_calculation( } /// Returns the usage and a new set of process times. Note: cpu_fraction should be represented WITHOUT the x100 factor! -#[cfg(target_os = "linux")] fn get_linux_cpu_usage( stat: &Stat, cpu_usage: f64, cpu_fraction: f64, prev_proc_times: u64, use_current_cpu_total: bool, @@ -222,40 +111,7 @@ fn get_linux_cpu_usage( } } -#[cfg(target_os = "macos")] -fn get_macos_process_cpu_usage( - pids: &[i32], -) -> std::io::Result> { - use itertools::Itertools; - let output = std::process::Command::new("ps") - .args(&["-o", "pid=,pcpu=", "-p"]) - .arg( - // Has to look like this since otherwise, it you hit a `unstable_name_collisions` warning. - Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string()) - .collect::(), - ) - .output()?; - let mut result = std::collections::HashMap::new(); - String::from_utf8_lossy(&output.stdout) - .split_whitespace() - .chunks(2) - .into_iter() - .for_each(|chunk| { - let chunk: Vec<&str> = chunk.collect(); - if chunk.len() != 2 { - panic!("Unexpected `ps` output"); - } - let pid = chunk[0].parse(); - let usage = chunk[1].parse(); - if let (Ok(pid), Ok(usage)) = (pid, usage) { - result.insert(pid, usage); - } - }); - Ok(result) -} - #[allow(clippy::too_many_arguments)] -#[cfg(target_os = "linux")] fn read_proc( prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64, use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64, @@ -361,7 +217,6 @@ fn read_proc( )) } -#[cfg(target_os = "linux")] pub fn get_process_data( prev_idle: &mut f64, prev_non_idle: &mut f64, pid_mapping: &mut FxHashMap, use_current_cpu_total: bool, @@ -437,142 +292,3 @@ pub fn get_process_data( )) } } - -#[cfg(not(target_os = "linux"))] -pub fn get_process_data( - sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, -) -> crate::utils::error::Result> { - let mut process_vector: Vec = Vec::new(); - let process_hashmap = sys.get_processes(); - let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0; - let num_cpus = sys.get_processors().len() as f64; - for process_val in process_hashmap.values() { - let name = if process_val.name().is_empty() { - let process_cmd = process_val.cmd(); - if process_cmd.len() > 1 { - process_cmd[0].clone() - } else { - let process_exe = process_val.exe().file_stem(); - if let Some(exe) = process_exe { - let process_exe_opt = exe.to_str(); - if let Some(exe_name) = process_exe_opt { - exe_name.to_string() - } else { - "".to_string() - } - } else { - "".to_string() - } - } - } else { - process_val.name().to_string() - }; - let command = { - let command = process_val.cmd().join(" "); - if command.is_empty() { - name.to_string() - } else { - command - } - }; - - let pcu = if cfg!(target_os = "windows") || num_cpus == 0.0 { - process_val.cpu_usage() as f64 - } else { - process_val.cpu_usage() as f64 / num_cpus - }; - let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 { - pcu / cpu_usage - } else { - pcu - }; - - let disk_usage = process_val.disk_usage(); - #[cfg(target_os = "macos")] - { - process_vector.push(ProcessHarvest { - pid: process_val.pid(), - parent_pid: process_val.parent(), - name, - command, - mem_usage_percent: if mem_total_kb > 0 { - process_val.memory() as f64 * 100.0 / mem_total_kb as f64 - } else { - 0.0 - }, - mem_usage_bytes: process_val.memory() * 1024, - cpu_usage_percent: process_cpu_usage, - read_bytes_per_sec: disk_usage.read_bytes, - write_bytes_per_sec: disk_usage.written_bytes, - total_read_bytes: disk_usage.total_read_bytes, - total_write_bytes: disk_usage.total_written_bytes, - process_state: process_val.status().to_string(), - process_state_char: convert_process_status_to_char(process_val.status()), - uid: Some(process_val.uid), - }); - } - #[cfg(not(target_os = "macos"))] - { - process_vector.push(ProcessHarvest { - pid: process_val.pid(), - parent_pid: process_val.parent(), - name, - command, - mem_usage_percent: if mem_total_kb > 0 { - process_val.memory() as f64 * 100.0 / mem_total_kb as f64 - } else { - 0.0 - }, - mem_usage_bytes: process_val.memory() * 1024, - cpu_usage_percent: process_cpu_usage, - read_bytes_per_sec: disk_usage.read_bytes, - write_bytes_per_sec: disk_usage.written_bytes, - total_read_bytes: disk_usage.total_read_bytes, - total_write_bytes: disk_usage.total_written_bytes, - process_state: process_val.status().to_string(), - process_state_char: convert_process_status_to_char(process_val.status()), - }); - } - } - - #[cfg(target_os = "macos")] - { - let unknown_state = ProcessStatus::Unknown(0).to_string(); - let cpu_usage_unknown_pids: Vec = process_vector - .iter() - .filter(|process| process.process_state == unknown_state) - .map(|process| process.pid) - .collect(); - let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?; - for process in &mut process_vector { - if cpu_usages.contains_key(&process.pid) { - process.cpu_usage_percent = if num_cpus == 0.0 { - *cpu_usages.get(&process.pid).unwrap() - } else { - *cpu_usages.get(&process.pid).unwrap() / num_cpus - }; - } - } - } - - Ok(process_vector) -} - -#[allow(unused_variables)] -#[cfg(not(target_os = "linux"))] -fn convert_process_status_to_char(status: ProcessStatus) -> char { - #[cfg(target_os = "macos")] - { - match status { - ProcessStatus::Run => 'R', - ProcessStatus::Sleep => 'S', - ProcessStatus::Idle => 'D', - ProcessStatus::Zombie => 'Z', - _ => '?', - } - } - #[cfg(not(target_os = "macos"))] - { - 'R' - } -} diff --git a/src/app/data_harvester/processes/macos.rs b/src/app/data_harvester/processes/macos.rs new file mode 100644 index 00000000..4290834e --- /dev/null +++ b/src/app/data_harvester/processes/macos.rs @@ -0,0 +1,139 @@ +//! Process data collection for macOS. Uses sysinfo. + +use super::ProcessHarvest; +use sysinfo::{ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt}; + +fn get_macos_process_cpu_usage( + pids: &[i32], +) -> std::io::Result> { + use itertools::Itertools; + let output = std::process::Command::new("ps") + .args(&["-o", "pid=,pcpu=", "-p"]) + .arg( + // Has to look like this since otherwise, it you hit a `unstable_name_collisions` warning. + Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string()) + .collect::(), + ) + .output()?; + let mut result = std::collections::HashMap::new(); + String::from_utf8_lossy(&output.stdout) + .split_whitespace() + .chunks(2) + .into_iter() + .for_each(|chunk| { + let chunk: Vec<&str> = chunk.collect(); + if chunk.len() != 2 { + panic!("Unexpected `ps` output"); + } + let pid = chunk[0].parse(); + let usage = chunk[1].parse(); + if let (Ok(pid), Ok(usage)) = (pid, usage) { + result.insert(pid, usage); + } + }); + Ok(result) +} + +pub fn get_process_data( + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, +) -> crate::utils::error::Result> { + let mut process_vector: Vec = Vec::new(); + let process_hashmap = sys.get_processes(); + let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0; + let num_cpus = sys.get_processors().len() as f64; + for process_val in process_hashmap.values() { + let name = if process_val.name().is_empty() { + let process_cmd = process_val.cmd(); + if process_cmd.len() > 1 { + process_cmd[0].clone() + } else { + let process_exe = process_val.exe().file_stem(); + if let Some(exe) = process_exe { + let process_exe_opt = exe.to_str(); + if let Some(exe_name) = process_exe_opt { + exe_name.to_string() + } else { + "".to_string() + } + } else { + "".to_string() + } + } + } else { + process_val.name().to_string() + }; + let command = { + let command = process_val.cmd().join(" "); + if command.is_empty() { + name.to_string() + } else { + command + } + }; + + let pcu = { + let p = process_val.cpu_usage() as f64 / num_cpus; + if p.is_nan() { + process_val.cpu_usage() as f64 + } else { + p + } + }; + let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 { + pcu / cpu_usage + } else { + pcu + }; + + let disk_usage = process_val.disk_usage(); + process_vector.push(ProcessHarvest { + pid: process_val.pid(), + parent_pid: process_val.parent(), + name, + command, + mem_usage_percent: if mem_total_kb > 0 { + process_val.memory() as f64 * 100.0 / mem_total_kb as f64 + } else { + 0.0 + }, + mem_usage_bytes: process_val.memory() * 1024, + cpu_usage_percent: process_cpu_usage, + read_bytes_per_sec: disk_usage.read_bytes, + write_bytes_per_sec: disk_usage.written_bytes, + total_read_bytes: disk_usage.total_read_bytes, + total_write_bytes: disk_usage.total_written_bytes, + process_state: process_val.status().to_string(), + process_state_char: convert_process_status_to_char(process_val.status()), + uid: Some(process_val.uid), + }); + } + + let unknown_state = ProcessStatus::Unknown(0).to_string(); + let cpu_usage_unknown_pids: Vec = process_vector + .iter() + .filter(|process| process.process_state == unknown_state) + .map(|process| process.pid) + .collect(); + let cpu_usages = get_macos_process_cpu_usage(&cpu_usage_unknown_pids)?; + for process in &mut process_vector { + if cpu_usages.contains_key(&process.pid) { + process.cpu_usage_percent = if num_cpus == 0.0 { + *cpu_usages.get(&process.pid).unwrap() + } else { + *cpu_usages.get(&process.pid).unwrap() / num_cpus + }; + } + } + + Ok(process_vector) +} + +fn convert_process_status_to_char(status: ProcessStatus) -> char { + match status { + ProcessStatus::Run => 'R', + ProcessStatus::Sleep => 'S', + ProcessStatus::Idle => 'D', + ProcessStatus::Zombie => 'Z', + _ => '?', + } +} diff --git a/src/app/data_harvester/processes/mod.rs b/src/app/data_harvester/processes/mod.rs new file mode 100644 index 00000000..283080b3 --- /dev/null +++ b/src/app/data_harvester/processes/mod.rs @@ -0,0 +1,97 @@ +//! Data collection for processes. +//! +//! For Linux, this is handled by a custom set of functions. +//! For Windows and macOS, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux; + pub use self::linux::*; + } else if #[cfg(target_os = "macos")] { + pub mod macos; + pub use self::macos::*; + } else if #[cfg(target_os = "windows")] { + pub mod windows; + pub use self::windows::*; + } +} + +cfg_if::cfg_if! { + if #[cfg(target_family = "unix")] { + pub mod unix; + pub use self::unix::*; + } +} + +use crate::Pid; + +// TODO: Add value so we know if it's sorted ascending or descending by default? +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum ProcessSorting { + CpuPercent, + Mem, + MemPercent, + Pid, + ProcessName, + Command, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, + User, + Count, +} + +impl std::fmt::Display for ProcessSorting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match &self { + ProcessSorting::CpuPercent => "CPU%", + ProcessSorting::MemPercent => "Mem%", + ProcessSorting::Mem => "Mem", + ProcessSorting::ReadPerSecond => "R/s", + ProcessSorting::WritePerSecond => "W/s", + ProcessSorting::TotalRead => "T.Read", + ProcessSorting::TotalWrite => "T.Write", + ProcessSorting::State => "State", + ProcessSorting::ProcessName => "Name", + ProcessSorting::Command => "Command", + ProcessSorting::Pid => "PID", + ProcessSorting::Count => "Count", + ProcessSorting::User => "User", + } + ) + } +} + +impl Default for ProcessSorting { + fn default() -> Self { + ProcessSorting::CpuPercent + } +} + +#[derive(Debug, Clone, Default)] +pub struct ProcessHarvest { + pub pid: Pid, + pub parent_pid: Option, // Remember, parent_pid 0 is root... + pub cpu_usage_percent: f64, + pub mem_usage_percent: f64, + pub mem_usage_bytes: u64, + // pub rss_kb: u64, + // pub virt_kb: u64, + pub name: String, + pub command: String, + pub read_bytes_per_sec: u64, + pub write_bytes_per_sec: u64, + pub total_read_bytes: u64, + pub total_write_bytes: u64, + pub process_state: String, + pub process_state_char: char, + + /// This is the *effective* user ID. + #[cfg(target_family = "unix")] + pub uid: Option, +} diff --git a/src/app/data_harvester/processes/unix.rs b/src/app/data_harvester/processes/unix.rs new file mode 100644 index 00000000..8fadc590 --- /dev/null +++ b/src/app/data_harvester/processes/unix.rs @@ -0,0 +1,30 @@ +//! Unix-specific parts of process collection. + +use crate::utils::error; + +#[derive(Debug, Default)] +pub struct UserTable { + pub uid_user_mapping: std::collections::HashMap, +} + +impl UserTable { + pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result { + if let Some(user) = self.uid_user_mapping.get(&uid) { + Ok(user.clone()) + } else { + // SAFETY: getpwuid returns a null pointer if no passwd entry is found for the uid + let passwd = unsafe { libc::getpwuid(uid) }; + + if passwd.is_null() { + return Err(error::BottomError::QueryError("Missing passwd".into())); + } + + let username = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) } + .to_str()? + .to_string(); + self.uid_user_mapping.insert(uid, username.clone()); + + Ok(username) + } + } +} diff --git a/src/app/data_harvester/processes/windows.rs b/src/app/data_harvester/processes/windows.rs new file mode 100644 index 00000000..e7e76004 --- /dev/null +++ b/src/app/data_harvester/processes/windows.rs @@ -0,0 +1,72 @@ +//! Process data collection for Windows. Uses sysinfo. + +use super::ProcessHarvest; +use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt}; + +pub fn get_process_data( + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, +) -> crate::utils::error::Result> { + let mut process_vector: Vec = Vec::new(); + let process_hashmap = sys.get_processes(); + let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0; + for process_val in process_hashmap.values() { + let name = if process_val.name().is_empty() { + let process_cmd = process_val.cmd(); + if process_cmd.len() > 1 { + process_cmd[0].clone() + } else { + let process_exe = process_val.exe().file_stem(); + if let Some(exe) = process_exe { + let process_exe_opt = exe.to_str(); + if let Some(exe_name) = process_exe_opt { + exe_name.to_string() + } else { + "".to_string() + } + } else { + "".to_string() + } + } + } else { + process_val.name().to_string() + }; + let command = { + let command = process_val.cmd().join(" "); + if command.is_empty() { + name.to_string() + } else { + command + } + }; + + let pcu = process_val.cpu_usage() as f64; + let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 { + pcu / cpu_usage + } else { + pcu + }; + + let disk_usage = process_val.disk_usage(); + process_vector.push(ProcessHarvest { + pid: process_val.pid(), + parent_pid: process_val.parent(), + name, + command, + mem_usage_percent: if mem_total_kb > 0 { + process_val.memory() as f64 * 100.0 / mem_total_kb as f64 + } else { + 0.0 + }, + mem_usage_bytes: process_val.memory() * 1024, + cpu_usage_percent: process_cpu_usage, + read_bytes_per_sec: disk_usage.read_bytes, + write_bytes_per_sec: disk_usage.written_bytes, + total_read_bytes: disk_usage.total_read_bytes, + total_write_bytes: disk_usage.total_written_bytes, + process_state: process_val.status().to_string(), + process_state_char: 'R', + }); + } + + Ok(process_vector) +} diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs deleted file mode 100644 index eb3c2f6f..00000000 --- a/src/app/data_harvester/temperature.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::cmp::Ordering; - -use crate::app::Filter; - -#[derive(Default, Debug, Clone)] -pub struct TempHarvest { - pub name: String, - pub temperature: f32, -} - -#[derive(Clone, Debug)] -pub enum TemperatureType { - Celsius, - Kelvin, - Fahrenheit, -} - -impl Default for TemperatureType { - fn default() -> Self { - TemperatureType::Celsius - } -} - -fn is_temp_filtered(filter: &Option, text: &str) -> bool { - if let Some(filter) = filter { - if filter.is_list_ignored { - let mut ret = true; - for r in &filter.list { - if r.is_match(text) { - ret = false; - break; - } - } - ret - } else { - true - } - } else { - true - } -} - -#[cfg(not(target_os = "linux"))] -pub async fn get_temperature_data( - sys: &sysinfo::System, temp_type: &TemperatureType, actually_get: bool, filter: &Option, -) -> crate::utils::error::Result>> { - use sysinfo::{ComponentExt, SystemExt}; - - if !actually_get { - return Ok(None); - } - - fn convert_celsius_to_kelvin(celsius: f32) -> f32 { - celsius + 273.15 - } - - fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 { - (celsius * (9.0 / 5.0)) + 32.0 - } - - let mut temperature_vec: Vec = Vec::new(); - - let sensor_data = sys.get_components(); - for component in sensor_data { - let name = component.get_label().to_string(); - - if is_temp_filtered(filter, &name) { - temperature_vec.push(TempHarvest { - name, - temperature: match temp_type { - TemperatureType::Celsius => component.get_temperature(), - TemperatureType::Kelvin => { - convert_celsius_to_kelvin(component.get_temperature()) - } - TemperatureType::Fahrenheit => { - convert_celsius_to_fahrenheit(component.get_temperature()) - } - }, - }); - } - } - - temp_vec_sort(&mut temperature_vec); - Ok(Some(temperature_vec)) -} - -#[cfg(target_os = "linux")] -pub async fn get_temperature_data( - temp_type: &TemperatureType, actually_get: bool, filter: &Option, -) -> crate::utils::error::Result>> { - use futures::StreamExt; - use heim::units::thermodynamic_temperature; - - if !actually_get { - return Ok(None); - } - - let mut temperature_vec: Vec = Vec::new(); - - let mut sensor_data = heim::sensors::temperatures().boxed_local(); - while let Some(sensor) = sensor_data.next().await { - if let Ok(sensor) = sensor { - let component_name = Some(sensor.unit().to_string()); - let component_label = sensor.label().map(|label| label.to_string()); - - let name = match (component_name, component_label) { - (Some(name), Some(label)) => format!("{}: {}", name, label), - (None, Some(label)) => label.to_string(), - (Some(name), None) => name.to_string(), - (None, None) => String::default(), - }; - - if is_temp_filtered(filter, &name) { - temperature_vec.push(TempHarvest { - name, - temperature: match temp_type { - TemperatureType::Celsius => sensor - .current() - .get::( - ), - TemperatureType::Kelvin => { - sensor.current().get::() - } - TemperatureType::Fahrenheit => sensor - .current() - .get::( - ), - }, - }); - } - } - } - - temp_vec_sort(&mut temperature_vec); - Ok(Some(temperature_vec)) -} - -fn temp_vec_sort(temperature_vec: &mut Vec) { - // By default, sort temperature, then by alphabetically! - // TODO: [TEMPS] Allow users to control this. - - // Note we sort in reverse here; we want greater temps to be higher priority. - temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) { - Some(x) => match x { - Ordering::Less => Ordering::Greater, - Ordering::Greater => Ordering::Less, - Ordering::Equal => Ordering::Equal, - }, - None => Ordering::Equal, - }); - - temperature_vec.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap_or(Ordering::Equal)); -} diff --git a/src/app/data_harvester/temperature/heim.rs b/src/app/data_harvester/temperature/heim.rs new file mode 100644 index 00000000..570f490e --- /dev/null +++ b/src/app/data_harvester/temperature/heim.rs @@ -0,0 +1,54 @@ +//! Gets temperature data via heim. + +use super::{is_temp_filtered, temp_vec_sort, TempHarvest, TemperatureType}; +use crate::app::Filter; + +pub async fn get_temperature_data( + temp_type: &TemperatureType, actually_get: bool, filter: &Option, +) -> crate::utils::error::Result>> { + use futures::StreamExt; + use heim::units::thermodynamic_temperature; + + if !actually_get { + return Ok(None); + } + + let mut temperature_vec: Vec = Vec::new(); + + let mut sensor_data = heim::sensors::temperatures().boxed_local(); + while let Some(sensor) = sensor_data.next().await { + if let Ok(sensor) = sensor { + let component_name = Some(sensor.unit().to_string()); + let component_label = sensor.label().map(|label| label.to_string()); + + let name = match (component_name, component_label) { + (Some(name), Some(label)) => format!("{}: {}", name, label), + (None, Some(label)) => label.to_string(), + (Some(name), None) => name.to_string(), + (None, None) => String::default(), + }; + + if is_temp_filtered(filter, &name) { + temperature_vec.push(TempHarvest { + name, + temperature: match temp_type { + TemperatureType::Celsius => sensor + .current() + .get::( + ), + TemperatureType::Kelvin => { + sensor.current().get::() + } + TemperatureType::Fahrenheit => sensor + .current() + .get::( + ), + }, + }); + } + } + } + + temp_vec_sort(&mut temperature_vec); + Ok(Some(temperature_vec)) +} diff --git a/src/app/data_harvester/temperature/mod.rs b/src/app/data_harvester/temperature/mod.rs new file mode 100644 index 00000000..8f3b776e --- /dev/null +++ b/src/app/data_harvester/temperature/mod.rs @@ -0,0 +1,73 @@ +//! Data collection for temperature metrics. +//! +//! For Linux and macOS, this is handled by Heim. +//! For Windows, this is handled by sysinfo. + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod heim; + pub use self::heim::*; + } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { + pub mod sysinfo; + pub use self::sysinfo::*; + } +} + +use std::cmp::Ordering; + +use crate::app::Filter; + +#[derive(Default, Debug, Clone)] +pub struct TempHarvest { + pub name: String, + pub temperature: f32, +} + +#[derive(Clone, Debug)] +pub enum TemperatureType { + Celsius, + Kelvin, + Fahrenheit, +} + +impl Default for TemperatureType { + fn default() -> Self { + TemperatureType::Celsius + } +} + +fn is_temp_filtered(filter: &Option, text: &str) -> bool { + if let Some(filter) = filter { + if filter.is_list_ignored { + let mut ret = true; + for r in &filter.list { + if r.is_match(text) { + ret = false; + break; + } + } + ret + } else { + true + } + } else { + true + } +} + +fn temp_vec_sort(temperature_vec: &mut Vec) { + // By default, sort temperature, then by alphabetically! + // TODO: [TEMPS] Allow users to control this. + + // Note we sort in reverse here; we want greater temps to be higher priority. + temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) { + Some(x) => match x { + Ordering::Less => Ordering::Greater, + Ordering::Greater => Ordering::Less, + Ordering::Equal => Ordering::Equal, + }, + None => Ordering::Equal, + }); + + temperature_vec.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap_or(Ordering::Equal)); +} diff --git a/src/app/data_harvester/temperature/sysinfo.rs b/src/app/data_harvester/temperature/sysinfo.rs new file mode 100644 index 00000000..f0f79b1a --- /dev/null +++ b/src/app/data_harvester/temperature/sysinfo.rs @@ -0,0 +1,47 @@ +//! Gets temperature data via sysinfo. + +use super::{is_temp_filtered, temp_vec_sort, TempHarvest, TemperatureType}; +use crate::app::Filter; + +pub async fn get_temperature_data( + sys: &sysinfo::System, temp_type: &TemperatureType, actually_get: bool, filter: &Option, +) -> crate::utils::error::Result>> { + use sysinfo::{ComponentExt, SystemExt}; + + if !actually_get { + return Ok(None); + } + + fn convert_celsius_to_kelvin(celsius: f32) -> f32 { + celsius + 273.15 + } + + fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 { + (celsius * (9.0 / 5.0)) + 32.0 + } + + let mut temperature_vec: Vec = Vec::new(); + + let sensor_data = sys.get_components(); + for component in sensor_data { + let name = component.get_label().to_string(); + + if is_temp_filtered(filter, &name) { + temperature_vec.push(TempHarvest { + name, + temperature: match temp_type { + TemperatureType::Celsius => component.get_temperature(), + TemperatureType::Kelvin => { + convert_celsius_to_kelvin(component.get_temperature()) + } + TemperatureType::Fahrenheit => { + convert_celsius_to_fahrenheit(component.get_temperature()) + } + }, + }); + } + } + + temp_vec_sort(&mut temperature_vec); + Ok(Some(temperature_vec)) +}