From 577fda96fc30165fe613ae2131b70844ba47c3b2 Mon Sep 17 00:00:00 2001 From: Wesley Moore Date: Sun, 24 Jul 2022 10:44:29 +1000 Subject: [PATCH] Implement support for FreeBSD (#766) * WIP FreeBSD support * Implement get_cpu_data_list for FreeBSD * Implement disks for FreeBSD It doesn't work though as sysinfo doesn't make the device name available. * Use libxo to read process cpu info on FreeBSD * Populate get_io_usage with libxo too Actual I/O stats still aren't populated though as there's not an easy source for them. * Share more processes code between macos and freebsd * Extract function for deserializing libxo output on FreeBSD * Implement filtering of disks in FreeBSD * Clean up memory data collection * Update module docs --- Cargo.lock | 18 ++ Cargo.toml | 3 + src/app.rs | 3 + src/app/data_harvester.rs | 79 ++++++-- src/app/data_harvester/cpu.rs | 19 +- src/app/data_harvester/cpu/heim.rs | 13 +- src/app/data_harvester/cpu/sysinfo.rs | 45 +++++ src/app/data_harvester/disks.rs | 23 ++- src/app/data_harvester/disks/freebsd.rs | 105 +++++++++++ src/app/data_harvester/disks/heim.rs | 21 +-- src/app/data_harvester/memory.rs | 4 +- src/app/data_harvester/memory/general.rs | 163 +---------------- src/app/data_harvester/memory/general/heim.rs | 170 ++++++++++++++++++ .../data_harvester/memory/general/sysinfo.rs | 47 +++++ src/app/data_harvester/network.rs | 2 +- src/app/data_harvester/processes.rs | 5 + src/app/data_harvester/processes/freebsd.rs | 71 ++++++++ src/app/data_harvester/processes/macos.rs | 126 ++----------- .../data_harvester/processes/macos_freebsd.rs | 122 +++++++++++++ src/app/data_harvester/temperature.rs | 2 +- src/canvas/dialogs/dd_dialog.rs | 39 ++++ src/utils/error.rs | 1 + 22 files changed, 766 insertions(+), 315 deletions(-) create mode 100644 src/app/data_harvester/cpu/sysinfo.rs create mode 100644 src/app/data_harvester/disks/freebsd.rs create mode 100644 src/app/data_harvester/memory/general/heim.rs create mode 100644 src/app/data_harvester/memory/general/sysinfo.rs create mode 100644 src/app/data_harvester/processes/freebsd.rs create mode 100644 src/app/data_harvester/processes/macos_freebsd.rs diff --git a/Cargo.lock b/Cargo.lock index 1d1693ad..afa29d97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,7 @@ dependencies = [ "procfs", "regex", "serde", + "serde_json", "smol", "starship-battery", "sysinfo", @@ -1351,6 +1352,12 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1377,6 +1384,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "signal-hook" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 170bd6ea..8befb5c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,9 @@ heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory", "net"] } heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory"] } winapi = "0.3.9" +[target.'cfg(target_os = "freebsd")'.dependencies] +serde_json = { version = "1.0.82" } + [dev-dependencies] assert_cmd = "2.0.4" predicates = "2.1.1" diff --git a/src/app.rs b/src/app.rs index 9b354d43..c1a5c969 100644 --- a/src/app.rs +++ b/src/app.rs @@ -158,6 +158,9 @@ const MAX_SIGNAL: usize = 1; const MAX_SIGNAL: usize = 64; #[cfg(target_os = "macos")] const MAX_SIGNAL: usize = 31; +// https://www.freebsd.org/cgi/man.cgi?query=signal&apropos=0&sektion=3&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html +#[cfg(target_os = "freebsd")] +const MAX_SIGNAL: usize = 33; impl App { pub fn reset(&mut self) { diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 979bdadb..3342ed17 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -161,6 +161,15 @@ impl DataCollector { if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net { self.sys.refresh_networks_list(); } + + if cfg!(target_os = "freebsd") && self.widgets_to_harvest.use_cpu { + self.sys.refresh_cpu(); + } + + // Refresh disk list once... + if cfg!(target_os = "freebsd") && self.widgets_to_harvest.use_disk { + self.sys.refresh_disks_list(); + } } #[cfg(feature = "battery")] @@ -215,31 +224,54 @@ impl DataCollector { pub async fn update_data(&mut self) { #[cfg(not(target_os = "linux"))] { - if self.widgets_to_harvest.use_proc { + if self.widgets_to_harvest.use_proc || self.widgets_to_harvest.use_cpu { self.sys.refresh_cpu(); + } + if self.widgets_to_harvest.use_proc { self.sys.refresh_processes(); } if self.widgets_to_harvest.use_temp { self.sys.refresh_components(); } - if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net { self.sys.refresh_networks(); } + if cfg!(target_os = "freebsd") && self.widgets_to_harvest.use_disk { + self.sys.refresh_disks(); + } + if cfg!(target_os = "freebsd") && self.widgets_to_harvest.use_mem { + self.sys.refresh_memory(); + } } let current_instant = std::time::Instant::now(); // CPU if self.widgets_to_harvest.use_cpu { - if let Ok(cpu_data) = cpu::get_cpu_data_list( - self.show_average_cpu, - &mut self.previous_cpu_times, - &mut self.previous_average_cpu_time, - ) - .await + #[cfg(not(target_os = "freebsd"))] { - self.data.cpu = Some(cpu_data); + if let Ok(cpu_data) = cpu::get_cpu_data_list( + self.show_average_cpu, + &mut self.previous_cpu_times, + &mut self.previous_average_cpu_time, + ) + .await + { + self.data.cpu = Some(cpu_data); + } + } + #[cfg(target_os = "freebsd")] + { + if let Ok(cpu_data) = cpu::get_cpu_data_list( + &self.sys, + self.show_average_cpu, + &mut self.previous_cpu_times, + &mut self.previous_average_cpu_time, + ) + .await + { + self.data.cpu = Some(cpu_data); + } } #[cfg(target_family = "unix")] @@ -304,7 +336,7 @@ impl DataCollector { } let network_data_fut = { - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "freebsd"))] { network::get_network_data( &self.sys, @@ -316,7 +348,7 @@ impl DataCollector { &self.filters.net_filter, ) } - #[cfg(not(target_os = "windows"))] + #[cfg(not(any(target_os = "windows", target_os = "freebsd")))] { network::get_network_data( self.last_collection_time, @@ -328,7 +360,16 @@ impl DataCollector { ) } }; - let mem_data_fut = memory::get_mem_data(self.widgets_to_harvest.use_mem); + let mem_data_fut = { + #[cfg(not(target_os = "freebsd"))] + { + memory::get_mem_data(self.widgets_to_harvest.use_mem) + } + #[cfg(target_os = "freebsd")] + { + memory::get_mem_data(&self.sys, self.widgets_to_harvest.use_mem) + } + }; let disk_data_fut = disks::get_disk_usage( self.widgets_to_harvest.use_disk, &self.filters.disk_filter, @@ -397,3 +438,17 @@ impl DataCollector { self.last_collection_time = current_instant; } } + +#[cfg(target_os = "freebsd")] +/// Deserialize [libxo](https://www.freebsd.org/cgi/man.cgi?query=libxo&apropos=0&sektion=0&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html) JSON data +fn deserialize_xo(key: &str, data: &[u8]) -> Result +where + T: serde::de::DeserializeOwned, +{ + let mut value: serde_json::Value = serde_json::from_slice(data)?; + value + .as_object_mut() + .and_then(|map| map.remove(key)) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "key not found")) + .and_then(|val| serde_json::from_value(val).map_err(|err| err.into())) +} diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs index 81a0db4c..23073606 100644 --- a/src/app/data_harvester/cpu.rs +++ b/src/app/data_harvester/cpu.rs @@ -1,14 +1,29 @@ //! Data collection for CPU usage and load average. //! -//! For CPU usage, Linux, macOS, and Windows are handled by Heim. +//! For CPU usage, Linux, macOS, and Windows are handled by Heim, FreeBSD by sysinfo. //! -//! For load average, macOS and Linux are supported through Heim. +//! For load average, macOS and Linux are supported through Heim, FreeBSD by sysinfo. cfg_if::cfg_if! { if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { pub mod heim; pub use self::heim::*; + } else if #[cfg(target_os = "freebsd")] { + pub mod sysinfo; + pub use self::sysinfo::*; } } pub type LoadAvgHarvest = [f32; 3]; + +#[derive(Default, Debug, Clone)] +pub struct CpuData { + pub cpu_prefix: String, + pub cpu_count: Option, + pub cpu_usage: f64, +} + +pub type CpuHarvest = Vec; + +pub type PastCpuWork = f64; +pub type PastCpuTotal = f64; diff --git a/src/app/data_harvester/cpu/heim.rs b/src/app/data_harvester/cpu/heim.rs index 6941dd0c..be2b251f 100644 --- a/src/app/data_harvester/cpu/heim.rs +++ b/src/app/data_harvester/cpu/heim.rs @@ -18,18 +18,7 @@ cfg_if::cfg_if! { } } -#[derive(Default, Debug, Clone)] -pub struct CpuData { - pub cpu_prefix: String, - pub cpu_count: Option, - pub cpu_usage: f64, -} - -pub type CpuHarvest = Vec; - -pub type PastCpuWork = f64; -pub type PastCpuTotal = f64; - +use crate::data_harvester::cpu::{CpuData, CpuHarvest, PastCpuTotal, PastCpuWork}; use futures::StreamExt; use std::collections::VecDeque; diff --git a/src/app/data_harvester/cpu/sysinfo.rs b/src/app/data_harvester/cpu/sysinfo.rs new file mode 100644 index 00000000..3096e556 --- /dev/null +++ b/src/app/data_harvester/cpu/sysinfo.rs @@ -0,0 +1,45 @@ +//! CPU stats through sysinfo. +//! Supports FreeBSD. + +use std::collections::VecDeque; + +use sysinfo::{LoadAvg, ProcessorExt, System, SystemExt}; + +use super::{CpuData, CpuHarvest, PastCpuTotal, PastCpuWork}; +use crate::app::data_harvester::cpu::LoadAvgHarvest; + +pub async fn get_cpu_data_list( + sys: &sysinfo::System, show_average_cpu: bool, + _previous_cpu_times: &mut Vec<(PastCpuWork, PastCpuTotal)>, + _previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>, +) -> crate::error::Result { + let mut cpu_deque: VecDeque<_> = sys + .processors() + .iter() + .enumerate() + .map(|(i, cpu)| CpuData { + cpu_prefix: "CPU".to_string(), + cpu_count: Some(i), + cpu_usage: cpu.cpu_usage() as f64, + }) + .collect(); + + if show_average_cpu { + let cpu = sys.global_processor_info(); + + cpu_deque.push_front(CpuData { + cpu_prefix: "AVG".to_string(), + cpu_count: None, + cpu_usage: cpu.cpu_usage() as f64, + }) + } + + Ok(Vec::from(cpu_deque)) +} + +pub async fn get_load_avg() -> crate::error::Result { + let sys = System::new(); + let LoadAvg { one, five, fifteen } = sys.load_average(); + + Ok([one as f32, five as f32, fifteen as f32]) +} diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs index e5a52336..b1d554ef 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks.rs @@ -1,10 +1,31 @@ //! Data collection for disks (IO, usage, space, etc.). //! -//! For Linux, macOS, and Windows, this is handled by heim. +//! For Linux, macOS, and Windows, this is handled by heim. For FreeBSD there is a custom +//! implementation. cfg_if::cfg_if! { if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { pub mod heim; pub use self::heim::*; + } else if #[cfg(target_os = "freebsd")] { + pub mod freebsd; + pub use self::freebsd::*; } } + +#[derive(Debug, Clone, Default)] +pub struct DiskHarvest { + pub name: String, + pub mount_point: String, + pub free_space: Option, + pub used_space: Option, + pub total_space: Option, +} + +#[derive(Clone, Debug)] +pub struct IoData { + pub read_bytes: u64, + pub write_bytes: u64, +} + +pub type IoHarvest = std::collections::HashMap>; diff --git a/src/app/data_harvester/disks/freebsd.rs b/src/app/data_harvester/disks/freebsd.rs new file mode 100644 index 00000000..3f15dff4 --- /dev/null +++ b/src/app/data_harvester/disks/freebsd.rs @@ -0,0 +1,105 @@ +//! Disk stats for FreeBSD. + +use serde::Deserialize; +use std::io; + +use super::{DiskHarvest, IoHarvest}; +use crate::app::Filter; +use crate::data_harvester::deserialize_xo; + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "kebab-case")] +struct StorageSystemInformation { + filesystem: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +struct FileSystem { + name: String, + total_blocks: u64, + used_blocks: u64, + available_blocks: u64, + mounted_on: String, +} + +pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { + if !actually_get { + return Ok(None); + } + + let io_harvest = get_disk_info().map(|storage_system_information| { + storage_system_information + .filesystem + .into_iter() + .map(|disk| (disk.name, None)) + .collect() + })?; + Ok(Some(io_harvest)) +} + +pub async fn get_disk_usage( + actually_get: bool, disk_filter: &Option, mount_filter: &Option, +) -> crate::utils::error::Result>> { + if !actually_get { + return Ok(None); + } + + let mut vec_disks: Vec = get_disk_info().map(|storage_system_information| { + storage_system_information + .filesystem + .into_iter() + .filter_map(|disk| { + // Precedence ordering in the case where name and mount filters disagree, "allow" + // takes precedence over "deny". + // + // For implementation, we do this as follows: + // + // 1. Is the entry allowed through any filter? That is, does it match an entry in a + // filter where `is_list_ignored` is `false`? If so, we always keep this entry. + // 2. Is the entry denied through any filter? That is, does it match an entry in a + // filter where `is_list_ignored` is `true`? If so, we always deny this entry. + // 3. Anything else is allowed. + let filter_check_map = + [(disk_filter, &disk.name), (mount_filter, &disk.mounted_on)]; + if matches_allow_list(filter_check_map.as_slice()) + || !matches_ignore_list(filter_check_map.as_slice()) + { + Some(DiskHarvest { + free_space: Some(disk.available_blocks * 1024), + used_space: Some(disk.used_blocks * 1024), + total_space: Some(disk.total_blocks * 1024), + mount_point: disk.mounted_on, + name: disk.name, + }) + } else { + None + } + }) + .collect() + })?; + + vec_disks.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(Some(vec_disks)) +} + +fn matches_allow_list(filter_check_map: &[(&Option, &String)]) -> bool { + filter_check_map.iter().any(|(filter, text)| match filter { + Some(f) if !f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)), + Some(_) | None => false, + }) +} + +fn matches_ignore_list(filter_check_map: &[(&Option, &String)]) -> bool { + filter_check_map.iter().any(|(filter, text)| match filter { + Some(f) if f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)), + Some(_) | None => false, + }) +} + +fn get_disk_info() -> io::Result { + let output = std::process::Command::new("df") + .args(&["--libxo", "json", "-k", "-t", "ufs,msdosfs,zfs"]) + .output()?; + deserialize_xo("storage-system-information", &output.stdout) +} diff --git a/src/app/data_harvester/disks/heim.rs b/src/app/data_harvester/disks/heim.rs index c99ae105..85608858 100644 --- a/src/app/data_harvester/disks/heim.rs +++ b/src/app/data_harvester/disks/heim.rs @@ -1,4 +1,8 @@ +//! Disk stats through heim. +//! Supports macOS, Linux, and Windows. + use crate::app::Filter; +use crate::data_harvester::disks::{DiskHarvest, IoData, IoHarvest}; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { @@ -10,23 +14,6 @@ cfg_if::cfg_if! { } } -#[derive(Debug, Clone, Default)] -pub struct DiskHarvest { - pub name: String, - pub mount_point: String, - pub free_space: Option, - pub used_space: Option, - pub total_space: Option, -} - -#[derive(Clone, Debug)] -pub struct IoData { - pub read_bytes: u64, - pub write_bytes: u64, -} - -pub type IoHarvest = std::collections::HashMap>; - pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { if !actually_get { return Ok(None); diff --git a/src/app/data_harvester/memory.rs b/src/app/data_harvester/memory.rs index 25fccf59..bf80427f 100644 --- a/src/app/data_harvester/memory.rs +++ b/src/app/data_harvester/memory.rs @@ -1,9 +1,9 @@ //! Data collection for memory. //! -//! For Linux, macOS, and Windows, this is handled by Heim. +//! For Linux, macOS, and Windows, this is handled by Heim. On FreeBSD it is handled by sysinfo. cfg_if::cfg_if! { - if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + if #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "macos", target_os = "windows"))] { pub mod general; pub use self::general::*; } diff --git a/src/app/data_harvester/memory/general.rs b/src/app/data_harvester/memory/general.rs index 2afe119f..45fb5fdd 100644 --- a/src/app/data_harvester/memory/general.rs +++ b/src/app/data_harvester/memory/general.rs @@ -1,4 +1,12 @@ -//! Data collection for memory via heim. +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { + pub mod heim; + pub use self::heim::*; + } else if #[cfg(target_os = "freebsd")] { + pub mod sysinfo; + pub use self::sysinfo::*; + } +} #[derive(Debug, Clone, Default)] pub struct MemHarvest { @@ -6,156 +14,3 @@ pub struct MemHarvest { pub mem_used_in_kib: u64, pub use_percent: Option, } - -pub async fn get_mem_data( - actually_get: bool, -) -> ( - crate::utils::error::Result>, - crate::utils::error::Result>, -) { - use futures::join; - - if !actually_get { - (Ok(None), Ok(None)) - } else { - join!(get_ram_data(), get_swap_data()) - } -} - -pub async fn get_ram_data() -> crate::utils::error::Result> { - let (mem_total_in_kib, mem_used_in_kib) = { - #[cfg(target_os = "linux")] - { - use smol::fs::read_to_string; - let meminfo = read_to_string("/proc/meminfo").await?; - - // All values are in KiB by default. - let mut mem_total = 0; - let mut cached = 0; - let mut s_reclaimable = 0; - let mut shmem = 0; - let mut buffers = 0; - let mut mem_free = 0; - - let mut keys_read: u8 = 0; - const TOTAL_KEYS_NEEDED: u8 = 6; - - for line in meminfo.lines() { - if let Some((label, value)) = line.split_once(':') { - let to_write = match label { - "MemTotal" => &mut mem_total, - "MemFree" => &mut mem_free, - "Buffers" => &mut buffers, - "Cached" => &mut cached, - "Shmem" => &mut shmem, - "SReclaimable" => &mut s_reclaimable, - _ => { - continue; - } - }; - - if let Some((number, _unit)) = value.trim_start().split_once(' ') { - // Parse the value, remember it's in KiB! - if let Ok(number) = number.parse::() { - *to_write = number; - - // We only need a few keys, so we can bail early. - keys_read += 1; - if keys_read == TOTAL_KEYS_NEEDED { - break; - } - } - } - } - } - - // Let's preface this by saying that memory usage calculations are... not straightforward. - // There are conflicting implementations everywhere. - // - // Now that we've added this preface (mainly for future reference), the current implementation below for usage - // is based on htop's calculation formula. See - // https://github.com/htop-dev/htop/blob/976c6123f41492aaf613b9d172eef1842fb7b0a3/linux/LinuxProcessList.c#L1584 - // for implementation details as of writing. - // - // Another implementation, commonly used in other things, is to skip the shmem part of the calculation, - // which matches gopsutil and stuff like free. - - let total = mem_total; - let cached_mem = cached + s_reclaimable - shmem; - let used_diff = mem_free + cached_mem + buffers; - let used = if total >= used_diff { - total - used_diff - } else { - total - mem_free - }; - - (total, used) - } - #[cfg(target_os = "macos")] - { - let memory = heim::memory::memory().await?; - - use heim::memory::os::macos::MemoryExt; - use heim::units::information::kibibyte; - ( - memory.total().get::(), - memory.active().get::() + memory.wire().get::(), - ) - } - #[cfg(target_os = "windows")] - { - let memory = heim::memory::memory().await?; - - use heim::units::information::kibibyte; - let mem_total_in_kib = memory.total().get::(); - ( - mem_total_in_kib, - mem_total_in_kib - memory.available().get::(), - ) - } - }; - - Ok(Some(MemHarvest { - mem_total_in_kib, - mem_used_in_kib, - use_percent: if mem_total_in_kib == 0 { - None - } else { - Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) - }, - })) -} - -pub async fn get_swap_data() -> crate::utils::error::Result> { - let memory = heim::memory::swap().await?; - - let (mem_total_in_kib, mem_used_in_kib) = { - #[cfg(target_os = "linux")] - { - // Similar story to above - heim parses this information incorrectly as far as I can tell, so kilobytes = kibibytes here. - use heim::units::information::kilobyte; - ( - memory.total().get::(), - memory.used().get::(), - ) - } - #[cfg(any(target_os = "windows", target_os = "macos"))] - { - use heim::units::information::kibibyte; - ( - memory.total().get::(), - memory.used().get::(), - ) - } - }; - - Ok(Some(MemHarvest { - mem_total_in_kib, - mem_used_in_kib, - use_percent: if mem_total_in_kib == 0 { - None - } else { - Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) - }, - })) -} diff --git a/src/app/data_harvester/memory/general/heim.rs b/src/app/data_harvester/memory/general/heim.rs new file mode 100644 index 00000000..9f845b27 --- /dev/null +++ b/src/app/data_harvester/memory/general/heim.rs @@ -0,0 +1,170 @@ +//! Data collection for memory via heim. + +use crate::data_harvester::memory::MemHarvest; + +pub async fn get_mem_data( + actually_get: bool, +) -> ( + crate::utils::error::Result>, + crate::utils::error::Result>, +) { + use futures::join; + + if !actually_get { + (Ok(None), Ok(None)) + } else { + join!(get_ram_data(), get_swap_data()) + } +} + +pub async fn get_ram_data() -> crate::utils::error::Result> { + let (mem_total_in_kib, mem_used_in_kib) = { + #[cfg(target_os = "linux")] + { + use smol::fs::read_to_string; + let meminfo = read_to_string("/proc/meminfo").await?; + + // All values are in KiB by default. + let mut mem_total = 0; + let mut cached = 0; + let mut s_reclaimable = 0; + let mut shmem = 0; + let mut buffers = 0; + let mut mem_free = 0; + + let mut keys_read: u8 = 0; + const TOTAL_KEYS_NEEDED: u8 = 6; + + for line in meminfo.lines() { + if let Some((label, value)) = line.split_once(':') { + let to_write = match label { + "MemTotal" => &mut mem_total, + "MemFree" => &mut mem_free, + "Buffers" => &mut buffers, + "Cached" => &mut cached, + "Shmem" => &mut shmem, + "SReclaimable" => &mut s_reclaimable, + _ => { + continue; + } + }; + + if let Some((number, _unit)) = value.trim_start().split_once(' ') { + // Parse the value, remember it's in KiB! + if let Ok(number) = number.parse::() { + *to_write = number; + + // We only need a few keys, so we can bail early. + keys_read += 1; + if keys_read == TOTAL_KEYS_NEEDED { + break; + } + } + } + } + } + + // Let's preface this by saying that memory usage calculations are... not straightforward. + // There are conflicting implementations everywhere. + // + // Now that we've added this preface (mainly for future reference), the current implementation below for usage + // is based on htop's calculation formula. See + // https://github.com/htop-dev/htop/blob/976c6123f41492aaf613b9d172eef1842fb7b0a3/linux/LinuxProcessList.c#L1584 + // for implementation details as of writing. + // + // Another implementation, commonly used in other things, is to skip the shmem part of the calculation, + // which matches gopsutil and stuff like free. + + let total = mem_total; + let cached_mem = cached + s_reclaimable - shmem; + let used_diff = mem_free + cached_mem + buffers; + let used = if total >= used_diff { + total - used_diff + } else { + total - mem_free + }; + + (total, used) + } + #[cfg(target_os = "macos")] + { + let memory = heim::memory::memory().await?; + + use heim::memory::os::macos::MemoryExt; + use heim::units::information::kibibyte; + ( + memory.total().get::(), + memory.active().get::() + memory.wire().get::(), + ) + } + #[cfg(target_os = "windows")] + { + let memory = heim::memory::memory().await?; + + use heim::units::information::kibibyte; + let mem_total_in_kib = memory.total().get::(); + ( + mem_total_in_kib, + mem_total_in_kib - memory.available().get::(), + ) + } + #[cfg(target_os = "freebsd")] + { + let mut s = System::new(); + s.refresh_memory(); + (s.total_memory(), s.used_memory()) + } + }; + + Ok(Some(MemHarvest { + mem_total_in_kib, + mem_used_in_kib, + use_percent: if mem_total_in_kib == 0 { + None + } else { + Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) + }, + })) +} + +pub async fn get_swap_data() -> crate::utils::error::Result> { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + let memory = heim::memory::swap().await?; + #[cfg(target_os = "freebsd")] + let mut memory = System::new(); + + let (mem_total_in_kib, mem_used_in_kib) = { + #[cfg(target_os = "linux")] + { + // Similar story to above - heim parses this information incorrectly as far as I can tell, so kilobytes = kibibytes here. + use heim::units::information::kilobyte; + ( + memory.total().get::(), + memory.used().get::(), + ) + } + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + use heim::units::information::kibibyte; + ( + memory.total().get::(), + memory.used().get::(), + ) + } + #[cfg(target_os = "freebsd")] + { + memory.refresh_memory(); + (memory.total_swap(), memory.used_swap()) + } + }; + + Ok(Some(MemHarvest { + mem_total_in_kib, + mem_used_in_kib, + use_percent: if mem_total_in_kib == 0 { + None + } else { + Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) + }, + })) +} diff --git a/src/app/data_harvester/memory/general/sysinfo.rs b/src/app/data_harvester/memory/general/sysinfo.rs new file mode 100644 index 00000000..8ff3bcab --- /dev/null +++ b/src/app/data_harvester/memory/general/sysinfo.rs @@ -0,0 +1,47 @@ +//! Data collection for memory via sysinfo. + +use crate::data_harvester::memory::MemHarvest; +use sysinfo::{System, SystemExt}; + +pub async fn get_mem_data( + sys: &System, actually_get: bool, +) -> ( + crate::utils::error::Result>, + crate::utils::error::Result>, +) { + use futures::join; + + if !actually_get { + (Ok(None), Ok(None)) + } else { + join!(get_ram_data(sys), get_swap_data(sys)) + } +} + +pub async fn get_ram_data(sys: &System) -> crate::utils::error::Result> { + let (mem_total_in_kib, mem_used_in_kib) = (sys.total_memory(), sys.used_memory()); + + Ok(Some(MemHarvest { + mem_total_in_kib, + mem_used_in_kib, + use_percent: if mem_total_in_kib == 0 { + None + } else { + Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) + }, + })) +} + +pub async fn get_swap_data(sys: &System) -> crate::utils::error::Result> { + let (mem_total_in_kib, mem_used_in_kib) = (sys.total_swap(), sys.used_swap()); + + Ok(Some(MemHarvest { + mem_total_in_kib, + mem_used_in_kib, + use_percent: if mem_total_in_kib == 0 { + None + } else { + Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) + }, + })) +} diff --git a/src/app/data_harvester/network.rs b/src/app/data_harvester/network.rs index c717e6ac..3497ceca 100644 --- a/src/app/data_harvester/network.rs +++ b/src/app/data_harvester/network.rs @@ -7,7 +7,7 @@ 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")] { + } else if #[cfg(any(target_os = "freebsd", target_os = "windows"))] { pub mod sysinfo; pub use self::sysinfo::*; } diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index 40662d7a..347b46c1 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -9,10 +9,15 @@ cfg_if::cfg_if! { pub use self::linux::*; } else if #[cfg(target_os = "macos")] { pub mod macos; + mod macos_freebsd; pub use self::macos::*; } else if #[cfg(target_os = "windows")] { pub mod windows; pub use self::windows::*; + } else if #[cfg(target_os = "freebsd")] { + pub mod freebsd; + mod macos_freebsd; + pub use self::freebsd::*; } } diff --git a/src/app/data_harvester/processes/freebsd.rs b/src/app/data_harvester/processes/freebsd.rs new file mode 100644 index 00000000..d050e5aa --- /dev/null +++ b/src/app/data_harvester/processes/freebsd.rs @@ -0,0 +1,71 @@ +//! Process data collection for FreeBSD. Uses sysinfo. + +use serde::{Deserialize, Deserializer}; +use std::io; + +use super::ProcessHarvest; +use sysinfo::System; + +use crate::data_harvester::deserialize_xo; +use crate::data_harvester::processes::UserTable; + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "kebab-case")] +struct ProcessInformation { + process: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +struct ProcessRow { + #[serde(deserialize_with = "pid")] + pid: i32, + #[serde(deserialize_with = "percent_cpu")] + percent_cpu: f64, +} + +pub fn get_process_data( + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, +) -> crate::utils::error::Result> { + super::macos_freebsd::get_process_data( + sys, + use_current_cpu_total, + mem_total_kb, + user_table, + get_freebsd_process_cpu_usage, + ) +} + +fn get_freebsd_process_cpu_usage(pids: &[i32]) -> io::Result> { + if pids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + + let output = std::process::Command::new("ps") + .args(&["--libxo", "json", "-o", "pid,pcpu", "-p"]) + .args(pids.iter().map(i32::to_string)) + .output()?; + deserialize_xo("process-information", &output.stdout).map(|process_info: ProcessInformation| { + process_info + .process + .into_iter() + .map(|row| (row.pid, row.percent_cpu)) + .collect() + }) +} + +fn pid<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) +} + +fn percent_cpu<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) +} diff --git a/src/app/data_harvester/processes/macos.rs b/src/app/data_harvester/processes/macos.rs index 0c401b0a..c9a3f601 100644 --- a/src/app/data_harvester/processes/macos.rs +++ b/src/app/data_harvester/processes/macos.rs @@ -1,10 +1,22 @@ //! Process data collection for macOS. Uses sysinfo. use super::ProcessHarvest; -use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt}; +use sysinfo::System; use crate::data_harvester::processes::UserTable; +pub fn get_process_data( + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, +) -> crate::utils::error::Result> { + super::macos_freebsd::get_process_data( + sys, + use_current_cpu_total, + mem_total_kb, + user_table, + get_macos_process_cpu_usage, + ) +} + fn get_macos_process_cpu_usage( pids: &[i32], ) -> std::io::Result> { @@ -35,115 +47,3 @@ fn get_macos_process_cpu_usage( }); Ok(result) } - -pub fn get_process_data( - sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, -) -> crate::utils::error::Result> { - let mut process_vector: Vec = Vec::new(); - let process_hashmap = sys.processes(); - let cpu_usage = sys.global_processor_info().cpu_usage() as f64 / 100.0; - let num_processors = sys.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_processors; - 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(); - let process_state = { - let ps = process_val.status(); - (ps.to_string(), convert_process_status_to_char(ps)) - }; - let uid = process_val.uid; - process_vector.push(ProcessHarvest { - pid: process_val.pid().as_u32() as _, - parent_pid: process_val.parent().map(|p| p.as_u32() as _), - 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, - uid, - user: user_table - .get_uid_to_username_mapping(uid) - .map(Into::into) - .unwrap_or_else(|_| "N/A".into()), - }); - } - - let unknown_state = ProcessStatus::Unknown(0).to_string(); - let cpu_usage_unknown_pids: Vec = process_vector - .iter() - .filter(|process| process.process_state.0 == 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_processors == 0.0 { - *cpu_usages.get(&process.pid).unwrap() - } else { - *cpu_usages.get(&process.pid).unwrap() / num_processors - }; - } - } - - Ok(process_vector) -} - -fn convert_process_status_to_char(status: ProcessStatus) -> char { - match status { - ProcessStatus::Run => 'R', - ProcessStatus::Sleep => 'S', - ProcessStatus::Idle => 'D', - ProcessStatus::Zombie => 'Z', - _ => '?', - } -} diff --git a/src/app/data_harvester/processes/macos_freebsd.rs b/src/app/data_harvester/processes/macos_freebsd.rs new file mode 100644 index 00000000..c50bc74d --- /dev/null +++ b/src/app/data_harvester/processes/macos_freebsd.rs @@ -0,0 +1,122 @@ +//! Shared process data harvesting code from macOS and FreeBSD via sysinfo. + +use std::collections::HashMap; +use std::io; + +use super::ProcessHarvest; +use sysinfo::{PidExt, ProcessExt, ProcessStatus, ProcessorExt, System, SystemExt}; + +use crate::data_harvester::processes::UserTable; + +pub fn get_process_data( + sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, + get_process_cpu_usage: impl Fn(&[i32]) -> io::Result>, +) -> crate::utils::error::Result> { + let mut process_vector: Vec = Vec::new(); + let process_hashmap = sys.processes(); + let cpu_usage = sys.global_processor_info().cpu_usage() as f64 / 100.0; + let num_processors = sys.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_processors; + 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(); + let process_state = { + let ps = process_val.status(); + (ps.to_string(), convert_process_status_to_char(ps)) + }; + let uid = process_val.uid; + process_vector.push(ProcessHarvest { + pid: process_val.pid().as_u32() as _, + parent_pid: process_val.parent().map(|p| p.as_u32() as _), + 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, + uid, + user: user_table + .get_uid_to_username_mapping(uid) + .map(Into::into) + .unwrap_or_else(|_| "N/A".into()), + }); + } + + let unknown_state = ProcessStatus::Unknown(0).to_string(); + let cpu_usage_unknown_pids: Vec = process_vector + .iter() + .filter(|process| process.process_state.0 == unknown_state) + .map(|process| process.pid) + .collect(); + let cpu_usages = get_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_processors == 0.0 { + *cpu_usages.get(&process.pid).unwrap() + } else { + *cpu_usages.get(&process.pid).unwrap() / num_processors + }; + } + } + + Ok(process_vector) +} + +fn convert_process_status_to_char(status: ProcessStatus) -> char { + match status { + ProcessStatus::Run => 'R', + ProcessStatus::Sleep => 'S', + ProcessStatus::Idle => 'D', + ProcessStatus::Zombie => 'Z', + _ => '?', + } +} diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs index 7d5d247d..1b64112c 100644 --- a/src/app/data_harvester/temperature.rs +++ b/src/app/data_harvester/temperature.rs @@ -7,7 +7,7 @@ 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"))] { + } else if #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "windows"))] { pub mod sysinfo; pub use self::sysinfo::*; } diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index 1ee4282b..1914ddec 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -239,6 +239,45 @@ impl Painter { "31: USR2", ]; } + #[cfg(target_os = "freebsd")] + { + signal_text = vec![ + "0: Cancel", + "1: HUP", + "2: INT", + "3: QUIT", + "4: ILL", + "5: TRAP", + "6: ABRT", + "7: EMT", + "8: FPE", + "9: KILL", + "10: BUS", + "11: SEGV", + "12: SYS", + "13: PIPE", + "14: ALRM", + "15: TERM", + "16: URG", + "17: STOP", + "18: TSTP", + "19: CONT", + "20: CHLD", + "21: TTIN", + "22: TTOU", + "23: IO", + "24: XCPU", + "25: XFSZ", + "26: VTALRM", + "27: PROF", + "28: WINCH", + "29: INFO", + "30: USR1", + "31: USR2", + "32: THR", + "33: LIBRT", + ]; + } let button_rect = Layout::default() .direction(Direction::Horizontal) diff --git a/src/utils/error.rs b/src/utils/error.rs index e265ad3e..6a7a2cfe 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -49,6 +49,7 @@ impl From for BottomError { } } +#[cfg(not(target_os = "freebsd"))] impl From for BottomError { fn from(err: heim::Error) -> Self { BottomError::InvalidHeim(err.to_string())