Skip to main content

rigtop_core/linux/
systemd.rs

1//! systemd units in the "failed" state, from
2//! `systemctl list-units --state=failed` (no root needed), refreshed at
3//! most every [`FAILED_UNITS_REFRESH`] to keep the cost of spawning it
4//! negligible.
5
6use std::process::Command;
7use std::time::{Duration, Instant};
8
9/// How often the failed units are looked up again.
10const FAILED_UNITS_REFRESH: Duration = Duration::from_secs(30);
11
12/// Failed units of the system manager, cached between refreshes.
13#[derive(Debug, Default)]
14pub struct FailedUnitsReader {
15    last: Option<(Instant, Vec<String>)>,
16}
17
18impl FailedUnitsReader {
19    /// Creates a reader with an empty cache.
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// The failed units (e.g. `"nginx.service"`); empty without systemd.
25    pub fn read(&mut self, now: Instant) -> Vec<String> {
26        if let Some((at, units)) = &self.last
27            && now.saturating_duration_since(*at) < FAILED_UNITS_REFRESH
28        {
29            return units.clone();
30        }
31        let units = Command::new("systemctl")
32            .args([
33                "list-units",
34                "--state=failed",
35                "--plain",
36                "--no-legend",
37                "--no-pager",
38            ])
39            .output()
40            .ok()
41            .filter(|out| out.status.success())
42            .map(|out| parse(&String::from_utf8_lossy(&out.stdout)))
43            .unwrap_or_default();
44        self.last = Some((now, units.clone()));
45        units
46    }
47}
48
49/// Unit names from `systemctl list-units --plain --no-legend` output: the
50/// first word of each line.
51pub(crate) fn parse(text: &str) -> Vec<String> {
52    text.lines()
53        // Older versions mark failed units with "●" even with --plain.
54        .map(|line| line.trim_start().trim_start_matches('●'))
55        .filter_map(|line| line.split_whitespace().next())
56        .map(str::to_owned)
57        .collect()
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn parses_unit_names() {
66        let text = "nginx.service loaded failed failed A high performance web server\n\
67                    ● backup.timer  loaded failed failed Nightly backup\n\n";
68        assert_eq!(parse(text), vec!["nginx.service", "backup.timer"]);
69        assert!(parse("").is_empty());
70    }
71}