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 .without_tasks()
93 .with_user(UpdateKind::OnlyIfNotSet)
94 .with_cmd(UpdateKind::OnlyIfNotSet),
95 );
96 self.networks.refresh(true);
97 self.disks.refresh_specifics(true, disk_refresh_kind());
98 }
99
100 pub fn host(&self) -> HostInfo {
102 HostInfo {
103 hostname: System::host_name().unwrap_or_default(),
104 os: System::long_os_version().unwrap_or_default(),
105 kernel: System::kernel_version().unwrap_or_default(),
106 uptime_secs: System::uptime(),
107 rigtop_version: env!("CARGO_PKG_VERSION").to_owned(),
108 }
109 }
110
111 pub fn cpu(&self) -> CpuStats {
113 let cpus = self.system.cpus();
114 let load = System::load_average();
115 CpuStats {
116 model: cpus
117 .first()
118 .map(|c| c.brand().trim().to_owned())
119 .unwrap_or_default(),
120 total_percent: self.system.global_cpu_usage(),
121 per_core_percent: cpus.iter().map(|c| c.cpu_usage()).collect(),
122 per_core_mhz: cpus.iter().map(|c| c.frequency()).collect(),
123 load_average: cfg!(unix).then_some([load.one, load.five, load.fifteen]),
125 temperatures: Vec::new(),
126 }
127 }
128
129 pub fn memory(&self) -> MemoryStats {
131 let s = &self.system;
132 let available_bytes = if cfg!(target_os = "macos") {
137 s.total_memory().saturating_sub(s.used_memory())
138 } else {
139 s.available_memory()
140 };
141 #[cfg(target_os = "macos")]
142 let cached_bytes = macos_cached_bytes();
143 #[cfg(not(target_os = "macos"))]
144 let cached_bytes = None;
145 MemoryStats {
146 total_bytes: s.total_memory(),
147 used_bytes: s.used_memory(),
148 available_bytes,
149 free_bytes: s.free_memory(),
150 cached_bytes,
151 swap_total_bytes: s.total_swap(),
152 swap_used_bytes: s.used_swap(),
153 }
154 }
155
156 pub fn networks(&self, elapsed_secs: Option<f64>) -> Vec<NetworkStats> {
159 let mut networks: Vec<NetworkStats> = self
160 .networks
161 .iter()
162 .filter(|(name, _)| name.as_str() != "lo")
164 .map(|(name, data)| NetworkStats {
165 name: name.clone(),
166 rx_bytes_per_sec: rate(data.received(), elapsed_secs),
167 tx_bytes_per_sec: rate(data.transmitted(), elapsed_secs),
168 rx_total_bytes: data.total_received(),
169 tx_total_bytes: data.total_transmitted(),
170 ..Default::default()
172 })
173 .collect();
174 networks.sort_by(|a, b| a.name.cmp(&b.name));
175 networks
176 }
177
178 pub fn filesystems(&self) -> Vec<FilesystemUsage> {
181 let mut by_device: HashMap<String, FilesystemUsage> = HashMap::new();
182 for disk in self.disks.list() {
183 let fs_type = disk.file_system().to_string_lossy().into_owned();
184 if PSEUDO_FILESYSTEMS.contains(&fs_type.as_str()) || disk.total_space() == 0 {
185 continue;
186 }
187 let device = disk.name().to_string_lossy().into_owned();
188 let mount_point = disk.mount_point().to_string_lossy().into_owned();
189 let key = if cfg!(windows) || device.is_empty() {
193 mount_point.clone()
194 } else {
195 device.clone()
196 };
197 let entry = by_device.entry(key).or_insert_with(|| FilesystemUsage {
198 device,
199 fs_type,
200 mount_points: Vec::new(),
201 total_bytes: disk.total_space(),
202 available_bytes: disk.available_space(),
203 });
204 entry.mount_points.push(mount_point);
205 }
206 let mut filesystems: Vec<FilesystemUsage> = by_device.into_values().collect();
207 for fs in &mut filesystems {
208 fs.mount_points.sort_by_key(|m| (m.len(), m.clone()));
209 }
210 filesystems.sort_by(|a, b| {
212 let key = |f: &FilesystemUsage| f.mount_points.first().cloned().unwrap_or_default();
213 (key(a).len(), key(a)).cmp(&(key(b).len(), key(b)))
214 });
215 filesystems
216 }
217
218 #[cfg(not(target_os = "linux"))]
221 pub fn volume_io(&self, elapsed_secs: Option<f64>) -> Vec<DiskIo> {
222 let mut volumes: Vec<DiskIo> = self
223 .disks
224 .list()
225 .iter()
226 .filter(|disk| disk.total_space() > 0)
227 .map(|disk| {
228 let usage = disk.usage();
229 let mount = disk.mount_point().to_string_lossy();
231 let name = mount.trim_end_matches(['\\', '/']);
232 DiskIo {
233 name: if name.is_empty() {
234 mount.to_string()
235 } else {
236 name.to_owned()
237 },
238 read_bytes_per_sec: rate(usage.read_bytes, elapsed_secs),
239 write_bytes_per_sec: rate(usage.written_bytes, elapsed_secs),
240 ..Default::default()
241 }
242 })
243 .collect();
244 volumes.sort_by(|a, b| a.name.cmp(&b.name));
245 volumes.dedup_by(|a, b| a.name == b.name);
246 volumes
247 }
248
249 #[cfg(not(target_os = "linux"))]
253 pub fn components(&mut self) -> (Vec<SensorChip>, Vec<Temperature>) {
254 self.components.refresh(true);
255 let temperatures: Vec<Temperature> = self
256 .components
257 .list()
258 .iter()
259 .filter_map(|c| {
260 Some(Temperature {
261 key: format!("system/{}", c.label()),
262 label: c.label().to_owned(),
263 celsius: c.temperature()?,
264 max_celsius: c.max(),
265 critical_celsius: c.critical(),
266 })
267 })
268 .collect();
269 let cpu: Vec<Temperature> = temperatures
270 .iter()
271 .filter(|t| is_cpu_sensor_label(&t.label))
272 .cloned()
273 .collect();
274 let chips = if temperatures.is_empty() {
275 Vec::new()
276 } else {
277 vec![SensorChip {
278 key: "system".to_owned(),
279 name: "system".to_owned(),
280 temperatures,
281 fans: Vec::new(),
282 }]
283 };
284 (chips, cpu)
285 }
286
287 pub fn processes(&mut self, elapsed_secs: Option<f64>) -> Vec<ProcessInfo> {
290 let unknown_uid = self.system.processes().values().any(|p| {
293 p.user_id()
294 .is_some_and(|uid| self.users.get_user_by_id(uid).is_none())
295 });
296 if unknown_uid {
297 self.users.refresh();
298 }
299
300 self.system
301 .processes()
302 .values()
303 .filter(|p| p.thread_kind() != Some(ThreadKind::Userland))
307 .map(|p| {
308 let disk = p.disk_usage();
309 let user = p
310 .user_id()
311 .map(|uid| {
312 self.users
313 .get_user_by_id(uid)
314 .map(|u| u.name().to_owned())
315 .unwrap_or_else(|| uid.to_string())
316 })
317 .unwrap_or_default();
318 let command = p
319 .cmd()
320 .iter()
321 .map(|arg| arg.to_string_lossy())
322 .collect::<Vec<_>>()
323 .join(" ");
324 ProcessInfo {
325 pid: p.pid().as_u32(),
326 parent_pid: p.parent().map(|pid| pid.as_u32()),
327 name: p.name().to_string_lossy().into_owned(),
328 command,
329 user,
330 state: p.status().to_string(),
331 cpu_percent: p.cpu_usage(),
332 memory_bytes: p.memory(),
333 threads: None,
336 read_bytes_per_sec: rate(disk.read_bytes, elapsed_secs),
337 write_bytes_per_sec: rate(disk.written_bytes, elapsed_secs),
338 run_time_secs: p.run_time(),
339 kernel_thread: p.thread_kind() == Some(ThreadKind::Kernel),
340 }
341 })
342 .collect()
343 }
344}
345
346impl Default for SystemCollector {
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352#[cfg_attr(target_os = "linux", allow(dead_code))]
355fn is_cpu_sensor_label(label: &str) -> bool {
356 let label = label.to_lowercase();
357 ["cpu", "package", "tctl", "tdie", "tccd", "core "]
358 .iter()
359 .any(|word| label.contains(word))
360}
361
362#[cfg(target_os = "macos")]
365fn macos_cached_bytes() -> Option<u64> {
366 use std::sync::OnceLock;
367
368 static HOST: OnceLock<libc::mach_port_t> = OnceLock::new();
370 #[allow(deprecated)]
372 let host = *HOST.get_or_init(|| unsafe { libc::mach_host_self() });
373
374 let mut stats: libc::vm_statistics64 = unsafe { std::mem::zeroed() };
376 let mut count = libc::HOST_VM_INFO64_COUNT;
377 let result = unsafe {
380 libc::host_statistics64(
381 host,
382 libc::HOST_VM_INFO64,
383 (&mut stats as *mut libc::vm_statistics64).cast(),
384 &mut count,
385 )
386 };
387 if result != libc::KERN_SUCCESS {
388 return None;
389 }
390 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
392 let page_size = u64::try_from(page_size).ok().filter(|&size| size > 0)?;
393 Some(u64::from(stats.external_page_count) * page_size)
394}
395
396fn rate(bytes: u64, elapsed_secs: Option<f64>) -> f64 {
398 match elapsed_secs {
399 Some(secs) if secs > 0.0 => bytes as f64 / secs,
400 _ => 0.0,
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn cpu_sensor_labels() {
410 for label in [
411 "CPU",
412 "CPU Package",
413 "Tctl",
414 "Core 3",
415 "coretemp Package id 0",
416 ] {
417 assert!(is_cpu_sensor_label(label), "{label}");
418 }
419 for label in ["GPU", "nvme Composite", "acpitz"] {
420 assert!(!is_cpu_sensor_label(label), "{label}");
421 }
422 }
423
424 #[cfg(target_os = "macos")]
425 #[test]
426 fn macos_memory_adds_up() {
427 let memory = SystemCollector::new().memory();
428 assert!(memory.total_bytes > 0);
429 assert!(memory.used_bytes + memory.available_bytes <= memory.total_bytes);
430 let cached = memory.cached_bytes.expect("host_statistics64 answers");
431 assert!(cached > 0 && cached < memory.total_bytes, "{cached}");
432 }
433}