1use super::{Check, Status};
4
5const 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 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 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 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 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 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
123fn 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
145fn 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 let num_str = rest.split_whitespace().next()?;
155 return num_str.parse().ok();
156 }
157 }
158 None
159}
160
161fn 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 let after_comm = stat.rfind(')')? + 2;
170 let fields: Vec<&str> = stat[after_comm..].split_whitespace().collect();
171 let starttime_ticks: u64 = fields.get(19)?.parse().ok()?;
173 #[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}