use std::collections::HashMap;
use sysinfo::{
CpuRefreshKind, DiskRefreshKind, Disks, MemoryRefreshKind, Networks, ProcessRefreshKind,
ProcessesToUpdate, System, ThreadKind, UpdateKind, Users,
};
use crate::snapshot::{
CpuStats, FilesystemUsage, HostInfo, MemoryStats, NetworkStats, ProcessInfo,
};
#[cfg(not(target_os = "linux"))]
use crate::snapshot::{DiskIo, SensorChip, Temperature};
fn disk_refresh_kind() -> DiskRefreshKind {
let kind = DiskRefreshKind::nothing().with_storage();
if cfg!(target_os = "linux") {
kind
} else {
kind.with_io_usage()
}
}
const PSEUDO_FILESYSTEMS: &[&str] = &[
"tmpfs",
"devtmpfs",
"squashfs",
"overlay",
"efivarfs",
"ramfs",
"proc",
"sysfs",
"cgroup2",
"fuse.portal",
"fuse.snapfuse",
];
pub struct SystemCollector {
system: System,
networks: Networks,
disks: Disks,
users: Users,
#[cfg(not(target_os = "linux"))]
components: sysinfo::Components,
}
impl SystemCollector {
pub fn new() -> Self {
let mut collector = Self {
system: System::new(),
networks: Networks::new_with_refreshed_list(),
disks: Disks::new_with_refreshed_list_specifics(disk_refresh_kind()),
users: Users::new_with_refreshed_list(),
#[cfg(not(target_os = "linux"))]
components: sysinfo::Components::new_with_refreshed_list(),
};
collector.refresh();
collector
}
pub fn refresh(&mut self) {
self.system
.refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage().with_frequency());
self.system
.refresh_memory_specifics(MemoryRefreshKind::everything());
self.system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing()
.with_cpu()
.with_memory()
.with_disk_usage()
.without_tasks()
.with_user(UpdateKind::OnlyIfNotSet)
.with_cmd(UpdateKind::OnlyIfNotSet),
);
self.networks.refresh(true);
self.disks.refresh_specifics(true, disk_refresh_kind());
}
pub fn host(&self) -> HostInfo {
HostInfo {
hostname: System::host_name().unwrap_or_default(),
os: System::long_os_version().unwrap_or_default(),
kernel: System::kernel_version().unwrap_or_default(),
uptime_secs: System::uptime(),
}
}
pub fn cpu(&self) -> CpuStats {
let cpus = self.system.cpus();
let load = System::load_average();
CpuStats {
model: cpus
.first()
.map(|c| c.brand().trim().to_owned())
.unwrap_or_default(),
total_percent: self.system.global_cpu_usage(),
per_core_percent: cpus.iter().map(|c| c.cpu_usage()).collect(),
per_core_mhz: cpus.iter().map(|c| c.frequency()).collect(),
load_average: cfg!(unix).then_some([load.one, load.five, load.fifteen]),
temperatures: Vec::new(),
}
}
pub fn memory(&self) -> MemoryStats {
let s = &self.system;
MemoryStats {
total_bytes: s.total_memory(),
used_bytes: s.used_memory(),
available_bytes: s.available_memory(),
free_bytes: s.free_memory(),
cached_bytes: None,
swap_total_bytes: s.total_swap(),
swap_used_bytes: s.used_swap(),
}
}
pub fn networks(&self, elapsed_secs: Option<f64>) -> Vec<NetworkStats> {
let mut networks: Vec<NetworkStats> = self
.networks
.iter()
.filter(|(name, _)| name.as_str() != "lo")
.map(|(name, data)| NetworkStats {
name: name.clone(),
rx_bytes_per_sec: rate(data.received(), elapsed_secs),
tx_bytes_per_sec: rate(data.transmitted(), elapsed_secs),
rx_total_bytes: data.total_received(),
tx_total_bytes: data.total_transmitted(),
..Default::default()
})
.collect();
networks.sort_by(|a, b| a.name.cmp(&b.name));
networks
}
pub fn filesystems(&self) -> Vec<FilesystemUsage> {
let mut by_device: HashMap<String, FilesystemUsage> = HashMap::new();
for disk in self.disks.list() {
let fs_type = disk.file_system().to_string_lossy().into_owned();
if PSEUDO_FILESYSTEMS.contains(&fs_type.as_str()) || disk.total_space() == 0 {
continue;
}
let device = disk.name().to_string_lossy().into_owned();
let mount_point = disk.mount_point().to_string_lossy().into_owned();
let key = if cfg!(windows) || device.is_empty() {
mount_point.clone()
} else {
device.clone()
};
let entry = by_device.entry(key).or_insert_with(|| FilesystemUsage {
device,
fs_type,
mount_points: Vec::new(),
total_bytes: disk.total_space(),
available_bytes: disk.available_space(),
});
entry.mount_points.push(mount_point);
}
let mut filesystems: Vec<FilesystemUsage> = by_device.into_values().collect();
for fs in &mut filesystems {
fs.mount_points.sort_by_key(|m| (m.len(), m.clone()));
}
filesystems.sort_by(|a, b| {
let key = |f: &FilesystemUsage| f.mount_points.first().cloned().unwrap_or_default();
(key(a).len(), key(a)).cmp(&(key(b).len(), key(b)))
});
filesystems
}
#[cfg(not(target_os = "linux"))]
pub fn volume_io(&self, elapsed_secs: Option<f64>) -> Vec<DiskIo> {
let mut volumes: Vec<DiskIo> = self
.disks
.list()
.iter()
.filter(|disk| disk.total_space() > 0)
.map(|disk| {
let usage = disk.usage();
let mount = disk.mount_point().to_string_lossy();
let name = mount.trim_end_matches(['\\', '/']);
DiskIo {
name: if name.is_empty() {
mount.to_string()
} else {
name.to_owned()
},
read_bytes_per_sec: rate(usage.read_bytes, elapsed_secs),
write_bytes_per_sec: rate(usage.written_bytes, elapsed_secs),
..Default::default()
}
})
.collect();
volumes.sort_by(|a, b| a.name.cmp(&b.name));
volumes.dedup_by(|a, b| a.name == b.name);
volumes
}
#[cfg(not(target_os = "linux"))]
pub fn components(&mut self) -> (Vec<SensorChip>, Vec<Temperature>) {
self.components.refresh(true);
let temperatures: Vec<Temperature> = self
.components
.list()
.iter()
.filter_map(|c| {
Some(Temperature {
key: format!("system/{}", c.label()),
label: c.label().to_owned(),
celsius: c.temperature()?,
max_celsius: c.max(),
critical_celsius: c.critical(),
})
})
.collect();
let cpu: Vec<Temperature> = temperatures
.iter()
.filter(|t| is_cpu_sensor_label(&t.label))
.cloned()
.collect();
let chips = if temperatures.is_empty() {
Vec::new()
} else {
vec![SensorChip {
key: "system".to_owned(),
name: "system".to_owned(),
temperatures,
fans: Vec::new(),
}]
};
(chips, cpu)
}
pub fn processes(&mut self, elapsed_secs: Option<f64>) -> Vec<ProcessInfo> {
let unknown_uid = self.system.processes().values().any(|p| {
p.user_id()
.is_some_and(|uid| self.users.get_user_by_id(uid).is_none())
});
if unknown_uid {
self.users.refresh();
}
self.system
.processes()
.values()
.filter(|p| p.thread_kind() != Some(ThreadKind::Userland))
.map(|p| {
let disk = p.disk_usage();
let user = p
.user_id()
.map(|uid| {
self.users
.get_user_by_id(uid)
.map(|u| u.name().to_owned())
.unwrap_or_else(|| uid.to_string())
})
.unwrap_or_default();
let command = p
.cmd()
.iter()
.map(|arg| arg.to_string_lossy())
.collect::<Vec<_>>()
.join(" ");
ProcessInfo {
pid: p.pid().as_u32(),
parent_pid: p.parent().map(|pid| pid.as_u32()),
name: p.name().to_string_lossy().into_owned(),
command,
user,
state: p.status().to_string(),
cpu_percent: p.cpu_usage(),
memory_bytes: p.memory(),
threads: None,
read_bytes_per_sec: rate(disk.read_bytes, elapsed_secs),
write_bytes_per_sec: rate(disk.written_bytes, elapsed_secs),
run_time_secs: p.run_time(),
kernel_thread: p.thread_kind() == Some(ThreadKind::Kernel),
}
})
.collect()
}
}
impl Default for SystemCollector {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
fn is_cpu_sensor_label(label: &str) -> bool {
let label = label.to_lowercase();
["cpu", "package", "tctl", "tdie", "tccd", "core "]
.iter()
.any(|word| label.contains(word))
}
fn rate(bytes: u64, elapsed_secs: Option<f64>) -> f64 {
match elapsed_secs {
Some(secs) if secs > 0.0 => bytes as f64 / secs,
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_sensor_labels() {
for label in [
"CPU",
"CPU Package",
"Tctl",
"Core 3",
"coretemp Package id 0",
] {
assert!(is_cpu_sensor_label(label), "{label}");
}
for label in ["GPU", "nvme Composite", "acpitz"] {
assert!(!is_cpu_sensor_label(label), "{label}");
}
}
}