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.
This commit is contained in:
Clement Tsang 2021-05-15 18:57:02 -07:00 committed by GitHub
parent 39c5ee991e
commit 6847f2ff0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 797 additions and 608 deletions

1
Cargo.lock generated
View file

@ -243,6 +243,7 @@ dependencies = [
"battery",
"beef",
"cargo-husky",
"cfg-if 1.0.0",
"chrono",
"clap",
"crossterm",

View file

@ -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"

View file

@ -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<Instant>,
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<processes::ProcessHarvest>,
pub disk_harvest: Vec<disks::DiskHarvest>,
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;

View file

@ -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<cpu::CpuHarvest>,
pub load_avg: Option<load_avg::LoadAvgHarvest>,
pub memory: Option<mem::MemHarvest>,
pub swap: Option<mem::MemHarvest>,
pub load_avg: Option<cpu::LoadAvgHarvest>,
pub memory: Option<memory::MemHarvest>,
pub swap: Option<memory::MemHarvest>,
pub temperature_sensors: Option<Vec<temperature::TempHarvest>>,
pub network: Option<network::NetworkHarvest>,
pub list_of_processes: Option<Vec<processes::ProcessHarvest>>,
@ -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,

View file

@ -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,

View file

@ -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::*;
}
}

View file

@ -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::<heim::units::time::second>();
(
working_time,
working_time + (cpu_time.idle() + cpu_time.io_wait()).get::<heim::units::time::second>(),
)
}

View file

@ -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<CpuData>;
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<CpuHarvest> {
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::<heim::units::time::second>();
(
working_time,
working_time + cpu_time.idle().get::<heim::units::time::second>(),
)
}
#[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::<heim::units::time::second>();
(
working_time,
working_time
+ (cpu_time.idle() + cpu_time.io_wait()).get::<heim::units::time::second>(),
)
}
}
fn calculate_cpu_usage_percentage(
(previous_working_time, previous_total_time): (f64, f64),
(current_working_time, current_total_time): (f64, f64),

View file

@ -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<LoadAvgHarvest> {
let (one, five, fifteen) = heim::cpu::os::unix::loadavg().await?;

View file

@ -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::<heim::units::time::second>();
(
working_time,
working_time + cpu_time.idle().get::<heim::units::time::second>(),
)
}

View file

@ -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];

View file

@ -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()
}
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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::*;
}
}

View file

@ -1,3 +1,5 @@
//! Data collection for memory via heim.
#[derive(Debug, Clone)]
pub struct MemHarvest {
pub mem_total_in_kib: u64,

View file

@ -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::*;
}
}

View file

@ -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::app::Filter>,
) -> crate::utils::error::Result<Option<NetworkHarvest>> {
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::app::Filter>,

View file

@ -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;
}
}

View file

@ -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::app::Filter>,
) -> crate::utils::error::Result<Option<NetworkHarvest>> {
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,
}))
}

View file

@ -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/<PID>/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<Pid>, // 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<libc::uid_t>,
}
#[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<Self> {
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<libc::uid_t, String>,
}
#[cfg(target_family = "unix")]
impl UserTable {
pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result<String> {
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<std::collections::HashMap<i32, f64>> {
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::<String>(),
)
.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<Pid, PrevProcDetails>, 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<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = 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<i32> = 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'
}
}

View file

@ -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<std::collections::HashMap<i32, f64>> {
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::<String>(),
)
.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<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = 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<i32> = 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',
_ => '?',
}
}

View file

@ -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<Pid>, // 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<libc::uid_t>,
}

View file

@ -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<libc::uid_t, String>,
}
impl UserTable {
pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result<String> {
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)
}
}
}

View file

@ -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<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = 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)
}

View file

@ -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<Filter>, 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<Filter>,
) -> crate::utils::error::Result<Option<Vec<TempHarvest>>> {
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<TempHarvest> = 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<Filter>,
) -> crate::utils::error::Result<Option<Vec<TempHarvest>>> {
use futures::StreamExt;
use heim::units::thermodynamic_temperature;
if !actually_get {
return Ok(None);
}
let mut temperature_vec: Vec<TempHarvest> = 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::<thermodynamic_temperature::degree_celsius>(
),
TemperatureType::Kelvin => {
sensor.current().get::<thermodynamic_temperature::kelvin>()
}
TemperatureType::Fahrenheit => sensor
.current()
.get::<thermodynamic_temperature::degree_fahrenheit>(
),
},
});
}
}
}
temp_vec_sort(&mut temperature_vec);
Ok(Some(temperature_vec))
}
fn temp_vec_sort(temperature_vec: &mut Vec<TempHarvest>) {
// 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));
}

View file

@ -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<Filter>,
) -> crate::utils::error::Result<Option<Vec<TempHarvest>>> {
use futures::StreamExt;
use heim::units::thermodynamic_temperature;
if !actually_get {
return Ok(None);
}
let mut temperature_vec: Vec<TempHarvest> = 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::<thermodynamic_temperature::degree_celsius>(
),
TemperatureType::Kelvin => {
sensor.current().get::<thermodynamic_temperature::kelvin>()
}
TemperatureType::Fahrenheit => sensor
.current()
.get::<thermodynamic_temperature::degree_fahrenheit>(
),
},
});
}
}
}
temp_vec_sort(&mut temperature_vec);
Ok(Some(temperature_vec))
}

View file

@ -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<Filter>, 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<TempHarvest>) {
// 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));
}

View file

@ -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<Filter>,
) -> crate::utils::error::Result<Option<Vec<TempHarvest>>> {
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<TempHarvest> = 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))
}