use std::collections::{HashMap, HashSet};
use std::process::Command;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
use crate::MachineStats;
#[derive(Error, Debug)]
pub enum MonitordVerifyError {
#[error("Failed to execute systemd-analyze: {0}")]
CommandError(String),
#[error("Unable to connect to D-Bus via zbus: {0:#}")]
ZbusError(#[from] zbus::Error),
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
pub struct VerifyStats {
pub total: u64,
#[serde(flatten)]
pub by_type: HashMap<String, u64>,
}
fn get_unit_type(unit_name: &str) -> Option<String> {
if unit_name.len() < 3 {
return None;
}
let first_char = unit_name.chars().next()?;
if !first_char.is_alphanumeric() && first_char != '-' && first_char != '\\' {
return None;
}
unit_name.rsplit('.').next().map(|s| s.to_string())
}
fn parse_verify_output(stderr: &str) -> HashSet<String> {
let mut failing_units = HashSet::new();
for line in stderr.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.contains("Failed to prepare filename") {
continue;
}
let mut found_in_line = false;
if line.starts_with('/') {
if let Some(pos) = line.find(':') {
let path_part = &line[..pos];
if let Some(filename) = path_part.rsplit('/').next() {
if filename.contains('.') && get_unit_type(filename).is_some() {
failing_units.insert(filename.to_string());
found_in_line = true;
}
}
}
}
if !found_in_line {
for word in line.split_whitespace() {
let cleaned = word.trim_end_matches(':').trim_end_matches('.');
if cleaned.contains('.')
&& cleaned.len() > 2 && !cleaned.contains('(') && get_unit_type(cleaned).is_some()
{
failing_units.insert(cleaned.to_string());
break; }
}
}
}
failing_units
}
pub async fn get_verify_stats(
connection: &zbus::Connection,
allowlist: &HashSet<String>,
blocklist: &HashSet<String>,
) -> Result<VerifyStats, MonitordVerifyError> {
let mut stats = VerifyStats::default();
let manager_proxy = crate::dbus::zbus_systemd::ManagerProxy::new(connection).await?;
let all_units = manager_proxy.list_units().await?;
let units_to_check: Vec<String> = all_units
.into_iter()
.map(|unit| unit.0)
.filter(|unit_name| {
if !allowlist.is_empty() && !allowlist.contains(unit_name) {
return false;
}
if blocklist.contains(unit_name) {
return false;
}
true
})
.collect();
if units_to_check.is_empty() {
return Ok(stats);
}
let output = tokio::task::spawn_blocking(move || {
let mut cmd = Command::new("systemd-analyze");
cmd.arg("verify");
for unit_name in &units_to_check {
cmd.arg(unit_name);
}
cmd.output()
})
.await
.map_err(|e| MonitordVerifyError::CommandError(e.to_string()))?
.map_err(|e| MonitordVerifyError::CommandError(e.to_string()))?;
let stderr = String::from_utf8_lossy(&output.stderr);
let failing_units = parse_verify_output(&stderr);
for unit_name in failing_units {
stats.total += 1;
if let Some(unit_type) = get_unit_type(&unit_name) {
*stats.by_type.entry(unit_type).or_insert(0) += 1;
}
}
Ok(stats)
}
pub async fn update_verify_stats(
connection: zbus::Connection,
locked_machine_stats: Arc<RwLock<MachineStats>>,
allowlist: HashSet<String>,
blocklist: HashSet<String>,
) -> anyhow::Result<()> {
let verify_stats = get_verify_stats(&connection, &allowlist, &blocklist)
.await
.map_err(|e| anyhow::anyhow!("Error getting verify stats: {:?}", e))?;
let mut machine_stats = locked_machine_stats.write().await;
machine_stats.verify_stats = Some(verify_stats);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_unit_type() {
assert_eq!(get_unit_type("foo.service"), Some("service".to_string()));
assert_eq!(get_unit_type("bar.slice"), Some("slice".to_string()));
assert_eq!(get_unit_type("baz.timer"), Some("timer".to_string()));
assert_eq!(get_unit_type("test"), Some("test".to_string()));
}
#[test]
fn test_verify_stats_default() {
let stats = VerifyStats::default();
assert_eq!(stats.total, 0);
assert_eq!(stats.by_type.len(), 0);
}
#[test]
fn test_parse_verify_output() {
let stderr = r#"
/usr/lib/systemd/system/foo.service:4: Unknown section 'Service'. Ignoring.
bar.slice: Command /bin/foo is not executable: No such file or directory
Unit baz.timer not found.
test-with-error.target: Some error message here
"#;
let failing = parse_verify_output(stderr);
let mut sorted: Vec<_> = failing.iter().collect();
sorted.sort();
for unit in &sorted {
eprintln!("Found unit: {}", unit);
}
assert!(failing.contains("foo.service"));
assert!(failing.contains("bar.slice"));
assert!(failing.contains("baz.timer"));
assert!(failing.contains("test-with-error.target"));
assert_eq!(failing.len(), 4);
}
}