sesame/doctor/
daemon.rs

1//! Daemon health checks — running state, uptime, memory, restarts.
2
3use super::{Check, Status};
4
5/// All open-sesame systemd user units to check.
6const DAEMONS: &[(&str, &str)] = &[
7    ("profile", "open-sesame-profile.service"),
8    ("secrets", "open-sesame-secrets.service"),
9    ("wm", "open-sesame-wm.service"),
10    ("launcher", "open-sesame-launcher.service"),
11    ("clipboard", "open-sesame-clipboard.service"),
12    ("input", "open-sesame-input.service"),
13    ("snippets", "open-sesame-snippets.service"),
14];
15
16pub fn checks() -> Vec<Check> {
17    let mut results = Vec::new();
18
19    for &(name, unit) in DAEMONS {
20        let active = systemctl_prop(unit, "ActiveState");
21        let pid = systemctl_prop(unit, "MainPID");
22        let restarts = systemctl_prop(unit, "NRestarts");
23        let memory_max = systemctl_prop(unit, "MemoryMax");
24
25        // Running check.
26        let is_active = active.as_deref() == Some("active");
27        results.push(Check {
28            id: format!("daemon.{name}.running"),
29            category: "daemon",
30            status: if is_active {
31                Status::Pass
32            } else {
33                Status::Fail
34            },
35            value: active.clone().unwrap_or_else(|| "unknown".into()),
36            description: if is_active {
37                String::new()
38            } else {
39                format!("systemctl --user status {unit}")
40            },
41        });
42
43        // Skip remaining checks if not running.
44        let Some(pid_str) = &pid else { continue };
45        let pid_num: u32 = match pid_str.parse() {
46            Ok(n) if n > 0 => n,
47            _ => continue,
48        };
49
50        // Memory check.
51        let vmrss_kb = read_proc_status(pid_num, "VmRSS");
52        let vmrss_mb = vmrss_kb.unwrap_or(0) / 1024;
53        let max_bytes: u64 = memory_max
54            .as_deref()
55            .and_then(|s| s.parse().ok())
56            .unwrap_or(u64::MAX);
57        let max_mb = if max_bytes == u64::MAX {
58            "∞".to_string()
59        } else {
60            format!("{} MB", max_bytes / (1024 * 1024))
61        };
62
63        let mem_status = if max_bytes != u64::MAX && vmrss_mb * 1024 * 1024 >= max_bytes {
64            Status::Fail
65        } else if max_bytes != u64::MAX && vmrss_mb * 1024 * 1024 >= max_bytes / 2 {
66            Status::Warn
67        } else {
68            Status::Pass
69        };
70
71        results.push(Check {
72            id: format!("daemon.{name}.memory"),
73            category: "daemon",
74            status: mem_status,
75            value: format!("{vmrss_mb} MB / {max_mb}"),
76            description: if mem_status != Status::Pass {
77                "Memory usage approaching MemoryMax ceiling".into()
78            } else {
79                String::new()
80            },
81        });
82
83        // Restart count.
84        let restart_count: u32 = restarts
85            .as_deref()
86            .and_then(|s| s.parse().ok())
87            .unwrap_or(0);
88        let restart_status = match restart_count {
89            0 => Status::Pass,
90            1..=3 => Status::Warn,
91            _ => Status::Fail,
92        };
93        results.push(Check {
94            id: format!("daemon.{name}.restarts"),
95            category: "daemon",
96            status: restart_status,
97            value: restart_count.to_string(),
98            description: if restart_count > 0 {
99                format!("Restarted {restart_count} times since last daemon-reload")
100            } else {
101                String::new()
102            },
103        });
104
105        // Uptime.
106        let uptime = read_proc_uptime(pid_num);
107        results.push(Check {
108            id: format!("daemon.{name}.uptime"),
109            category: "daemon",
110            status: if uptime.as_deref() == Some("0s") {
111                Status::Fail
112            } else {
113                Status::Pass
114            },
115            value: uptime.unwrap_or_else(|| "unknown".into()),
116            description: String::new(),
117        });
118    }
119
120    results
121}
122
123/// Read a property from a systemd user unit via `systemctl --user show`.
124fn systemctl_prop(unit: &str, prop: &str) -> Option<String> {
125    let output = std::process::Command::new("systemctl")
126        .arg("--user")
127        .arg("show")
128        .arg(unit)
129        .arg(format!("-p{prop}"))
130        .arg("--value")
131        .output()
132        .ok()?;
133    if output.status.success() {
134        let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
135        if val.is_empty() || val == "[not set]" {
136            None
137        } else {
138            Some(val)
139        }
140    } else {
141        None
142    }
143}
144
145/// Read a value from `/proc/<pid>/status` by key (e.g. "VmRSS").
146/// Returns the value in kB.
147fn read_proc_status(pid: u32, key: &str) -> Option<u64> {
148    let path = format!("/proc/{pid}/status");
149    let content = std::fs::read_to_string(path).ok()?;
150    for line in content.lines() {
151        if let Some(rest) = line.strip_prefix(key) {
152            let rest = rest.trim_start_matches(':').trim();
153            // Parse "12345 kB" → 12345
154            let num_str = rest.split_whitespace().next()?;
155            return num_str.parse().ok();
156        }
157    }
158    None
159}
160
161/// Read process uptime from `/proc/<pid>/stat` and format as human-readable.
162fn read_proc_uptime(pid: u32) -> Option<String> {
163    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
164    let uptime_secs = std::fs::read_to_string("/proc/uptime").ok()?;
165    let system_uptime: f64 = uptime_secs.split_whitespace().next()?.parse().ok()?;
166
167    // Field 22 (0-indexed from after comm) is starttime in clock ticks.
168    // We need to find it after the (comm) field which may contain spaces.
169    let after_comm = stat.rfind(')')? + 2;
170    let fields: Vec<&str> = stat[after_comm..].split_whitespace().collect();
171    // starttime is field index 19 (0-indexed from after state field).
172    let starttime_ticks: u64 = fields.get(19)?.parse().ok()?;
173    // SAFETY: sysconf(_SC_CLK_TCK) is always safe — no side effects,
174    // returns the kernel's USER_HZ for /proc timing field interpretation.
175    #[allow(unsafe_code)]
176    let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as u64;
177
178    let start_secs = starttime_ticks as f64 / clk_tck as f64;
179    let elapsed = system_uptime - start_secs;
180    if elapsed < 0.0 {
181        return Some("0s".into());
182    }
183
184    let hours = elapsed as u64 / 3600;
185    let minutes = (elapsed as u64 % 3600) / 60;
186    if hours > 0 {
187        Some(format!("{hours}h {minutes}m"))
188    } else if minutes > 0 {
189        Some(format!("{minutes}m"))
190    } else {
191        Some(format!("{}s", elapsed as u64))
192    }
193}