1use std::collections::HashMap;
10
11use sysinfo::{
12 CpuRefreshKind, DiskRefreshKind, Disks, MemoryRefreshKind, Networks, ProcessRefreshKind,
13 ProcessesToUpdate, System, ThreadKind, UpdateKind, Users,
14};
15
16use crate::snapshot::{
17 CpuStats, FilesystemUsage, HostInfo, MemoryStats, NetworkStats, ProcessInfo,
18};
19#[cfg(not(target_os = "linux"))]
20use crate::snapshot::{DiskIo, SensorChip, Temperature};
21
22fn disk_refresh_kind() -> DiskRefreshKind {
25 let kind = DiskRefreshKind::nothing().with_storage();
26 if cfg!(target_os = "linux") {
27 kind
28 } else {
29 kind.with_io_usage()
30 }
31}
32
33const PSEUDO_FILESYSTEMS: &[&str] = &[
35 "tmpfs",
36 "devtmpfs",
37 "squashfs",
38 "overlay",
39 "efivarfs",
40 "ramfs",
41 "proc",
42 "sysfs",
43 "cgroup2",
44 "fuse.portal",
45 "fuse.snapfuse",
46];
47
48pub struct SystemCollector {
51 system: System,
52 networks: Networks,
53 disks: Disks,
54 users: Users,
55 #[cfg(not(target_os = "linux"))]
57 components: sysinfo::Components,
58}
59
60impl SystemCollector {
61 pub fn new() -> Self {
63 let mut collector = Self {
64 system: System::new(),
65 networks: Networks::new_with_refreshed_list(),
66 disks: Disks::new_with_refreshed_list_specifics(disk_refresh_kind()),
67 users: Users::new_with_refreshed_list(),
68 #[cfg(not(target_os = "linux"))]
69 components: sysinfo::Components::new_with_refreshed_list(),
70 };
71 collector.refresh();
72 collector
73 }
74
75 pub fn refresh(&mut self) {
77 self.system
78 .refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage().with_frequency());
79 self.system
80 .refresh_memory_specifics(MemoryRefreshKind::everything());
81 self.system.refresh_processes_specifics(
82 ProcessesToUpdate::All,
83 true,
84 ProcessRefreshKind::nothing()
85 .with_cpu()
86 .with_memory()
87 .with_disk_usage()
88 .with_tasks()
89 .with_user(UpdateKind::OnlyIfNotSet)
90 .with_cmd(UpdateKind::OnlyIfNotSet),
91 );
92 self.networks.refresh(true);
93 self.disks.refresh_specifics(true, disk_refresh_kind());
94 }
95
96 pub fn host(&self) -> HostInfo {
98 HostInfo {
99 hostname: System::host_name().unwrap_or_default(),
100 os: System::long_os_version().unwrap_or_default(),
101 kernel: System::kernel_version().unwrap_or_default(),
102 uptime_secs: System::uptime(),
103 }
104 }
105
106 pub fn cpu(&self) -> CpuStats {
108 let cpus = self.system.cpus();
109 let load = System::load_average();
110 CpuStats {
111 model: cpus
112 .first()
113 .map(|c| c.brand().trim().to_owned())
114 .unwrap_or_default(),
115 total_percent: self.system.global_cpu_usage(),
116 per_core_percent: cpus.iter().map(|c| c.cpu_usage()).collect(),
117 per_core_mhz: cpus.iter().map(|c| c.frequency()).collect(),
118 load_average: cfg!(unix).then_some([load.one, load.five, load.fifteen]),
120 temperatures: Vec::new(),
121 }
122 }
123
124 pub fn memory(&self) -> MemoryStats {
126 let s = &self.system;
127 MemoryStats {
128 total_bytes: s.total_memory(),
129 used_bytes: s.used_memory(),
130 available_bytes: s.available_memory(),
131 free_bytes: s.free_memory(),
132 cached_bytes: None,
133 swap_total_bytes: s.total_swap(),
134 swap_used_bytes: s.used_swap(),
135 }
136 }
137
138 pub fn networks(&self, elapsed_secs: Option<f64>) -> Vec<NetworkStats> {
141 let mut networks: Vec<NetworkStats> = self
142 .networks
143 .iter()
144 .filter(|(name, _)| name.as_str() != "lo")
146 .map(|(name, data)| NetworkStats {
147 name: name.clone(),
148 rx_bytes_per_sec: rate(data.received(), elapsed_secs),
149 tx_bytes_per_sec: rate(data.transmitted(), elapsed_secs),
150 rx_total_bytes: data.total_received(),
151 tx_total_bytes: data.total_transmitted(),
152 ..Default::default()
154 })
155 .collect();
156 networks.sort_by(|a, b| a.name.cmp(&b.name));
157 networks
158 }
159
160 pub fn filesystems(&self) -> Vec<FilesystemUsage> {
163 let mut by_device: HashMap<String, FilesystemUsage> = HashMap::new();
164 for disk in self.disks.list() {
165 let fs_type = disk.file_system().to_string_lossy().into_owned();
166 if PSEUDO_FILESYSTEMS.contains(&fs_type.as_str()) || disk.total_space() == 0 {
167 continue;
168 }
169 let device = disk.name().to_string_lossy().into_owned();
170 let mount_point = disk.mount_point().to_string_lossy().into_owned();
171 let key = if cfg!(windows) || device.is_empty() {
175 mount_point.clone()
176 } else {
177 device.clone()
178 };
179 let entry = by_device.entry(key).or_insert_with(|| FilesystemUsage {
180 device,
181 fs_type,
182 mount_points: Vec::new(),
183 total_bytes: disk.total_space(),
184 available_bytes: disk.available_space(),
185 });
186 entry.mount_points.push(mount_point);
187 }
188 let mut filesystems: Vec<FilesystemUsage> = by_device.into_values().collect();
189 for fs in &mut filesystems {
190 fs.mount_points.sort_by_key(|m| (m.len(), m.clone()));
191 }
192 filesystems.sort_by(|a, b| {
194 let key = |f: &FilesystemUsage| f.mount_points.first().cloned().unwrap_or_default();
195 (key(a).len(), key(a)).cmp(&(key(b).len(), key(b)))
196 });
197 filesystems
198 }
199
200 #[cfg(not(target_os = "linux"))]
203 pub fn volume_io(&self, elapsed_secs: Option<f64>) -> Vec<DiskIo> {
204 let mut volumes: Vec<DiskIo> = self
205 .disks
206 .list()
207 .iter()
208 .filter(|disk| disk.total_space() > 0)
209 .map(|disk| {
210 let usage = disk.usage();
211 let mount = disk.mount_point().to_string_lossy();
213 let name = mount.trim_end_matches(['\\', '/']);
214 DiskIo {
215 name: if name.is_empty() {
216 mount.to_string()
217 } else {
218 name.to_owned()
219 },
220 read_bytes_per_sec: rate(usage.read_bytes, elapsed_secs),
221 write_bytes_per_sec: rate(usage.written_bytes, elapsed_secs),
222 ..Default::default()
223 }
224 })
225 .collect();
226 volumes.sort_by(|a, b| a.name.cmp(&b.name));
227 volumes.dedup_by(|a, b| a.name == b.name);
228 volumes
229 }
230
231 #[cfg(not(target_os = "linux"))]
235 pub fn components(&mut self) -> (Vec<SensorChip>, Vec<Temperature>) {
236 self.components.refresh(true);
237 let temperatures: Vec<Temperature> = self
238 .components
239 .list()
240 .iter()
241 .filter_map(|c| {
242 Some(Temperature {
243 key: format!("system/{}", c.label()),
244 label: c.label().to_owned(),
245 celsius: c.temperature()?,
246 max_celsius: c.max(),
247 critical_celsius: c.critical(),
248 })
249 })
250 .collect();
251 let cpu: Vec<Temperature> = temperatures
252 .iter()
253 .filter(|t| is_cpu_sensor_label(&t.label))
254 .cloned()
255 .collect();
256 let chips = if temperatures.is_empty() {
257 Vec::new()
258 } else {
259 vec![SensorChip {
260 key: "system".to_owned(),
261 name: "system".to_owned(),
262 temperatures,
263 fans: Vec::new(),
264 }]
265 };
266 (chips, cpu)
267 }
268
269 pub fn processes(&mut self, elapsed_secs: Option<f64>) -> Vec<ProcessInfo> {
272 let unknown_uid = self.system.processes().values().any(|p| {
275 p.user_id()
276 .is_some_and(|uid| self.users.get_user_by_id(uid).is_none())
277 });
278 if unknown_uid {
279 self.users.refresh();
280 }
281
282 self.system
283 .processes()
284 .values()
285 .filter(|p| p.thread_kind() != Some(ThreadKind::Userland))
289 .map(|p| {
290 let disk = p.disk_usage();
291 let user = p
292 .user_id()
293 .map(|uid| {
294 self.users
295 .get_user_by_id(uid)
296 .map(|u| u.name().to_owned())
297 .unwrap_or_else(|| uid.to_string())
298 })
299 .unwrap_or_default();
300 let command = p
301 .cmd()
302 .iter()
303 .map(|arg| arg.to_string_lossy())
304 .collect::<Vec<_>>()
305 .join(" ");
306 ProcessInfo {
307 pid: p.pid().as_u32(),
308 parent_pid: p.parent().map(|pid| pid.as_u32()),
309 name: p.name().to_string_lossy().into_owned(),
310 command,
311 user,
312 state: p.status().to_string(),
313 cpu_percent: p.cpu_usage(),
314 memory_bytes: p.memory(),
315 threads: p.tasks().map(|t| t.len().max(1) as u32),
317 read_bytes_per_sec: rate(disk.read_bytes, elapsed_secs),
318 write_bytes_per_sec: rate(disk.written_bytes, elapsed_secs),
319 run_time_secs: p.run_time(),
320 kernel_thread: p.thread_kind() == Some(ThreadKind::Kernel),
321 }
322 })
323 .collect()
324 }
325}
326
327impl Default for SystemCollector {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333#[cfg_attr(target_os = "linux", allow(dead_code))]
336fn is_cpu_sensor_label(label: &str) -> bool {
337 let label = label.to_lowercase();
338 ["cpu", "package", "tctl", "tdie", "tccd", "core "]
339 .iter()
340 .any(|word| label.contains(word))
341}
342
343fn rate(bytes: u64, elapsed_secs: Option<f64>) -> f64 {
345 match elapsed_secs {
346 Some(secs) if secs > 0.0 => bytes as f64 / secs,
347 _ => 0.0,
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn cpu_sensor_labels() {
357 for label in [
358 "CPU",
359 "CPU Package",
360 "Tctl",
361 "Core 3",
362 "coretemp Package id 0",
363 ] {
364 assert!(is_cpu_sensor_label(label), "{label}");
365 }
366 for label in ["GPU", "nvme Composite", "acpitz"] {
367 assert!(!is_cpu_sensor_label(label), "{label}");
368 }
369 }
370}