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