From 6847f2ff0ce5827c0779a1dfcb9e6a8a657652b1 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 15 May 2021 18:57:02 -0700 Subject: refactor: split up data collection by OS (#482) Refactor to split up data collection by OS and/or the backing library. The goal is to make it easier to work with and add new OS support, as opposed to how it was prior where we stored OS-independent implementations all in the same file. --- src/app/data_harvester/batteries.rs | 38 -- src/app/data_harvester/batteries/battery.rs | 49 ++ src/app/data_harvester/batteries/mod.rs | 10 + src/app/data_harvester/cpu.rs | 179 ------- src/app/data_harvester/cpu/heim/linux.rs | 16 + src/app/data_harvester/cpu/heim/mod.rs | 170 ++++++ src/app/data_harvester/cpu/heim/unix.rs | 13 + src/app/data_harvester/cpu/heim/windows_macos.rs | 10 + src/app/data_harvester/cpu/mod.rs | 14 + src/app/data_harvester/disks.rs | 181 ------- src/app/data_harvester/disks/heim/linux.rs | 34 ++ src/app/data_harvester/disks/heim/mod.rs | 154 ++++++ src/app/data_harvester/disks/heim/windows_macos.rs | 14 + src/app/data_harvester/disks/mod.rs | 10 + src/app/data_harvester/load_avg.rs | 12 - src/app/data_harvester/mem.rs | 52 -- src/app/data_harvester/memory/heim.rs | 54 ++ src/app/data_harvester/memory/mod.rs | 10 + src/app/data_harvester/network.rs | 143 ----- src/app/data_harvester/network/heim.rs | 71 +++ src/app/data_harvester/network/mod.rs | 30 ++ src/app/data_harvester/network/sysinfo.rs | 60 +++ src/app/data_harvester/processes.rs | 578 --------------------- src/app/data_harvester/processes/linux.rs | 294 +++++++++++ src/app/data_harvester/processes/macos.rs | 139 +++++ src/app/data_harvester/processes/mod.rs | 97 ++++ src/app/data_harvester/processes/unix.rs | 30 ++ src/app/data_harvester/processes/windows.rs | 72 +++ src/app/data_harvester/temperature.rs | 153 ------ src/app/data_harvester/temperature/heim.rs | 54 ++ src/app/data_harvester/temperature/mod.rs | 73 +++ src/app/data_harvester/temperature/sysinfo.rs | 47 ++ 32 files changed, 1525 insertions(+), 1336 deletions(-) delete mode 100644 src/app/data_harvester/batteries.rs create mode 100644 src/app/data_harvester/batteries/battery.rs create mode 100644 src/app/data_harvester/batteries/mod.rs delete mode 100644 src/app/data_harvester/cpu.rs create mode 100644 src/app/data_harvester/cpu/heim/linux.rs create mode 100644 src/app/data_harvester/cpu/heim/mod.rs create mode 100644 src/app/data_harvester/cpu/heim/unix.rs create mode 100644 src/app/data_harvester/cpu/heim/windows_macos.rs create mode 100644 src/app/data_harvester/cpu/mod.rs delete mode 100644 src/app/data_harvester/disks.rs create mode 100644 src/app/data_harvester/disks/heim/linux.rs create mode 100644 src/app/data_harvester/disks/heim/mod.rs create mode 100644 src/app/data_harvester/disks/heim/windows_macos.rs create mode 100644 src/app/data_harvester/disks/mod.rs delete mode 100644 src/app/data_harvester/load_avg.rs delete mode 100644 src/app/data_harvester/mem.rs create mode 100644 src/app/data_harvester/memory/heim.rs create mode 100644 src/app/data_harvester/memory/mod.rs delete mode 100644 src/app/data_harvester/network.rs create mode 100644 src/app/data_harvester/network/heim.rs create mode 100644 src/app/data_harvester/network/mod.rs create mode 100644 src/app/data_harvester/network/sysinfo.rs delete mode 100644 src/app/data_harvester/processes.rs create mode 100644 src/app/data_harvester/processes/linux.rs create mode 100644 src/app/data_harvester/processes/macos.rs create mode 100644 src/app/data_harvester/processes/mod.rs create mode 100644 src/app/data_harvester/processes/unix.rs create mode 100644 src/app/data_harvester/processes/windows.rs delete mode 100644 src/app/data_harvester/temperature.rs create mode 100644 src/app/data_harvester/temperature/heim.rs create mode 100644 src/app/data_harvester/temperature/mod.rs create mode 100644 src/app/data_harvester/temperature/sysinfo.rs (limited to 'src/app/data_harvester') diff --git a/src/app/data_harvester/batteries.rs b/src/app/data_harvester/batteries.rs deleted file mode 100644 index 98cf6ae6..00000000 --- a/src/app/data_harvester/batteries.rs +++ /dev/null @@ -1,38 +0,0 @@ -use battery::{ - units::{power::watt, ratio::percent, time::second}, - Battery, Manager, -}; - -#[derive(Debug, Clone)] -pub struct BatteryHarvest { - pub charge_percent: f64, - pub secs_until_full: Option, - pub secs_until_empty: Option, - pub power_consumption_rate_watts: f64, - pub health_percent: f64, -} - -pub fn refresh_batteries(manager: &Manager, batteries: &mut [Battery]) -> Vec { - batteries - .iter_mut() - .filter_map(|battery| { - if manager.refresh(battery).is_ok() { - Some(BatteryHarvest { - secs_until_full: { - let optional_time = battery.time_to_full(); - optional_time.map(|time| f64::from(time.get::()) as i64) - }, - secs_until_empty: { - let optional_time = battery.time_to_empty(); - optional_time.map(|time| f64::from(time.get::()) as i64) - }, - charge_percent: f64::from(battery.state_of_charge().get::()), - power_consumption_rate_watts: f64::from(battery.energy_rate().get::()), - health_percent: f64::from(battery.state_of_health().get::()), - }) - } else { - None - } - }) - .collect::>() -} diff --git a/src/app/data_harvester/batteries/battery.rs b/src/app/data_harvester/batteries/battery.rs new file mode 100644 index 00000000..7e4644e9 --- /dev/null +++ b/src/app/data_harvester/batteries/battery.rs @@ -0,0 +1,49 @@ +//! 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, +}; + +#[derive(Debug, Clone)] +pub struct BatteryHarvest { + pub charge_percent: f64, + pub secs_until_full: Option, + pub secs_until_empty: Option, + pub power_consumption_rate_watts: f64, + pub health_percent: f64, +} + +pub fn refresh_batteries(manager: &Manager, batteries: &mut [Battery]) -> Vec { + batteries + .iter_mut() + .filter_map(|battery| { + if manager.refresh(battery).is_ok() { + Some(BatteryHarvest { + secs_until_full: { + let optional_time = battery.time_to_full(); + optional_time.map(|time| f64::from(time.get::()) as i64) + }, + secs_until_empty: { + let optional_time = battery.time_to_empty(); + optional_time.map(|time| f64::from(time.get::()) as i64) + }, + charge_percent: f64::from(battery.state_of_charge().get::()), + power_consumption_rate_watts: f64::from(battery.energy_rate().get::()), + health_percent: f64::from(battery.state_of_health().get::()), + }) + } else { + None + } + }) + .collect::>() +} 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.rs b/src/app/data_harvester/cpu.rs deleted file mode 100644 index 61e79e28..00000000 --- a/src/app/data_harvester/cpu.rs +++ /dev/null @@ -1,179 +0,0 @@ -#[derive(Default, Debug, Clone)] -pub struct CpuData { - pub cpu_prefix: String, - pub cpu_count: Option, - pub cpu_usage: f64, -} - -pub type CpuHarvest = Vec; - -pub type PastCpuWork = f64; -pub type PastCpuTotal = f64; - -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), - ) -> f64 { - ((if current_working_time > previous_working_time { - current_working_time - previous_working_time - } else { - 0.0 - }) * 100.0) - / (if current_total_time > previous_total_time { - current_total_time - previous_total_time - } else { - 1.0 - }) - } - - // Get all CPU times... - let cpu_times = heim::cpu::times().await?; - futures::pin_mut!(cpu_times); - - let mut cpu_deque: VecDeque = if previous_cpu_times.is_empty() { - // Must initialize ourselves. Use a very quick timeout to calculate an initial. - futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; - - let second_cpu_times = heim::cpu::times().await?; - futures::pin_mut!(second_cpu_times); - - let mut new_cpu_times: Vec<(PastCpuWork, PastCpuTotal)> = Vec::new(); - let mut cpu_deque: VecDeque = VecDeque::new(); - let mut collected_zip = cpu_times.zip(second_cpu_times).enumerate(); // Gotta move it here, can't on while line. - - while let Some((itx, (past, present))) = collected_zip.next().await { - if let (Ok(past), Ok(present)) = (past, present) { - let present_times = convert_cpu_times(&present); - new_cpu_times.push(present_times); - cpu_deque.push_back(CpuData { - cpu_prefix: "CPU".to_string(), - cpu_count: Some(itx), - cpu_usage: calculate_cpu_usage_percentage( - convert_cpu_times(&past), - present_times, - ), - }); - } else { - new_cpu_times.push((0.0, 0.0)); - cpu_deque.push_back(CpuData { - cpu_prefix: "CPU".to_string(), - cpu_count: Some(itx), - cpu_usage: 0.0, - }); - } - } - - *previous_cpu_times = new_cpu_times; - cpu_deque - } else { - let (new_cpu_times, cpu_deque): (Vec<(PastCpuWork, PastCpuTotal)>, VecDeque) = - cpu_times - .collect::>() - .await - .iter() - .zip(&*previous_cpu_times) - .enumerate() - .map(|(itx, (current_cpu, (past_cpu_work, past_cpu_total)))| { - if let Ok(cpu_time) = current_cpu { - let present_times = convert_cpu_times(&cpu_time); - - ( - present_times, - CpuData { - cpu_prefix: "CPU".to_string(), - cpu_count: Some(itx), - cpu_usage: calculate_cpu_usage_percentage( - (*past_cpu_work, *past_cpu_total), - present_times, - ), - }, - ) - } else { - ( - (*past_cpu_work, *past_cpu_total), - CpuData { - cpu_prefix: "CPU".to_string(), - cpu_count: Some(itx), - cpu_usage: 0.0, - }, - ) - } - }) - .unzip(); - - *previous_cpu_times = new_cpu_times; - cpu_deque - }; - - // Get average CPU if needed... and slap it at the top - if show_average_cpu { - let cpu_time = heim::cpu::time().await?; - - let (cpu_usage, new_average_cpu_time) = if let Some((past_cpu_work, past_cpu_total)) = - previous_average_cpu_time - { - let present_times = convert_cpu_times(&cpu_time); - ( - calculate_cpu_usage_percentage((*past_cpu_work, *past_cpu_total), present_times), - present_times, - ) - } else { - // Again, we need to do a quick timeout... - futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; - let second_cpu_time = heim::cpu::time().await?; - - let present_times = convert_cpu_times(&second_cpu_time); - ( - calculate_cpu_usage_percentage(convert_cpu_times(&cpu_time), present_times), - present_times, - ) - }; - - *previous_average_cpu_time = Some(new_average_cpu_time); - cpu_deque.push_front(CpuData { - cpu_prefix: "AVG".to_string(), - cpu_count: None, - cpu_usage, - }) - } - - // Ok(Vec::from(cpu_deque.drain(0..3).collect::>())) // For artificially limiting the CPU results - - Ok(Vec::from(cpu_deque)) -} 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/heim/mod.rs b/src/app/data_harvester/cpu/heim/mod.rs new file mode 100644 index 00000000..73a97b5b --- /dev/null +++ b/src/app/data_harvester/cpu/heim/mod.rs @@ -0,0 +1,170 @@ +//! 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, + pub cpu_count: Option, + pub cpu_usage: f64, +} + +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 { + fn calculate_cpu_usage_percentage( + (previous_working_time, previous_total_time): (f64, f64), + (current_working_time, current_total_time): (f64, f64), + ) -> f64 { + ((if current_working_time > previous_working_time { + current_working_time - previous_working_time + } else { + 0.0 + }) * 100.0) + / (if current_total_time > previous_total_time { + current_total_time - previous_total_time + } else { + 1.0 + }) + } + + // Get all CPU times... + let cpu_times = heim::cpu::times().await?; + futures::pin_mut!(cpu_times); + + let mut cpu_deque: VecDeque = if previous_cpu_times.is_empty() { + // Must initialize ourselves. Use a very quick timeout to calculate an initial. + futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; + + let second_cpu_times = heim::cpu::times().await?; + futures::pin_mut!(second_cpu_times); + + let mut new_cpu_times: Vec<(PastCpuWork, PastCpuTotal)> = Vec::new(); + let mut cpu_deque: VecDeque = VecDeque::new(); + let mut collected_zip = cpu_times.zip(second_cpu_times).enumerate(); // Gotta move it here, can't on while line. + + while let Some((itx, (past, present))) = collected_zip.next().await { + if let (Ok(past), Ok(present)) = (past, present) { + let present_times = convert_cpu_times(&present); + new_cpu_times.push(present_times); + cpu_deque.push_back(CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: calculate_cpu_usage_percentage( + convert_cpu_times(&past), + present_times, + ), + }); + } else { + new_cpu_times.push((0.0, 0.0)); + cpu_deque.push_back(CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: 0.0, + }); + } + } + + *previous_cpu_times = new_cpu_times; + cpu_deque + } else { + let (new_cpu_times, cpu_deque): (Vec<(PastCpuWork, PastCpuTotal)>, VecDeque) = + cpu_times + .collect::>() + .await + .iter() + .zip(&*previous_cpu_times) + .enumerate() + .map(|(itx, (current_cpu, (past_cpu_work, past_cpu_total)))| { + if let Ok(cpu_time) = current_cpu { + let present_times = convert_cpu_times(&cpu_time); + + ( + present_times, + CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: calculate_cpu_usage_percentage( + (*past_cpu_work, *past_cpu_total), + present_times, + ), + }, + ) + } else { + ( + (*past_cpu_work, *past_cpu_total), + CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(itx), + cpu_usage: 0.0, + }, + ) + } + }) + .unzip(); + + *previous_cpu_times = new_cpu_times; + cpu_deque + }; + + // Get average CPU if needed... and slap it at the top + if show_average_cpu { + let cpu_time = heim::cpu::time().await?; + + let (cpu_usage, new_average_cpu_time) = if let Some((past_cpu_work, past_cpu_total)) = + previous_average_cpu_time + { + let present_times = convert_cpu_times(&cpu_time); + ( + calculate_cpu_usage_percentage((*past_cpu_work, *past_cpu_total), present_times), + present_times, + ) + } else { + // Again, we need to do a quick timeout... + futures_timer::Delay::new(std::time::Duration::from_millis(100)).await; + let second_cpu_time = heim::cpu::time().await?; + + let present_times = convert_cpu_times(&second_cpu_time); + ( + calculate_cpu_usage_percentage(convert_cpu_times(&cpu_time), present_times), + present_times, + ) + }; + + *previous_average_cpu_time = Some(new_average_cpu_time); + cpu_deque.push_front(CpuData { + cpu_prefix: "AVG".to_string(), + cpu_count: None, + cpu_usage, + }) + } + + // Ok(Vec::from(cpu_deque.drain(0..3).collect::>())) // For artificially limiting the CPU results + + Ok(Vec::from(cpu_deque)) +} diff --git a/src/app/data_harvester/cpu/heim/unix.rs b/src/app/data_harvester/cpu/heim/unix.rs new file mode 100644 index 00000000..74340951 --- /dev/null +++ b/src/app/data_harvester/cpu/heim/unix.rs @@ -0,0 +1,13 @@ +//! Unix-specific functions regarding CPU usage. + +use crate::app::data_harvester::cpu::LoadAvgHarvest; + +pub async fn get_load_avg() -> crate::error::Result { + let (one, five, fifteen) = heim::cpu::os::unix::loadavg().await?; + + Ok([ + one.get::(), + five.get::(), + fifteen.get::(), + ]) +} 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.rs b/src/app/data_harvester/disks.rs deleted file mode 100644 index 103bb701..00000000 --- a/src/app/data_harvester/disks.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::app::Filter; - -#[derive(Debug, Clone, Default)] -pub struct DiskHarvest { - pub name: String, - pub mount_point: String, - pub free_space: Option, - pub used_space: Option, - pub total_space: Option, -} - -#[derive(Clone, Debug)] -pub struct IoData { - pub read_bytes: u64, - pub write_bytes: u64, -} - -pub type IoHarvest = std::collections::HashMap>; - -pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { - if !actually_get { - return Ok(None); - } - - use futures::StreamExt; - - let mut io_hash: std::collections::HashMap> = - std::collections::HashMap::new(); - - let counter_stream = heim::disk::io_counters().await?; - futures::pin_mut!(counter_stream); - - while let Some(io) = counter_stream.next().await { - if let Ok(io) = io { - let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); - - io_hash.insert( - mount_point.to_string(), - Some(IoData { - read_bytes: io.read_bytes().get::(), - write_bytes: io.write_bytes().get::(), - }), - ); - } - } - - Ok(Some(io_hash)) -} - -pub async fn get_disk_usage( - actually_get: bool, disk_filter: &Option, mount_filter: &Option, -) -> crate::utils::error::Result>> { - if !actually_get { - return Ok(None); - } - - use futures::StreamExt; - - let mut vec_disks: Vec = Vec::new(); - let partitions_stream = heim::disk::partitions_physical().await?; - futures::pin_mut!(partitions_stream); - - 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 mount_point = (partition - .mount_point() - .to_str() - .unwrap_or("Name Unavailable")) - .to_string(); - - // Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny". - // - // For implementation, we do this as follows: - // 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry. - // 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry. - // 3. Anything else is allowed. - - let filter_check_map = [(disk_filter, &name), (mount_filter, &mount_point)]; - - // This represents case 1. That is, if there is a match in an allowing list - if there is, then - // immediately allow it! - let matches_allow_list = filter_check_map.iter().any(|(filter, text)| { - if let Some(filter) = filter { - if !filter.is_list_ignored { - for r in &filter.list { - if r.is_match(text) { - return true; - } - } - } - } - false - }); - - let to_keep = if matches_allow_list { - true - } else { - // If it doesn't match an allow list, then check if it is denied. - // That is, if it matches in a reject filter, then reject. Otherwise, we always keep it. - !filter_check_map.iter().any(|(filter, text)| { - if let Some(filter) = filter { - if filter.is_list_ignored { - for r in &filter.list { - if r.is_match(text) { - return true; - } - } - } - } - false - }) - }; - - if to_keep { - // The usage line can fail in some cases (for example, if you use Void Linux + LUKS, - // see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check - // it like this instead. - if let Ok(usage) = heim::disk::usage(partition.mount_point().to_path_buf()).await { - vec_disks.push(DiskHarvest { - free_space: Some(usage.free().get::()), - used_space: Some(usage.used().get::()), - total_space: Some(usage.total().get::()), - mount_point, - name, - }); - } else { - vec_disks.push(DiskHarvest { - free_space: None, - used_space: None, - total_space: None, - mount_point, - name, - }); - } - } - } - } - - vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(Some(vec_disks)) -} 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/heim/mod.rs b/src/app/data_harvester/disks/heim/mod.rs new file mode 100644 index 00000000..a79d00db --- /dev/null +++ b/src/app/data_harvester/disks/heim/mod.rs @@ -0,0 +1,154 @@ +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, + pub mount_point: String, + pub free_space: Option, + pub used_space: Option, + pub total_space: Option, +} + +#[derive(Clone, Debug)] +pub struct IoData { + pub read_bytes: u64, + pub write_bytes: u64, +} + +pub type IoHarvest = std::collections::HashMap>; + +pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { + if !actually_get { + return Ok(None); + } + + use futures::StreamExt; + + let mut io_hash: std::collections::HashMap> = + std::collections::HashMap::new(); + + let counter_stream = heim::disk::io_counters().await?; + futures::pin_mut!(counter_stream); + + while let Some(io) = counter_stream.next().await { + if let Ok(io) = io { + let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); + + io_hash.insert( + mount_point.to_string(), + Some(IoData { + read_bytes: io.read_bytes().get::(), + write_bytes: io.write_bytes().get::(), + }), + ); + } + } + + Ok(Some(io_hash)) +} + +pub async fn get_disk_usage( + actually_get: bool, disk_filter: &Option, mount_filter: &Option, +) -> crate::utils::error::Result>> { + if !actually_get { + return Ok(None); + } + + use futures::StreamExt; + + let mut vec_disks: Vec = Vec::new(); + let partitions_stream = heim::disk::partitions_physical().await?; + futures::pin_mut!(partitions_stream); + + while let Some(part) = partitions_stream.next().await { + if let Ok(partition) = part { + let name = get_device_name(&partition); + + let mount_point = (partition + .mount_point() + .to_str() + .unwrap_or("Name Unavailable")) + .to_string(); + + // Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny". + // + // For implementation, we do this as follows: + // 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry. + // 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry. + // 3. Anything else is allowed. + + let filter_check_map = [(disk_filter, &name), (mount_filter, &mount_point)]; + + // This represents case 1. That is, if there is a match in an allowing list - if there is, then + // immediately allow it! + let matches_allow_list = filter_check_map.iter().any(|(filter, text)| { + if let Some(filter) = filter { + if !filter.is_list_ignored { + for r in &filter.list { + if r.is_match(text) { + return true; + } + } + } + } + false + }); + + let to_keep = if matches_allow_list { + true + } else { + // If it doesn't match an allow list, then check if it is denied. + // That is, if it matches in a reject filter, then reject. Otherwise, we always keep it. + !filter_check_map.iter().any(|(filter, text)| { + if let Some(filter) = filter { + if filter.is_list_ignored { + for r in &filter.list { + if r.is_match(text) { + return true; + } + } + } + } + false + }) + }; + + if to_keep { + // The usage line can fail in some cases (for example, if you use Void Linux + LUKS, + // see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check + // it like this instead. + if let Ok(usage) = heim::disk::usage(partition.mount_point().to_path_buf()).await { + vec_disks.push(DiskHarvest { + free_space: Some(usage.free().get::()), + used_space: Some(usage.used().get::()), + total_space: Some(usage.total().get::()), + mount_point, + name, + }); + } else { + vec_disks.push(DiskHarvest { + free_space: None, + used_space: None, + total_space: None, + mount_point, + name, + }); + } + } + } + } + + vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(Some(vec_disks)) +} 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/load_avg.rs b/src/app/data_harvester/load_avg.rs deleted file mode 100644 index 0f58ea8b..00000000 --- a/src/app/data_harvester/load_avg.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub type LoadAvgHarvest = [f32; 3]; - -#[cfg(target_family = "unix")] -pub async fn get_load_avg() -> crate::error::Result { - let (one, five, fifteen) = heim::cpu::os::unix::loadavg().await?; - - Ok([ - one.get::(), - five.get::(), - fifteen.get::(), - ]) -} diff --git a/src/app/data_harvester/mem.rs b/src/app/data_harvester/mem.rs deleted file mode 100644 index 014cbcdc..00000000 --- a/src/app/data_harvester/mem.rs +++ /dev/null @@ -1,52 +0,0 @@ -#[derive(Debug, Clone)] -pub struct MemHarvest { - pub mem_total_in_kib: u64, - pub mem_used_in_kib: u64, -} - -impl Default for MemHarvest { - fn default() -> Self { - MemHarvest { - mem_total_in_kib: 0, - mem_used_in_kib: 0, - } - } -} - -pub async fn get_mem_data( - actually_get: bool, -) -> ( - crate::utils::error::Result>, - crate::utils::error::Result>, -) { - use futures::join; - - if !actually_get { - (Ok(None), Ok(None)) - } else { - join!(get_ram_data(), get_swap_data()) - } -} - -pub async fn get_ram_data() -> crate::utils::error::Result> { - let memory = heim::memory::memory().await?; - - let mem_total_in_kb = memory.total().get::(); - - Ok(Some(MemHarvest { - mem_total_in_kib: mem_total_in_kb, - mem_used_in_kib: mem_total_in_kb - - memory - .available() - .get::(), - })) -} - -pub async fn get_swap_data() -> crate::utils::error::Result> { - let memory = heim::memory::swap().await?; - - Ok(Some(MemHarvest { - mem_total_in_kib: memory.total().get::(), - mem_used_in_kib: memory.used().get::(), - })) -} diff --git a/src/app/data_harvester/memory/heim.rs b/src/app/data_harvester/memory/heim.rs new file mode 100644 index 00000000..5319b1b3 --- /dev/null +++ b/src/app/data_harvester/memory/heim.rs @@ -0,0 +1,54 @@ +//! Data collection for memory via heim. + +#[derive(Debug, Clone)] +pub struct MemHarvest { + pub mem_total_in_kib: u64, + pub mem_used_in_kib: u64, +} + +impl Default for MemHarvest { + fn default() -> Self { + MemHarvest { + mem_total_in_kib: 0, + mem_used_in_kib: 0, + } + } +} + +pub async fn get_mem_data( + actually_get: bool, +) -> ( + crate::utils::error::Result>, + crate::utils::error::Result>, +) { + use futures::join; + + if !actually_get { + (Ok(None), Ok(None)) + } else { + join!(get_ram_data(), get_swap_data()) + } +} + +pub async fn get_ram_data() -> crate::utils::error::Result> { + let memory = heim::memory::memory().await?; + + let mem_total_in_kb = memory.total().get::(); + + Ok(Some(MemHarvest { + mem_total_in_kib: mem_total_in_kb, + mem_used_in_kib: mem_total_in_kb + - memory + .available() + .get::(), + })) +} + +pub async fn get_swap_data() -> crate::utils::error::Result> { + let memory = heim::memory::swap().await?; + + Ok(Some(MemHarvest { + mem_total_in_kib: memory.total().get::(), + mem_used_in_kib: memory.used().get::(), + })) +} 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.rs deleted file mode 100644 index 650a68e3..00000000 --- a/src/app/data_harvester/network.rs +++ /dev/null @@ -1,143 +0,0 @@ -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, -) -> crate::utils::error::Result> { - use futures::StreamExt; - - if !actually_get { - return Ok(None); - } - - let io_data = heim::net::io_counters().await?; - futures::pin_mut!(io_data); - let mut total_rx: u64 = 0; - let mut total_tx: u64 = 0; - - while let Some(io) = io_data.next().await { - if let Ok(io) = io { - let to_keep = if let Some(filter) = filter { - if filter.is_list_ignored { - let mut ret = true; - for r in &filter.list { - if r.is_match(&io.interface()) { - ret = false; - break; - } - } - ret - } else { - true - } - } else { - true - }; - - if to_keep { - // TODO: Use bytes as the default instead, perhaps? - // Since you might have to do a double conversion (bytes -> bits -> bytes) in some cases; - // but if you stick to bytes, then in the bytes, case, you do no conversion, and in the bits case, - // you only do one conversion... - total_rx += io.bytes_recv().get::(); - total_tx += io.bytes_sent().get::(); - } - } - } - - 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/network/heim.rs b/src/app/data_harvester/network/heim.rs new file mode 100644 index 00000000..d18287c8 --- /dev/null +++ b/src/app/data_harvester/network/heim.rs @@ -0,0 +1,71 @@ +//! Gets network data via heim. + +use super::NetworkHarvest; +use std::time::Instant; + +// FIXME: Eventually make it so that this thing also takes individual usage into account, so we can allow for showing per-interface! +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, +) -> crate::utils::error::Result> { + use futures::StreamExt; + + if !actually_get { + return Ok(None); + } + + let io_data = heim::net::io_counters().await?; + futures::pin_mut!(io_data); + let mut total_rx: u64 = 0; + let mut total_tx: u64 = 0; + + while let Some(io) = io_data.next().await { + if let Ok(io) = io { + let to_keep = if let Some(filter) = filter { + if filter.is_list_ignored { + let mut ret = true; + for r in &filter.list { + if r.is_match(&io.interface()) { + ret = false; + break; + } + } + ret + } else { + true + } + } else { + true + }; + + if to_keep { + // TODO: Use bytes as the default instead, perhaps? + // Since you might have to do a double conversion (bytes -> bits -> bytes) in some cases; + // but if you stick to bytes, then in the bytes, case, you do no conversion, and in the bits case, + // you only do one conversion... + total_rx += io.bytes_recv().get::(); + total_tx += io.bytes_sent().get::(); + } + } + } + + 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/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.rs deleted file mode 100644 index 94f6fb62..00000000 --- a/src/app/data_harvester/processes.rs +++ /dev/null @@ -1,578 +0,0 @@ -use crate::Pid; - -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, - pub total_write_bytes: u64, - pub cpu_time: u64, - pub process: Process, -} - -#[cfg(target_os = "linux")] -impl PrevProcDetails { - fn new(pid: Pid) -> error::Result { - Ok(Self { - total_read_bytes: 0, - total_write_bytes: 0, - cpu_time: 0, - process: Process::new(pid)?, - }) - } -} - -#[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)> { - use std::io::prelude::*; - use std::io::BufReader; - - // From SO answer: https://stackoverflow.com/a/23376195 - - let mut reader = BufReader::new(std::fs::File::open("/proc/stat")?); - let mut first_line = String::new(); - reader.read_line(&mut first_line)?; - - let val = first_line.split_whitespace().collect::>(); - - // SC in case that the parsing will fail due to length: - if val.len() <= 10 { - return Err(error::BottomError::InvalidIo(format!( - "CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.", - val.len() - ))); - } - - let user: f64 = val[1].parse::<_>().unwrap_or(0_f64); - let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64); - let system: f64 = val[3].parse::<_>().unwrap_or(0_f64); - let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64); - let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64); - let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64); - let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64); - let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64); - let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64); - - let idle = idle + iowait; - let non_idle = user + nice + system + irq + softirq + steal + guest; - - let total = idle + non_idle; - let prev_total = *prev_idle + *prev_non_idle; - - let total_delta: f64 = total - prev_total; - let idle_delta: f64 = idle - *prev_idle; - - *prev_idle = idle; - *prev_non_idle = non_idle; - - let result = if total_delta - idle_delta != 0_f64 { - total_delta - idle_delta - } else { - 1_f64 - }; - - let cpu_percentage = if total_delta != 0_f64 { - result / total_delta - } else { - 0_f64 - }; - - Ok((result, cpu_percentage)) -} - -/// 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, -) -> (f64, u64) { - // Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556 - let new_proc_times = stat.utime + stat.stime; - let diff = (new_proc_times - prev_proc_times) as f64; // I HATE that it's done like this but there isn't a try_from for u64 -> f64... we can accept a bit of loss in the worst case though - - if cpu_usage == 0.0 { - (0.0, new_proc_times) - } else if use_current_cpu_total { - (diff / cpu_usage * 100_f64, new_proc_times) - } else { - (diff / cpu_usage * 100_f64 * cpu_fraction, new_proc_times) - } -} - -#[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, -) -> error::Result<(ProcessHarvest, u64)> { - use std::convert::TryFrom; - - let process = &prev_proc.process; - - let (command, name) = { - let truncated_name = stat.comm.as_str(); - if let Ok(cmdline) = process.cmdline() { - if cmdline.is_empty() { - (format!("[{}]", truncated_name), truncated_name.to_string()) - } else { - ( - cmdline.join(" "), - if truncated_name.len() >= MAX_STAT_NAME_LEN { - if let Some(first_part) = cmdline.first() { - // We're only interested in the executable part... not the file path. - // That's for command. - first_part - .rsplit_once('/') - .map(|(_prefix, suffix)| suffix) - .unwrap_or(&truncated_name) - .to_string() - } else { - truncated_name.to_string() - } - } else { - truncated_name.to_string() - }, - ) - } - } else { - (truncated_name.to_string(), truncated_