Skip to main content

rigtop_core/linux/
mod.rs

1//! Linux-specific readers for `/sys` and `/proc`.
2//!
3//! These cover what `sysinfo` does not expose: hwmon sensors, RAPL energy
4//! counters, per-device disk I/O, btrfs, mdadm and ZFS state, failed
5//! systemd units, OOM kills, pending reboots, the containers processes run
6//! in, pressure stall information.
7//!
8//! Every reader takes its root directories from [`SysRoots`] instead of
9//! hard-coding `/sys` and `/proc`, so tests can point them at a fixture tree.
10
11pub mod battery;
12pub mod btrfs;
13pub mod containers;
14pub mod diskstats;
15pub mod drm;
16pub mod hwmon;
17pub mod mdstat;
18pub mod meminfo;
19pub mod mounts;
20pub mod net;
21pub mod oom;
22pub mod pressure;
23pub mod procstat;
24pub mod rapl;
25pub mod reboot;
26pub mod systemd;
27pub mod zfs;
28
29use std::fs;
30use std::path::{Path, PathBuf};
31
32/// Root directories of the kernel pseudo filesystems.
33#[derive(Debug, Clone)]
34pub struct SysRoots {
35    /// Normally `/sys`.
36    pub sys: PathBuf,
37    /// Normally `/proc`.
38    pub proc: PathBuf,
39}
40
41impl Default for SysRoots {
42    fn default() -> Self {
43        Self {
44            sys: PathBuf::from("/sys"),
45            proc: PathBuf::from("/proc"),
46        }
47    }
48}
49
50/// Reads a sysfs attribute and trims the trailing newline.
51///
52/// Returns `None` on any error: sysfs attributes may disappear at any time
53/// (hot-unplug) or be unreadable without root, and callers treat both as
54/// "not available".
55pub(crate) fn read_trimmed(path: &Path) -> Option<String> {
56    fs::read_to_string(path).ok().map(|s| s.trim().to_owned())
57}
58
59/// Reads a sysfs attribute holding a single unsigned integer.
60pub(crate) fn read_u64(path: &Path) -> Option<u64> {
61    read_trimmed(path)?.parse().ok()
62}
63
64/// Reads a sysfs attribute holding a single signed integer.
65pub(crate) fn read_i64(path: &Path) -> Option<i64> {
66    read_trimmed(path)?.parse().ok()
67}
68
69/// Returns the file names in `dir`, sorted, or an empty list if it cannot be read.
70pub(crate) fn sorted_entries(dir: &Path) -> Vec<String> {
71    let mut names: Vec<String> = match fs::read_dir(dir) {
72        Ok(entries) => entries
73            .filter_map(|e| e.ok())
74            .filter_map(|e| e.file_name().into_string().ok())
75            .collect(),
76        Err(_) => Vec::new(),
77    };
78    names.sort_by(|a, b| natural_cmp(a, b));
79    names
80}
81
82/// Compares strings so that embedded numbers sort numerically
83/// (`hwmon2` < `hwmon10`, `fan2` < `fan10`).
84pub(crate) fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
85    // Split into (non-digit prefix, trailing number). Kernel object names
86    // ("hwmon10", "nvme2", "md127") only carry a number at the end.
87    fn split(s: &str) -> (&str, Option<u64>) {
88        let digits_start = s
89            .char_indices()
90            .rev()
91            .take_while(|(_, c)| c.is_ascii_digit())
92            .last()
93            .map(|(i, _)| i)
94            .unwrap_or(s.len());
95        let (prefix, digits) = s.split_at(digits_start);
96        (prefix, digits.parse().ok())
97    }
98    let (pa, na) = split(a);
99    let (pb, nb) = split(b);
100    pa.cmp(pb).then(na.cmp(&nb)).then_with(|| a.cmp(b))
101}
102
103#[cfg(test)]
104pub(crate) mod testutil {
105    //! Helpers to build fake sysfs/procfs trees in a temporary directory.
106
107    use std::fs;
108    use std::path::Path;
109
110    /// Writes `content` to `root/rel`, creating parent directories.
111    pub fn write(root: &Path, rel: &str, content: &str) {
112        let path = root.join(rel);
113        fs::create_dir_all(path.parent().unwrap()).unwrap();
114        fs::write(path, content).unwrap();
115    }
116
117    /// Creates a symlink at `root/rel` pointing to `target` (absolute or relative).
118    pub fn symlink(root: &Path, rel: &str, target: &Path) {
119        let path = root.join(rel);
120        fs::create_dir_all(path.parent().unwrap()).unwrap();
121        std::os::unix::fs::symlink(target, path).unwrap();
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::natural_cmp;
128
129    #[test]
130    fn natural_order_sorts_numbers_numerically() {
131        let mut names = vec!["hwmon10", "hwmon2", "hwmon1", "fan"];
132        names.sort_by(|a, b| natural_cmp(a, b));
133        assert_eq!(names, vec!["fan", "hwmon1", "hwmon2", "hwmon10"]);
134    }
135}