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
This commit is contained in:
Wesley Moore 2022-07-24 10:44:29 +10:00 committed by GitHub
parent 510aa5c404
commit 577fda96fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 766 additions and 315 deletions

18
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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<T>(key: &str, data: &[u8]) -> Result<T, std::io::Error>
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()))
}

View File

@ -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<usize>,
pub cpu_usage: f64,
}
pub type CpuHarvest = Vec<CpuData>;
pub type PastCpuWork = f64;
pub type PastCpuTotal = f64;

View File

@ -18,18 +18,7 @@ cfg_if::cfg_if! {
}
}
#[derive(Default, Debug, Clone)]
pub struct CpuData {
pub cpu_prefix: String,
pub cpu_count: Option<usize>,
pub cpu_usage: f64,
}
pub type CpuHarvest = Vec<CpuData>;
pub type PastCpuWork = f64;
pub type PastCpuTotal = f64;
use crate::data_harvester::cpu::{CpuData, CpuHarvest, PastCpuTotal, PastCpuWork};
use futures::StreamExt;
use std::collections::VecDeque;

View File

@ -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<CpuHarvest> {
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<LoadAvgHarvest> {
let sys = System::new();
let LoadAvg { one, five, fifteen } = sys.load_average();
Ok([one as f32, five as f32, fifteen as f32])
}

View File

@ -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<u64>,
pub used_space: Option<u64>,
pub total_space: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct IoData {
pub read_bytes: u64,
pub write_bytes: u64,
}
pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>;

View File

@ -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<FileSystem>,
}
#[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<Option<IoHarvest>> {
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<Filter>, mount_filter: &Option<Filter>,
) -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
if !actually_get {
return Ok(None);
}
let mut vec_disks: Vec<DiskHarvest> = 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<Filter>, &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<Filter>, &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<StorageSystemInformation> {
let output = std::process::Command::new("df")
.args(&["--libxo", "json", "-k", "-t", "ufs,msdosfs,zfs"])
.output()?;
deserialize_xo("storage-system-information", &output.stdout)
}

View File

@ -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<u64>,
pub used_space: Option<u64>,
pub total_space: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct IoData {
pub read_bytes: u64,
pub write_bytes: u64,
}
pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>;
pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Option<IoHarvest>> {
if !actually_get {
return Ok(None);

View File

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

View File

@ -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<f64>,
}
pub async fn get_mem_data(
actually_get: bool,
) -> (
crate::utils::error::Result<Option<MemHarvest>>,
crate::utils::error::Result<Option<MemHarvest>>,
) {
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<Option<MemHarvest>> {
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::<u64>() {
*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::<kibibyte>(),
memory.active().get::<kibibyte>() + memory.wire().get::<kibibyte>(),
)
}
#[cfg(target_os = "windows")]
{
let memory = heim::memory::memory().await?;
use heim::units::information::kibibyte;
let mem_total_in_kib = memory.total().get::<kibibyte>();
(
mem_total_in_kib,
mem_total_in_kib - memory.available().get::<kibibyte>(),
)
}
};
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<Option<MemHarvest>> {
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::<kilobyte>(),
memory.used().get::<kilobyte>(),
)
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
use heim::units::information::kibibyte;
(
memory.total().get::<kibibyte>(),
memory.used().get::<kibibyte>(),
)
}
};
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)
},
}))
}

View File

@ -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<Option<MemHarvest>>,
crate::utils::error::Result<Option<MemHarvest>>,
) {
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<Option<MemHarvest>> {
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::<u64>() {
*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::<kibibyte>(),
memory.active().get::<kibibyte>() + memory.wire().get::<kibibyte>(),
)
}
#[cfg(target_os = "windows")]
{
let memory = heim::memory::memory().await?;
use heim::units::information::kibibyte;
let mem_total_in_kib = memory.total().get::<kibibyte>();
(
mem_total_in_kib,
mem_total_in_kib - memory.available().get::<kibibyte>(),
)
}
#[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<Option<MemHarvest>> {
#[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::<kilobyte>(),
memory.used().get::<kilobyte>(),
)
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
{
use heim::units::information::kibibyte;
(
memory.total().get::<kibibyte>(),
memory.used().get::<kibibyte>(),
)
}
#[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)
},
}))
}

View File

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

View File

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

View File

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

View File

@ -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<ProcessRow>,
}
#[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<Vec<ProcessHarvest>> {
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<std::collections::HashMap<i32, f64>> {
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<i32, D::Error>
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<f64, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}

View File

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

View File

@ -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<HashMap<i32, f64>>,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
let mut process_vector: Vec<ProcessHarvest> = 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<i32> = 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',
_ => '?',
}
}

View File

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

View File

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

View File

@ -49,6 +49,7 @@ impl From<std::io::Error> for BottomError {
}
}
#[cfg(not(target_os = "freebsd"))]
impl From<heim::Error> for BottomError {
fn from(err: heim::Error) -> Self {
BottomError::InvalidHeim(err.to_string())