1use std::collections::HashMap;
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Instant;
10use std::time::SystemTime;
11use std::time::UNIX_EPOCH;
12
13use struct_field_names_as_array::FieldNamesAsArray;
14use thiserror::Error;
15use tokio::sync::RwLock;
16use tracing::debug;
17use tracing::error;
18use tracing::warn;
19use zbus::zvariant::ObjectPath;
20use zbus::zvariant::OwnedObjectPath;
21
22#[derive(Error, Debug)]
23pub enum MonitordUnitsError {
24 #[error("Units D-Bus error: {0}")]
25 ZbusError(#[from] zbus::Error),
26 #[error("Integer conversion error: {0}")]
27 IntConversion(#[from] std::num::TryFromIntError),
28 #[error("System time error: {0}")]
29 SystemTimeError(#[from] std::time::SystemTimeError),
30}
31
32use crate::timer::TimerStats;
33use crate::MachineStats;
34
35pub use crate::unit_constants::is_unit_unhealthy;
37pub use crate::unit_constants::SystemdUnitActiveState;
38pub use crate::unit_constants::SystemdUnitLoadState;
39
40#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
45pub struct UnitsCollectionTimings {
46 pub list_units_ms: f64,
48 pub unit_files_ms: f64,
50 pub per_unit_loop_ms: f64,
53 pub timer_dbus_fetches: u64,
55 pub state_dbus_fetches: u64,
57 pub service_dbus_fetches: u64,
59}
60
61#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
63pub struct UnitFilesScope {
64 pub generated: HashMap<String, u64>,
66 pub transient: HashMap<String, u64>,
68}
69
70#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
72pub struct UnitFilesStats {
73 pub root: UnitFilesScope,
74 pub user: UnitFilesScope,
75}
76
77#[derive(
78 serde::Serialize, serde::Deserialize, Clone, Debug, Default, FieldNamesAsArray, PartialEq,
79)]
80
81pub struct SystemdUnitStats {
84 pub activating_units: u64,
86 pub active_units: u64,
88 pub automount_units: u64,
90 pub device_units: u64,
92 pub failed_units: u64,
94 pub inactive_units: u64,
96 pub jobs_queued: u64,
98 pub loaded_units: u64,
100 pub masked_units: u64,
102 pub mount_units: u64,
104 pub not_found_units: u64,
106 pub path_units: u64,
108 pub scope_units: u64,
110 pub service_units: u64,
112 pub slice_units: u64,
114 pub socket_units: u64,
116 pub target_units: u64,
118 pub timer_units: u64,
120 pub timer_persistent_units: u64,
122 pub timer_remain_after_elapse: u64,
124 pub total_units: u64,
126 pub unit_files: UnitFilesStats,
128 pub service_stats: HashMap<String, ServiceStats>,
130 pub timer_stats: HashMap<String, TimerStats>,
132 pub unit_states: HashMap<String, UnitStates>,
134 pub collection_timings: UnitsCollectionTimings,
137}
138
139#[derive(
142 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
143)]
144pub struct ServiceStats {
145 pub active_enter_timestamp: u64,
147 pub active_exit_timestamp: u64,
149 pub cpuusage_nsec: u64,
151 pub inactive_exit_timestamp: u64,
153 pub ioread_bytes: u64,
155 pub ioread_operations: u64,
157 pub memory_available: u64,
159 pub memory_current: u64,
161 pub nrestarts: u32,
163 pub processes: u32,
165 pub restart_usec: u64,
167 pub state_change_timestamp: u64,
169 pub status_errno: i32,
171 pub tasks_current: u64,
173 pub timeout_clean_usec: u64,
175 pub watchdog_usec: u64,
177}
178
179#[derive(
182 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
183)]
184pub struct UnitStates {
185 pub active_state: SystemdUnitActiveState,
187 pub load_state: SystemdUnitLoadState,
189 pub unhealthy: bool,
192 pub time_in_state_usecs: Option<u64>,
195}
196
197#[derive(Debug)]
202pub struct ListedUnit {
203 pub name: String, pub description: String, pub load_state: String, pub active_state: String, pub sub_state: String, pub follow_unit: String, pub unit_object_path: OwnedObjectPath, pub job_id: u32, pub job_type: String, pub job_object_path: OwnedObjectPath, }
214impl
215 From<(
216 String,
217 String,
218 String,
219 String,
220 String,
221 String,
222 OwnedObjectPath,
223 u32,
224 String,
225 OwnedObjectPath,
226 )> for ListedUnit
227{
228 fn from(
229 tuple: (
230 String,
231 String,
232 String,
233 String,
234 String,
235 String,
236 OwnedObjectPath,
237 u32,
238 String,
239 OwnedObjectPath,
240 ),
241 ) -> Self {
242 ListedUnit {
243 name: tuple.0,
244 description: tuple.1,
245 load_state: tuple.2,
246 active_state: tuple.3,
247 sub_state: tuple.4,
248 follow_unit: tuple.5,
249 unit_object_path: tuple.6,
250 job_id: tuple.7,
251 job_type: tuple.8,
252 job_object_path: tuple.9,
253 }
254 }
255}
256
257pub const SERVICE_FIELD_NAMES: &[&str] = &ServiceStats::FIELD_NAMES_AS_ARRAY;
258pub const UNIT_FIELD_NAMES: &[&str] = &SystemdUnitStats::FIELD_NAMES_AS_ARRAY;
259pub const UNIT_STATES_FIELD_NAMES: &[&str] = &UnitStates::FIELD_NAMES_AS_ARRAY;
260
261async fn parse_service(
263 connection: &zbus::Connection,
264 name: &str,
265 object_path: &OwnedObjectPath,
266) -> Result<ServiceStats, MonitordUnitsError> {
267 debug!("Parsing service {} stats", name);
268
269 let sp = crate::dbus::zbus_service::ServiceProxy::builder(connection)
270 .cache_properties(zbus::proxy::CacheProperties::No)
271 .path(object_path.clone())?
272 .build()
273 .await?;
274 let up = crate::dbus::zbus_unit::UnitProxy::builder(connection)
275 .cache_properties(zbus::proxy::CacheProperties::No)
276 .path(object_path.clone())?
277 .build()
278 .await?;
279
280 let (
283 active_enter_timestamp,
284 active_exit_timestamp,
285 cpuusage_nsec,
286 inactive_exit_timestamp,
287 ioread_bytes,
288 ioread_operations,
289 memory_current,
290 memory_available,
291 nrestarts,
292 processes,
293 restart_usec,
294 state_change_timestamp,
295 status_errno,
296 tasks_current,
297 timeout_clean_usec,
298 watchdog_usec,
299 ) = tokio::join!(
300 up.active_enter_timestamp(),
301 up.active_exit_timestamp(),
302 sp.cpuusage_nsec(),
303 up.inactive_exit_timestamp(),
304 sp.ioread_bytes(),
305 sp.ioread_operations(),
306 sp.memory_current(),
307 sp.memory_available(),
308 sp.nrestarts(),
309 sp.get_processes(),
310 sp.restart_usec(),
311 up.state_change_timestamp(),
312 sp.status_errno(),
313 sp.tasks_current(),
314 sp.timeout_clean_usec(),
315 sp.watchdog_usec(),
316 );
317
318 Ok(ServiceStats {
319 active_enter_timestamp: active_enter_timestamp?,
320 active_exit_timestamp: active_exit_timestamp?,
321 cpuusage_nsec: cpuusage_nsec?,
322 inactive_exit_timestamp: inactive_exit_timestamp?,
323 ioread_bytes: ioread_bytes?,
324 ioread_operations: ioread_operations?,
325 memory_current: memory_current?,
326 memory_available: memory_available?,
327 nrestarts: nrestarts?,
328 processes: processes?.len().try_into()?,
329 restart_usec: restart_usec?,
330 state_change_timestamp: state_change_timestamp?,
331 status_errno: status_errno?,
332 tasks_current: tasks_current?,
333 timeout_clean_usec: timeout_clean_usec?,
334 watchdog_usec: watchdog_usec?,
335 })
336}
337
338async fn get_time_in_state(
339 connection: Option<&zbus::Connection>,
340 unit: &ListedUnit,
341) -> Result<Option<u64>, MonitordUnitsError> {
342 match connection {
343 Some(c) => {
344 let up = crate::dbus::zbus_unit::UnitProxy::builder(c)
345 .cache_properties(zbus::proxy::CacheProperties::No)
346 .path(ObjectPath::from(unit.unit_object_path.clone()))?
347 .build()
348 .await?;
349 let now: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() * 1_000_000;
350 let state_change_timestamp = match up.state_change_timestamp().await {
351 Ok(sct) => sct,
352 Err(err) => {
353 error!(
354 "Unable to get state_change_timestamp for {} - Setting to 0: {:?}",
355 &unit.name, err,
356 );
357 0
358 }
359 };
360 Ok(Some(now - state_change_timestamp))
361 }
362 None => {
363 error!("No zbus connection passed, but time_in_state_usecs enabled");
364 Ok(None)
365 }
366 }
367}
368
369pub async fn parse_state(
375 stats: &mut SystemdUnitStats,
376 unit: &ListedUnit,
377 config: &crate::config::UnitsConfig,
378 connection: Option<&zbus::Connection>,
379) -> Result<bool, MonitordUnitsError> {
380 if config.state_stats_blocklist.contains(&unit.name) {
381 debug!("Skipping state stats for {} due to blocklist", &unit.name);
382 return Ok(false);
383 }
384 if !config.state_stats_allowlist.is_empty()
385 && !config.state_stats_allowlist.contains(&unit.name)
386 {
387 return Ok(false);
388 }
389 let active_state = SystemdUnitActiveState::from_str(&unit.active_state)
390 .unwrap_or(SystemdUnitActiveState::unknown);
391 let load_state = SystemdUnitLoadState::from_str(&unit.load_state.replace('-', "_"))
392 .unwrap_or(SystemdUnitLoadState::unknown);
393
394 let mut time_in_state_usecs: Option<u64> = None;
396 let mut did_dbus_fetch = false;
397 if config.state_stats_time_in_state {
398 time_in_state_usecs = get_time_in_state(connection, unit).await?;
399 did_dbus_fetch = connection.is_some();
402 }
403
404 stats.unit_states.insert(
405 unit.name.clone(),
406 UnitStates {
407 active_state,
408 load_state,
409 unhealthy: is_unit_unhealthy(active_state, load_state),
410 time_in_state_usecs,
411 },
412 );
413 Ok(did_dbus_fetch)
414}
415
416fn parse_unit(stats: &mut SystemdUnitStats, unit: &ListedUnit) {
418 match unit.name.rsplit('.').next() {
420 Some("automount") => stats.automount_units += 1,
421 Some("device") => stats.device_units += 1,
422 Some("mount") => stats.mount_units += 1,
423 Some("path") => stats.path_units += 1,
424 Some("scope") => stats.scope_units += 1,
425 Some("service") => stats.service_units += 1,
426 Some("slice") => stats.slice_units += 1,
427 Some("socket") => stats.socket_units += 1,
428 Some("target") => stats.target_units += 1,
429 Some("timer") => stats.timer_units += 1,
430 unknown => debug!("Found unhandled '{:?}' unit type", unknown),
431 };
432 match unit.load_state.as_str() {
434 "loaded" => stats.loaded_units += 1,
435 "masked" => stats.masked_units += 1,
436 "not-found" => stats.not_found_units += 1,
437 _ => debug!("{} is not loaded. It's {}", unit.name, unit.load_state),
438 };
439 match unit.active_state.as_str() {
441 "activating" => stats.activating_units += 1,
442 "active" => stats.active_units += 1,
443 "failed" => stats.failed_units += 1,
444 "inactive" => stats.inactive_units += 1,
445 unknown => debug!("Found unhandled '{}' unit state", unknown),
446 };
447 if unit.job_id != 0 {
449 stats.jobs_queued += 1;
450 }
451}
452
453const TRANSIENT_DIR: &str = "/run/systemd/transient";
454
455async fn count_unit_files_by_type(path: &str) -> HashMap<String, u64> {
456 let mut dir = match tokio::fs::read_dir(path).await {
457 Ok(d) => d,
458 Err(err) => {
459 debug!("Unable to read {}: {:?}", path, err);
460 return HashMap::new();
461 }
462 };
463 let mut counts = HashMap::new();
464 loop {
465 match dir.next_entry().await {
466 Ok(Some(entry)) => {
467 let file_type = match entry.file_type().await {
468 Ok(ft) => ft,
469 Err(_) => continue,
470 };
471 if !file_type.is_file() {
472 continue;
473 }
474 let name = entry.file_name();
475 let unit_type = name
476 .to_str()
477 .and_then(|n| n.rsplit('.').next())
478 .unwrap_or("unknown");
479 *counts.entry(unit_type.to_string()).or_insert(0) += 1;
480 }
481 Ok(None) => break,
482 Err(err) => {
483 warn!("Error reading entry in {}: {:?}", path, err);
484 continue;
485 }
486 }
487 }
488 counts
489}
490
491fn merge_counts(target: &mut HashMap<String, u64>, source: HashMap<String, u64>) {
492 for (unit_type, count) in source {
493 *target.entry(unit_type).or_insert(0) += count;
494 }
495}
496
497async fn enumerate_user_transient_dirs(fs_root: &str) -> Vec<String> {
499 let user_dir = format!("{fs_root}/run/user");
500 match tokio::fs::read_dir(&user_dir).await {
501 Ok(mut entries) => {
502 let mut dirs = Vec::new();
503 loop {
504 match entries.next_entry().await {
505 Ok(Some(entry)) => {
506 dirs.push(format!("{}/systemd/transient", entry.path().display()));
507 }
508 Ok(None) => break,
509 Err(err) => {
510 warn!("Error reading entry in {}: {:?}", user_dir, err);
511 continue;
512 }
513 }
514 }
515 dirs
516 }
517 Err(err) => {
518 debug!("Unable to read {}: {:?}", user_dir, err);
519 Vec::new()
520 }
521 }
522}
523
524pub async fn collect_unit_files_stats(fs_root: &str) -> UnitFilesStats {
532 let gen_path = format!("{fs_root}/run/systemd/generator");
534 let gen_early_path = format!("{fs_root}/run/systemd/generator.early");
535 let gen_late_path = format!("{fs_root}/run/systemd/generator.late");
536 let transient_path = format!("{fs_root}{TRANSIENT_DIR}");
537
538 let (gen, gen_early, gen_late, root_transient, user_dirs) = tokio::join!(
540 count_unit_files_by_type(&gen_path),
541 count_unit_files_by_type(&gen_early_path),
542 count_unit_files_by_type(&gen_late_path),
543 count_unit_files_by_type(&transient_path),
544 enumerate_user_transient_dirs(fs_root),
545 );
546
547 let mut root_generated = HashMap::new();
548 merge_counts(&mut root_generated, gen);
549 merge_counts(&mut root_generated, gen_early);
550 merge_counts(&mut root_generated, gen_late);
551
552 let user_transient_counts =
554 futures_util::future::join_all(user_dirs.iter().map(|d| count_unit_files_by_type(d))).await;
555
556 let mut user_transient = HashMap::new();
557 for counts in user_transient_counts {
558 merge_counts(&mut user_transient, counts);
559 }
560
561 UnitFilesStats {
562 root: UnitFilesScope {
563 generated: root_generated,
564 transient: root_transient,
565 },
566 user: UnitFilesScope {
567 generated: HashMap::new(),
568 transient: user_transient,
569 },
570 }
571}
572
573pub async fn parse_unit_state(
575 config: &crate::config::Config,
576 connection: &zbus::Connection,
577 fs_root: &str,
578) -> Result<SystemdUnitStats, MonitordUnitsError> {
579 if !config.units.state_stats_allowlist.is_empty() {
580 debug!(
581 "Using unit state allowlist: {:?}",
582 config.units.state_stats_allowlist
583 );
584 }
585
586 if !config.units.state_stats_blocklist.is_empty() {
587 debug!(
588 "Using unit state blocklist: {:?}",
589 config.units.state_stats_blocklist,
590 );
591 }
592
593 let mut stats = SystemdUnitStats::default();
594
595 let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
596 .cache_properties(zbus::proxy::CacheProperties::No)
597 .build()
598 .await?;
599
600 let (unit_files_result, units_result) = tokio::join!(
602 async {
603 let start = Instant::now();
604 let files = if config.units.unit_files {
605 collect_unit_files_stats(fs_root).await
606 } else {
607 UnitFilesStats::default()
608 };
609 (files, start.elapsed().as_secs_f64() * 1000.0)
610 },
611 async {
612 let start = Instant::now();
613 let units = p.list_units().await;
614 (units, start.elapsed().as_secs_f64() * 1000.0)
615 },
616 );
617 let (unit_files, unit_files_ms) = unit_files_result;
618 let (units_result, list_units_ms) = units_result;
619 stats.collection_timings.unit_files_ms = unit_files_ms;
620 stats.collection_timings.list_units_ms = list_units_ms;
621 stats.unit_files = unit_files;
622
623 let units = units_result?;
624 stats.total_units = units.len() as u64;
625
626 let per_unit_loop_start = Instant::now();
627 let mut state_dbus_fetches: u64 = 0;
628 let mut service_dbus_fetches: u64 = 0;
629 let mut timer_dbus_fetches: u64 = 0;
630
631 for unit_raw in units {
632 let unit: ListedUnit = unit_raw.into();
633 parse_unit(&mut stats, &unit);
635
636 if config.units.state_stats {
639 let did_dbus_fetch =
640 parse_state(&mut stats, &unit, &config.units, Some(connection)).await?;
641 if did_dbus_fetch {
642 state_dbus_fetches += 1;
643 }
644 }
645
646 if config.services.contains(&unit.name) {
648 debug!("Collecting service stats for {:?}", &unit);
649 match parse_service(connection, &unit.name, &unit.unit_object_path).await {
650 Ok(service_stats) => {
651 stats.service_stats.insert(unit.name.clone(), service_stats);
652 service_dbus_fetches += 1;
653 }
654 Err(err) => error!(
655 "Unable to get service stats for {} {}: {:#?}",
656 &unit.name, &unit.unit_object_path, err
657 ),
658 }
659 }
660
661 if config.timers.enabled && unit.name.contains(".timer") {
663 if config.timers.blocklist.contains(&unit.name) {
664 debug!("Skipping timer stats for {} due to blocklist", &unit.name);
665 continue;
666 }
667 if !config.timers.allowlist.is_empty() && !config.timers.allowlist.contains(&unit.name)
668 {
669 continue;
670 }
671 let timer_stats: Option<TimerStats> =
672 match crate::timer::collect_timer_stats(connection, &mut stats, &unit).await {
673 Ok(ts) => {
674 timer_dbus_fetches += 1;
675 Some(ts)
676 }
677 Err(err) => {
678 error!("Failed to get {} stats: {:#?}", &unit.name, err);
679 None
680 }
681 };
682 if let Some(ts) = timer_stats {
683 stats.timer_stats.insert(unit.name.clone(), ts);
684 }
685 }
686 }
687 let per_unit_loop_elapsed = per_unit_loop_start.elapsed();
688 stats.collection_timings.per_unit_loop_ms = per_unit_loop_elapsed.as_secs_f64() * 1000.0;
689 stats.collection_timings.state_dbus_fetches = state_dbus_fetches;
690 stats.collection_timings.service_dbus_fetches = service_dbus_fetches;
691 stats.collection_timings.timer_dbus_fetches = timer_dbus_fetches;
692
693 debug!("unit stats: {:?}", stats);
694 Ok(stats)
695}
696
697pub async fn update_unit_stats(
701 config: Arc<crate::config::Config>,
702 connection: zbus::Connection,
703 locked_machine_stats: Arc<RwLock<MachineStats>>,
704 fs_root: String,
705) -> anyhow::Result<()> {
706 let mut machine_stats = locked_machine_stats.write().await;
707 match parse_unit_state(&config, &connection, &fs_root).await {
708 Ok(units_stats) => machine_stats.units = units_stats,
709 Err(err) => error!("units stats failed: {:?}", err),
710 }
711 Ok(())
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use std::collections::HashSet;
718 use strum::IntoEnumIterator;
719
720 fn get_unit_file() -> ListedUnit {
721 ListedUnit {
722 name: String::from("apport-autoreport.timer"),
723 description: String::from(
724 "Process error reports when automatic reporting is enabled (timer based)",
725 ),
726 load_state: String::from("loaded"),
727 active_state: String::from("inactive"),
728 sub_state: String::from("dead"),
729 follow_unit: String::from(""),
730 unit_object_path: ObjectPath::try_from(
731 "/org/freedesktop/systemd1/unit/apport_2dautoreport_2etimer",
732 )
733 .expect("Unable to make an object path")
734 .into(),
735 job_id: 0,
736 job_type: String::from(""),
737 job_object_path: ObjectPath::try_from("/").unwrap().into(),
738 }
739 }
740
741 #[tokio::test]
742 async fn test_state_parse() -> Result<(), MonitordUnitsError> {
743 let test_unit_name = String::from("apport-autoreport.timer");
744 let expected_stats = SystemdUnitStats {
745 activating_units: 0,
746 active_units: 0,
747 automount_units: 0,
748 device_units: 0,
749 failed_units: 0,
750 inactive_units: 0,
751 jobs_queued: 0,
752 loaded_units: 0,
753 masked_units: 0,
754 mount_units: 0,
755 not_found_units: 0,
756 path_units: 0,
757 scope_units: 0,
758 service_units: 0,
759 slice_units: 0,
760 socket_units: 0,
761 target_units: 0,
762 timer_units: 0,
763 timer_persistent_units: 0,
764 timer_remain_after_elapse: 0,
765 total_units: 0,
766 unit_files: UnitFilesStats::default(),
767 service_stats: HashMap::new(),
768 timer_stats: HashMap::new(),
769 unit_states: HashMap::from([(
770 test_unit_name.clone(),
771 UnitStates {
772 active_state: SystemdUnitActiveState::inactive,
773 load_state: SystemdUnitLoadState::loaded,
774 unhealthy: true,
775 time_in_state_usecs: None,
776 },
777 )]),
778 collection_timings: UnitsCollectionTimings::default(),
779 };
780 let mut stats = SystemdUnitStats::default();
781 let systemd_unit = get_unit_file();
782 let mut config = crate::config::UnitsConfig::default();
783
784 let did_fetch = parse_state(&mut stats, &systemd_unit, &config, None).await?;
787 assert_eq!(expected_stats, stats);
788 assert!(!did_fetch);
789
790 config.state_stats_allowlist = HashSet::from([test_unit_name.clone()]);
792
793 let mut allowlist_stats = SystemdUnitStats::default();
795 let did_fetch = parse_state(&mut allowlist_stats, &systemd_unit, &config, None).await?;
796 assert_eq!(expected_stats, allowlist_stats);
797 assert!(!did_fetch);
798
799 config.state_stats_blocklist = HashSet::from([test_unit_name]);
801
802 let mut blocklist_stats = SystemdUnitStats::default();
804 let expected_blocklist_stats = SystemdUnitStats::default();
805 let did_fetch = parse_state(&mut blocklist_stats, &systemd_unit, &config, None).await?;
806 assert_eq!(expected_blocklist_stats, blocklist_stats);
807 assert!(!did_fetch);
809 Ok(())
810 }
811
812 #[test]
813 fn test_unit_parse() {
814 let expected_stats = SystemdUnitStats {
815 activating_units: 0,
816 active_units: 0,
817 automount_units: 0,
818 device_units: 0,
819 failed_units: 0,
820 inactive_units: 1,
821 jobs_queued: 0,
822 loaded_units: 1,
823 masked_units: 0,
824 mount_units: 0,
825 not_found_units: 0,
826 path_units: 0,
827 scope_units: 0,
828 service_units: 0,
829 slice_units: 0,
830 socket_units: 0,
831 target_units: 0,
832 timer_units: 1,
833 timer_persistent_units: 0,
834 timer_remain_after_elapse: 0,
835 total_units: 0,
836 unit_files: UnitFilesStats::default(),
837 service_stats: HashMap::new(),
838 timer_stats: HashMap::new(),
839 unit_states: HashMap::new(),
840 collection_timings: UnitsCollectionTimings::default(),
841 };
842 let mut stats = SystemdUnitStats::default();
843 let systemd_unit = get_unit_file();
844 parse_unit(&mut stats, &systemd_unit);
845 assert_eq!(expected_stats, stats);
846 }
847
848 #[test]
849 fn test_unit_parse_activating() {
850 let mut activating_unit = get_unit_file();
851 activating_unit.active_state = String::from("activating");
852 let mut stats = SystemdUnitStats::default();
853 parse_unit(&mut stats, &activating_unit);
854 assert_eq!(stats.activating_units, 1);
855 assert_eq!(stats.active_units, 0);
856 assert_eq!(stats.inactive_units, 0);
857 }
858
859 #[test]
860 fn test_iterators() {
861 assert!(SystemdUnitActiveState::iter().collect::<Vec<_>>().len() > 0);
862 assert!(SystemdUnitLoadState::iter().collect::<Vec<_>>().len() > 0);
863 }
864
865 #[tokio::test]
866 async fn test_count_unit_files_by_type() {
867 let dir = tempfile::tempdir().expect("Unable to create temp dir");
868 let path = dir.path();
869
870 std::fs::write(path.join("sshd.service"), "").unwrap();
871 std::fs::write(path.join("nginx.service"), "").unwrap();
872 std::fs::write(path.join("boot.mount"), "").unwrap();
873 std::fs::write(path.join("swap.swap"), "").unwrap();
874 std::fs::create_dir(path.join("multi-user.target.wants")).unwrap();
875
876 let counts = count_unit_files_by_type(path.to_str().unwrap()).await;
877 assert_eq!(counts.get("service"), Some(&2));
878 assert_eq!(counts.get("mount"), Some(&1));
879 assert_eq!(counts.get("swap"), Some(&1));
880 assert_eq!(counts.get("wants"), None);
881 assert_eq!(counts.len(), 3);
882 }
883
884 #[tokio::test]
885 async fn test_count_unit_files_by_type_nonexistent_dir() {
886 let counts = count_unit_files_by_type("/nonexistent/path").await;
887 assert!(counts.is_empty());
888 }
889
890 #[tokio::test]
891 async fn test_collect_unit_files_stats_with_fs_root() {
892 let root = tempfile::tempdir().expect("Unable to create temp dir");
893 let root_path = root.path();
894
895 let gen_dir = root_path.join("run/systemd/generator");
896 std::fs::create_dir_all(&gen_dir).unwrap();
897 std::fs::write(gen_dir.join("boot.mount"), "").unwrap();
898 std::fs::write(gen_dir.join("swap.swap"), "").unwrap();
899
900 let transient_dir = root_path.join("run/systemd/transient");
901 std::fs::create_dir_all(&transient_dir).unwrap();
902 std::fs::write(transient_dir.join("run-thing.service"), "").unwrap();
903
904 let user_transient = root_path.join("run/user/1000/systemd/transient");
905 std::fs::create_dir_all(&user_transient).unwrap();
906 std::fs::write(user_transient.join("app-code.scope"), "").unwrap();
907 std::fs::write(user_transient.join("app-term.scope"), "").unwrap();
908
909 let stats = collect_unit_files_stats(root_path.to_str().unwrap()).await;
910 assert_eq!(stats.root.generated.get("mount"), Some(&1));
911 assert_eq!(stats.root.generated.get("swap"), Some(&1));
912 assert_eq!(stats.root.transient.get("service"), Some(&1));
913 assert_eq!(stats.user.transient.get("scope"), Some(&2));
914 assert!(stats.user.generated.is_empty());
915 }
916}