Skip to main content

monitrs_core/model/
capability.rs

1//! Per-metric platform capability.
2//!
3//! §4: *do not represent platform support as one global boolean.* This snapshot
4//! is what the Inspect screen renders under "unavailable metrics and why" (§7.5)
5//! and what the layout engine consults before reserving space for an optional
6//! panel.
7//!
8//! Capabilities are a fixed struct rather than a map so that a snapshot costs no
9//! allocation and adding a capability is a compile error at every match site.
10
11/// Whether one capability is usable on this system.
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
15pub enum CapabilityState {
16    /// Present and readable.
17    Available,
18    /// This platform or kernel does not provide it.
19    Unsupported,
20    /// Present but the OS refuses the read at our privilege level.
21    ///
22    /// Distinct from `Unsupported` because §4 requires a help hint suggesting
23    /// what elevated privileges would provide.
24    PermissionDenied,
25    /// Not probed yet.
26    #[default]
27    Unknown,
28}
29
30impl CapabilityState {
31    /// Lower-case label for the Inspect screen.
32    #[must_use]
33    pub const fn label(self) -> &'static str {
34        match self {
35            Self::Available => "available",
36            Self::Unsupported => "unsupported",
37            Self::PermissionDenied => "permission denied",
38            Self::Unknown => "not probed",
39        }
40    }
41
42    /// A redundant non-color cue (§5.2).
43    #[must_use]
44    pub const fn symbol(self) -> char {
45        match self {
46            Self::Available => '+',
47            Self::Unsupported => '-',
48            Self::PermissionDenied => '!',
49            Self::Unknown => '?',
50        }
51    }
52
53    /// Whether elevated privileges would plausibly help.
54    ///
55    /// Drives the help hint §4 requires. Note that §15.1 forbids monitrs from
56    /// escalating on its own; this only informs the user.
57    #[must_use]
58    pub const fn privileges_might_help(self) -> bool {
59        matches!(self, Self::PermissionDenied)
60    }
61}
62
63/// Every capability the UI branches on.
64#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub struct CapabilitySnapshot {
67    /// Per-process read/write byte counters.
68    pub per_process_io: CapabilityState,
69    /// Per-process thread counts.
70    pub per_process_threads: CapabilityState,
71    /// Per-process open file descriptor counts.
72    pub per_process_open_files: CapabilityState,
73    /// Per-process socket counts.
74    pub per_process_sockets: CapabilityState,
75    /// Per-process working directory.
76    pub per_process_working_directory: CapabilityState,
77    /// Per-logical-CPU utilization.
78    pub per_core_cpu: CapabilityState,
79    /// The user/system/idle CPU time split.
80    pub cpu_breakdown: CapabilityState,
81    /// Load averages.
82    pub load_average: CapabilityState,
83    /// Swap-in and swap-out rates, as opposed to swap capacity.
84    pub swap_activity: CapabilityState,
85    /// Block-device throughput counters.
86    pub disk_io: CapabilityState,
87    /// Block-device busy percentage (§7.3).
88    pub disk_busy: CapabilityState,
89    /// Filesystem capacity.
90    pub filesystem_capacity: CapabilityState,
91    /// Interface byte and packet counters.
92    pub network_counters: CapabilityState,
93    /// Negotiated link speed, without which no utilization is shown (§7.4).
94    pub network_link_speed: CapabilityState,
95    /// Interface error and drop counters.
96    pub network_errors: CapabilityState,
97    /// Temperature sensors.
98    pub temperatures: CapabilityState,
99    /// Battery.
100    pub battery: CapabilityState,
101    /// Linux `/proc/pressure/*`.
102    pub linux_psi: CapabilityState,
103    /// cgroup limits, exposed separately from host totals (§9.2).
104    pub cgroup_limits: CapabilityState,
105    /// Whether kernel threads are distinguishable, so they can be hidden (§7.2).
106    pub kernel_threads: CapabilityState,
107    /// Whether signals can be sent at all.
108    pub process_signals: CapabilityState,
109    /// Whether renice is available (§6.2).
110    pub renice: CapabilityState,
111}
112
113impl CapabilitySnapshot {
114    /// The number of capabilities tracked.
115    pub const COUNT: usize = 22;
116
117    /// Every capability paired with its display label, for the Inspect screen.
118    ///
119    /// The order is stable so the panel does not reshuffle between frames.
120    #[must_use]
121    pub fn entries(&self) -> [(&'static str, CapabilityState); Self::COUNT] {
122        [
123            ("process I/O", self.per_process_io),
124            ("process threads", self.per_process_threads),
125            ("process open files", self.per_process_open_files),
126            ("process sockets", self.per_process_sockets),
127            (
128                "process working directory",
129                self.per_process_working_directory,
130            ),
131            ("per-core CPU", self.per_core_cpu),
132            ("CPU time breakdown", self.cpu_breakdown),
133            ("load average", self.load_average),
134            ("swap activity", self.swap_activity),
135            ("disk I/O", self.disk_io),
136            ("disk busy", self.disk_busy),
137            ("filesystem capacity", self.filesystem_capacity),
138            ("network counters", self.network_counters),
139            ("network link speed", self.network_link_speed),
140            ("network errors", self.network_errors),
141            ("temperatures", self.temperatures),
142            ("battery", self.battery),
143            ("Linux PSI", self.linux_psi),
144            ("cgroup limits", self.cgroup_limits),
145            ("kernel threads", self.kernel_threads),
146            ("process signals", self.process_signals),
147            ("renice", self.renice),
148        ]
149    }
150
151    /// Capabilities that are missing, with the reason, for the diagnostics
152    /// subsection §7.5 requires.
153    #[must_use]
154    pub fn unavailable(&self) -> Vec<(&'static str, CapabilityState)> {
155        self.entries()
156            .into_iter()
157            .filter(|(_, state)| *state != CapabilityState::Available)
158            .collect()
159    }
160
161    /// Whether any capability is denied rather than merely absent, so the UI can
162    /// show one privilege hint instead of one per metric.
163    #[must_use]
164    pub fn any_permission_denied(&self) -> bool {
165        self.entries()
166            .into_iter()
167            .any(|(_, state)| state.privileges_might_help())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn entries_covers_every_declared_capability() {
177        let capabilities = CapabilitySnapshot::default();
178        assert_eq!(capabilities.entries().len(), CapabilitySnapshot::COUNT);
179    }
180
181    #[test]
182    fn entry_labels_are_unique_and_ascii() {
183        let entries = CapabilitySnapshot::default().entries();
184        let mut labels: Vec<&str> = entries.iter().map(|(label, _)| *label).collect();
185        assert!(labels.iter().all(|label| label.is_ascii()));
186        labels.sort_unstable();
187        labels.dedup();
188        assert_eq!(
189            labels.len(),
190            CapabilitySnapshot::COUNT,
191            "duplicate capability label"
192        );
193    }
194
195    #[test]
196    fn an_unprobed_snapshot_reports_everything_as_unavailable() {
197        let capabilities = CapabilitySnapshot::default();
198        assert_eq!(capabilities.unavailable().len(), CapabilitySnapshot::COUNT);
199        assert!(
200            !capabilities.any_permission_denied(),
201            "unknown is not denied"
202        );
203    }
204
205    #[test]
206    fn permission_denied_is_distinct_from_unsupported() {
207        let capabilities = CapabilitySnapshot {
208            per_process_io: CapabilityState::PermissionDenied,
209            linux_psi: CapabilityState::Unsupported,
210            ..CapabilitySnapshot::default()
211        };
212
213        assert!(capabilities.any_permission_denied());
214        assert!(CapabilityState::PermissionDenied.privileges_might_help());
215        assert!(
216            !CapabilityState::Unsupported.privileges_might_help(),
217            "root cannot conjure a kernel feature that does not exist"
218        );
219    }
220
221    #[test]
222    fn available_capabilities_drop_out_of_the_unavailable_list() {
223        let capabilities = CapabilitySnapshot {
224            per_process_io: CapabilityState::Available,
225            ..CapabilitySnapshot::default()
226        };
227        let unavailable = capabilities.unavailable();
228        assert_eq!(unavailable.len(), CapabilitySnapshot::COUNT - 1);
229        assert!(!unavailable.iter().any(|(label, _)| *label == "process I/O"));
230    }
231
232    #[test]
233    fn state_symbols_are_distinguishable_without_color() {
234        let mut symbols: Vec<char> = [
235            CapabilityState::Available,
236            CapabilityState::Unsupported,
237            CapabilityState::PermissionDenied,
238            CapabilityState::Unknown,
239        ]
240        .iter()
241        .map(|state| state.symbol())
242        .collect();
243        symbols.sort_unstable();
244        symbols.dedup();
245        assert_eq!(symbols.len(), 4);
246    }
247}