1use std::collections::HashMap;
22use std::collections::HashSet;
23
24use tracing::debug;
25
26use crate::timer::TimerStats;
27use crate::units::ServiceStats;
28use crate::varlink::unit::{ListOutput, Unit};
29
30pub use crate::varlink::manager::MANAGER_SOCKET_PATH;
31
32const UNSET: u64 = u64::MAX;
35
36pub struct UnitLookup {
43 connection: zlink::unix::Connection,
44 cache: HashMap<String, Option<ListOutput>>,
45}
46
47impl UnitLookup {
48 pub async fn connect(socket_path: &str) -> anyhow::Result<Self> {
49 Ok(Self {
50 connection: zlink::unix::connect(socket_path).await?,
51 cache: HashMap::new(),
52 })
53 }
54
55 pub async fn get(&mut self, name: &str) -> anyhow::Result<Option<&ListOutput>> {
65 if !self.cache.contains_key(name) {
66 let fetched = match self.connection.list(Some(name)).await? {
67 Ok(output) => Some(output),
68 Err(err) => {
69 debug!("No unit {} over varlink: {}", name, err);
70 None
71 }
72 };
73 self.cache.insert(name.to_string(), fetched);
74 }
75 Ok(self.cache.get(name).and_then(|entry| entry.as_ref()))
76 }
77
78 pub fn fetches(&self) -> u64 {
80 self.cache.len() as u64
81 }
82}
83
84pub fn is_oneshot(output: &ListOutput) -> bool {
89 output
90 .context
91 .as_ref()
92 .and_then(|context| context.service.as_ref())
93 .and_then(|service| service.r#type.as_deref())
94 == Some("oneshot")
95}
96
97pub fn map_service_stats(output: &ListOutput, processes: u32) -> ServiceStats {
102 let runtime = output.runtime.as_ref();
103 let cgroup = runtime.and_then(|runtime| runtime.cgroup.as_ref());
104 let service_runtime = runtime.and_then(|runtime| runtime.service.as_ref());
105 let service_context = output
106 .context
107 .as_ref()
108 .and_then(|context| context.service.as_ref());
109
110 let realtime = |pick: fn(
111 &crate::varlink::unit::UnitRuntime,
112 ) -> Option<crate::varlink::unit::Timestamp>| {
113 runtime
114 .and_then(pick)
115 .and_then(|timestamp| timestamp.realtime)
116 .unwrap_or(0)
117 };
118
119 ServiceStats {
120 active_enter_timestamp: realtime(|runtime| runtime.active_enter_timestamp),
121 active_exit_timestamp: realtime(|runtime| runtime.active_exit_timestamp),
122 cpuusage_nsec: cgroup
127 .and_then(|cgroup| cgroup.cpu_usage_nsec)
128 .unwrap_or(UNSET),
129 inactive_exit_timestamp: realtime(|runtime| runtime.inactive_exit_timestamp),
130 ioread_bytes: cgroup
131 .and_then(|cgroup| cgroup.io_read_bytes)
132 .unwrap_or(UNSET),
133 ioread_operations: cgroup
134 .and_then(|cgroup| cgroup.io_read_operations)
135 .unwrap_or(UNSET),
136 memory_available: cgroup
137 .and_then(|cgroup| cgroup.memory_available)
138 .unwrap_or(UNSET),
139 memory_current: cgroup
140 .and_then(|cgroup| cgroup.memory_current)
141 .unwrap_or(UNSET),
142 nrestarts: service_runtime
143 .and_then(|service| service.n_restarts)
144 .unwrap_or(0),
145 processes,
146 restart_usec: service_context
147 .and_then(|service| service.restart_usec)
148 .unwrap_or(0),
149 state_change_timestamp: realtime(|runtime| runtime.state_change_timestamp),
150 status_errno: service_runtime
151 .and_then(|service| service.status_errno)
152 .unwrap_or(0),
153 tasks_current: cgroup
154 .and_then(|cgroup| cgroup.tasks_current)
155 .unwrap_or(UNSET),
156 timeout_clean_usec: output
159 .context
160 .as_ref()
161 .and_then(|context| context.exec.as_ref())
162 .and_then(|exec| exec.timeout_clean_usec)
163 .unwrap_or(UNSET),
164 watchdog_usec: service_context
165 .and_then(|service| service.watchdog_usec)
166 .unwrap_or(0),
167 }
168}
169
170pub fn timer_triggered_unit(output: &ListOutput) -> Option<&str> {
172 output
173 .context
174 .as_ref()
175 .and_then(|context| context.timer.as_ref())
176 .and_then(|timer| timer.unit.as_deref())
177}
178
179pub fn map_timer_stats(output: &ListOutput, triggered: Option<&ListOutput>) -> TimerStats {
187 let context = output
188 .context
189 .as_ref()
190 .and_then(|context| context.timer.as_ref());
191 let runtime = output
192 .runtime
193 .as_ref()
194 .and_then(|runtime| runtime.timer.as_ref());
195 let last_trigger = runtime.and_then(|runtime| runtime.last_trigger_usec);
196 let service_state_change = triggered
197 .and_then(|triggered| triggered.runtime.as_ref())
198 .and_then(|runtime| runtime.state_change_timestamp);
199
200 TimerStats {
201 accuracy_usec: context.and_then(|timer| timer.accuracy_usec).unwrap_or(0),
202 fixed_random_delay: context
203 .and_then(|timer| timer.fixed_random_delay)
204 .unwrap_or(false),
205 last_trigger_usec: last_trigger
206 .and_then(|timestamp| timestamp.realtime)
207 .unwrap_or(0),
208 last_trigger_usec_monotonic: last_trigger
209 .and_then(|timestamp| timestamp.monotonic)
210 .unwrap_or(0),
211 next_elapse_usec_monotonic: runtime
212 .and_then(|timer| timer.next_elapse_usec_monotonic)
213 .unwrap_or(0),
214 next_elapse_usec_realtime: runtime
215 .and_then(|timer| timer.next_elapse_usec_realtime)
216 .unwrap_or(0),
217 persistent: context.and_then(|timer| timer.persistent).unwrap_or(false),
218 randomized_delay_usec: context
219 .and_then(|timer| timer.randomized_delay_usec)
220 .unwrap_or(0),
221 remain_after_elapse: context
222 .and_then(|timer| timer.remain_after_elapse)
223 .unwrap_or(false),
224 service_unit_last_state_change_usec: service_state_change
225 .and_then(|timestamp| timestamp.realtime)
226 .unwrap_or(0),
227 service_unit_last_state_change_usec_monotonic: service_state_change
228 .and_then(|timestamp| timestamp.monotonic)
229 .unwrap_or(0),
230 }
231}
232
233pub async fn count_cgroup_processes(fs_root: &str, output: &ListOutput) -> u32 {
241 let runtime = output.runtime.as_ref();
242 let Some(cgroup_path) = runtime
243 .and_then(|runtime| runtime.cgroup.as_ref())
244 .and_then(|cgroup| cgroup.path.as_deref())
245 else {
246 return 0;
247 };
248
249 let mut pids: HashSet<u32> = HashSet::new();
250 let mut directories = vec![format!("{}/sys/fs/cgroup{}", fs_root, cgroup_path)];
251 while let Some(directory) = directories.pop() {
252 match tokio::fs::read_to_string(format!("{directory}/cgroup.procs")).await {
253 Ok(contents) => {
254 pids.extend(contents.lines().filter_map(|line| line.parse::<u32>().ok()))
255 }
256 Err(err) => debug!("Unable to read {}/cgroup.procs: {:?}", directory, err),
257 }
258 let Ok(mut entries) = tokio::fs::read_dir(&directory).await else {
259 continue;
260 };
261 while let Ok(Some(entry)) = entries.next_entry().await {
262 if entry.file_type().await.is_ok_and(|kind| kind.is_dir()) {
263 directories.push(entry.path().to_string_lossy().into_owned());
264 }
265 }
266 }
267
268 if let Some(main_pid) = runtime
269 .and_then(|runtime| runtime.service.as_ref())
270 .and_then(|service| service.main_pid.as_ref())
271 .and_then(|process| process.pid)
272 {
273 pids.insert(main_pid);
274 }
275
276 pids.len() as u32
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::varlink::unit::{
283 CGroupRuntime, ExecContext, ServiceContext, ServiceRuntime, TimerContext, TimerRuntime,
284 Timestamp, UnitContext, UnitRuntime,
285 };
286
287 fn output(context: Option<UnitContext>, runtime: Option<UnitRuntime>) -> ListOutput {
288 ListOutput { context, runtime }
289 }
290
291 fn service_context(r#type: &str) -> UnitContext {
292 UnitContext {
293 service: Some(ServiceContext {
294 r#type: Some(r#type.to_string()),
295 restart_usec: None,
296 watchdog_usec: None,
297 }),
298 exec: None,
299 timer: None,
300 }
301 }
302
303 #[test]
304 fn test_is_oneshot() {
305 assert!(is_oneshot(&output(Some(service_context("oneshot")), None)));
306 assert!(!is_oneshot(&output(
307 Some(service_context("notify-reload")),
308 None
309 )));
310 assert!(!is_oneshot(&output(None, None)));
313 }
314
315 #[test]
316 fn test_map_service_stats_from_a_populated_reply() {
317 let stats = map_service_stats(
318 &output(
319 Some(UnitContext {
320 service: Some(ServiceContext {
321 r#type: Some("notify-reload".to_string()),
322 restart_usec: Some(100_000),
323 watchdog_usec: None,
324 }),
325 exec: Some(ExecContext {
327 timeout_clean_usec: Some(30_000_000),
328 }),
329 timer: None,
330 }),
331 Some(UnitRuntime {
332 state_change_timestamp: Some(Timestamp {
333 realtime: Some(1_789_701_442_296_989),
334 monotonic: Some(3_774_344_633),
335 }),
336 active_enter_timestamp: Some(Timestamp {
337 realtime: Some(1_789_701_442_296_989),
338 monotonic: Some(3_774_344_633),
339 }),
340 inactive_exit_timestamp: Some(Timestamp {
341 realtime: Some(1_789_701_442_287_084),
342 monotonic: Some(3_774_334_729),
343 }),
344 active_exit_timestamp: Some(Timestamp {
345 realtime: Some(1_789_701_442_280_000),
346 monotonic: Some(3_774_327_645),
347 }),
348 cgroup: Some(CGroupRuntime {
349 path: Some("/system.slice/dbus-broker.service".to_string()),
350 cpu_usage_nsec: Some(86_690_000),
351 memory_current: Some(3_457_024),
352 memory_available: Some(7_619_899_392),
353 tasks_current: Some(2),
354 io_read_bytes: None,
355 io_read_operations: None,
356 }),
357 service: Some(ServiceRuntime {
358 main_pid: None,
359 status_errno: Some(0),
360 n_restarts: Some(0),
361 }),
362 timer: None,
363 }),
364 ),
365 2,
366 );
367
368 assert_eq!(stats.active_enter_timestamp, 1_789_701_442_296_989);
369 assert_eq!(stats.cpuusage_nsec, 86_690_000);
370 assert_eq!(stats.memory_current, 3_457_024);
371 assert_eq!(stats.tasks_current, 2);
372 assert_eq!(stats.processes, 2);
373 assert_eq!(stats.restart_usec, 100_000);
374 assert_eq!(stats.active_exit_timestamp, 1_789_701_442_280_000);
376 assert_eq!(stats.timeout_clean_usec, 30_000_000);
378 assert_eq!(stats.ioread_bytes, u64::MAX);
381 assert_eq!(stats.ioread_operations, u64::MAX);
382 assert_eq!(stats.watchdog_usec, 0);
383 }
384
385 fn timer_output() -> ListOutput {
386 ListOutput {
388 context: Some(UnitContext {
389 service: None,
390 exec: None,
391 timer: Some(TimerContext {
392 unit: Some("systemd-tmpfiles-clean.service".to_string()),
393 accuracy_usec: Some(60_000_000),
394 randomized_delay_usec: None,
395 fixed_random_delay: Some(false),
396 persistent: Some(false),
397 remain_after_elapse: Some(true),
398 }),
399 }),
400 runtime: Some(UnitRuntime {
401 state_change_timestamp: None,
402 active_enter_timestamp: None,
403 inactive_exit_timestamp: None,
404 active_exit_timestamp: None,
405 cgroup: None,
406 service: None,
407 timer: Some(TimerRuntime {
408 next_elapse_usec_realtime: Some(0),
409 next_elapse_usec_monotonic: Some(91_091_400_515),
410 last_trigger_usec: Some(Timestamp {
411 realtime: Some(1_789_702_359_355_506),
412 monotonic: Some(4_691_399_604),
413 }),
414 }),
415 }),
416 }
417 }
418
419 #[test]
420 fn test_map_timer_stats() {
421 let triggered = output(
422 None,
423 Some(UnitRuntime {
424 state_change_timestamp: Some(Timestamp {
425 realtime: Some(1_789_702_359_400_000),
426 monotonic: Some(4_691_444_098),
427 }),
428 active_enter_timestamp: None,
429 inactive_exit_timestamp: None,
430 active_exit_timestamp: None,
431 cgroup: None,
432 service: None,
433 timer: None,
434 }),
435 );
436 let stats = map_timer_stats(&timer_output(), Some(&triggered));
437
438 assert_eq!(stats.accuracy_usec, 60_000_000);
439 assert_eq!(stats.last_trigger_usec, 1_789_702_359_355_506);
440 assert_eq!(stats.last_trigger_usec_monotonic, 4_691_399_604);
441 assert_eq!(stats.next_elapse_usec_monotonic, 91_091_400_515);
442 assert_eq!(stats.next_elapse_usec_realtime, 0);
444 assert!(stats.remain_after_elapse);
445 assert!(!stats.persistent);
446 assert_eq!(stats.randomized_delay_usec, 0);
448 assert_eq!(
450 stats.service_unit_last_state_change_usec,
451 1_789_702_359_400_000
452 );
453 assert_eq!(
454 stats.service_unit_last_state_change_usec_monotonic,
455 4_691_444_098
456 );
457 }
458
459 #[test]
460 fn test_map_timer_stats_without_the_triggered_unit() {
461 let stats = map_timer_stats(&timer_output(), None);
464 assert_eq!(stats.service_unit_last_state_change_usec, 0);
465 assert_eq!(stats.service_unit_last_state_change_usec_monotonic, 0);
466 assert_eq!(stats.accuracy_usec, 60_000_000);
467 }
468
469 #[test]
470 fn test_timer_triggered_unit() {
471 assert_eq!(
472 timer_triggered_unit(&timer_output()),
473 Some("systemd-tmpfiles-clean.service")
474 );
475 assert_eq!(timer_triggered_unit(&output(None, None)), None);
476 }
477
478 #[test]
479 fn test_map_service_stats_from_an_empty_reply() {
480 let stats = map_service_stats(&output(None, None), 0);
484 assert_eq!(stats.ioread_bytes, u64::MAX);
485 assert_eq!(stats.ioread_operations, u64::MAX);
486 assert_eq!(stats.timeout_clean_usec, u64::MAX);
487 assert_eq!(stats.cpuusage_nsec, u64::MAX);
488 assert_eq!(stats.memory_current, u64::MAX);
489 assert_eq!(stats.memory_available, u64::MAX);
490 assert_eq!(stats.tasks_current, u64::MAX);
491 assert_eq!(stats.watchdog_usec, 0);
493 assert_eq!(stats.active_enter_timestamp, 0);
494 }
495}