mirror of
https://github.com/ClementTsang/bottom.git
synced 2024-11-05 02:15:05 +03:00
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:
parent
39c5ee991e
commit
6847f2ff0c
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -243,6 +243,7 @@ dependencies = [
|
||||
"battery",
|
||||
"beef",
|
||||
"cargo-husky",
|
||||
"cfg-if 1.0.0",
|
||||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
10
src/app/data_harvester/batteries/mod.rs
Normal file
10
src/app/data_harvester/batteries/mod.rs
Normal 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::*;
|
||||
}
|
||||
}
|
16
src/app/data_harvester/cpu/heim/linux.rs
Normal file
16
src/app/data_harvester/cpu/heim/linux.rs
Normal 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>(),
|
||||
)
|
||||
}
|
@ -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),
|
@ -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?;
|
||||
|
10
src/app/data_harvester/cpu/heim/windows_macos.rs
Normal file
10
src/app/data_harvester/cpu/heim/windows_macos.rs
Normal 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>(),
|
||||
)
|
||||
}
|
14
src/app/data_harvester/cpu/mod.rs
Normal file
14
src/app/data_harvester/cpu/mod.rs
Normal 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];
|
34
src/app/data_harvester/disks/heim/linux.rs
Normal file
34
src/app/data_harvester/disks/heim/linux.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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()
|
14
src/app/data_harvester/disks/heim/windows_macos.rs
Normal file
14
src/app/data_harvester/disks/heim/windows_macos.rs
Normal 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()
|
||||
}
|
||||
}
|
10
src/app/data_harvester/disks/mod.rs
Normal file
10
src/app/data_harvester/disks/mod.rs
Normal 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::*;
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
//! Data collection for memory via heim.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemHarvest {
|
||||
pub mem_total_in_kib: u64,
|
10
src/app/data_harvester/memory/mod.rs
Normal file
10
src/app/data_harvester/memory/mod.rs
Normal 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::*;
|
||||
}
|
||||
}
|
@ -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>,
|
30
src/app/data_harvester/network/mod.rs
Normal file
30
src/app/data_harvester/network/mod.rs
Normal 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;
|
||||
}
|
||||
}
|
60
src/app/data_harvester/network/sysinfo.rs
Normal file
60
src/app/data_harvester/network/sysinfo.rs
Normal 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,
|
||||
}))
|
||||
}
|
@ -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'
|
||||
}
|
||||
}
|
139
src/app/data_harvester/processes/macos.rs
Normal file
139
src/app/data_harvester/processes/macos.rs
Normal 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',
|
||||
_ => '?',
|
||||
}
|
||||
}
|
97
src/app/data_harvester/processes/mod.rs
Normal file
97
src/app/data_harvester/processes/mod.rs
Normal 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>,
|
||||
}
|
30
src/app/data_harvester/processes/unix.rs
Normal file
30
src/app/data_harvester/processes/unix.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
72
src/app/data_harvester/processes/windows.rs
Normal file
72
src/app/data_harvester/processes/windows.rs
Normal 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)
|
||||
}
|
@ -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));
|
||||
}
|
54
src/app/data_harvester/temperature/heim.rs
Normal file
54
src/app/data_harvester/temperature/heim.rs
Normal 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))
|
||||
}
|
73
src/app/data_harvester/temperature/mod.rs
Normal file
73
src/app/data_harvester/temperature/mod.rs
Normal 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));
|
||||
}
|
47
src/app/data_harvester/temperature/sysinfo.rs
Normal file
47
src/app/data_harvester/temperature/sysinfo.rs
Normal 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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user