use crate::{command, config::valid_service_name}; use anyhow::{bail, Result}; use serde::Serialize; use std::{collections::HashMap, time::Duration}; #[derive(Debug, Serialize)] pub struct ServiceStatus { pub name: String, pub active_state: String, pub sub_state: String, pub pid: u32, pub memory_bytes: Option, pub active_since_monotonic_us: Option, } pub async fn list(allowed: &[String], timeout: Duration) -> Vec { let mut result = Vec::with_capacity(allowed.len()); for name in allowed { match inspect(name, timeout).await { Ok(status) => result.push(status), Err(_) => result.push(ServiceStatus { name: name.clone(), active_state: "unavailable".into(), sub_state: "unknown".into(), pid: 0, memory_bytes: None, active_since_monotonic_us: None, }), } } result } pub async fn restart(name: &str, allowed: &[String], timeout: Duration) -> Result<()> { if !valid_service_name(name) || !allowed.iter().any(|item| item == name) { bail!("service is not allow-listed") } command::run("systemctl", &["restart", "--", name], timeout).await?; Ok(()) } async fn inspect(name: &str, timeout: Duration) -> Result { if !valid_service_name(name) { bail!("invalid service name") } let output = command::run( "systemctl", &[ "show", "--no-pager", "--property=ActiveState,SubState,MainPID,MemoryCurrent,ActiveEnterTimestampMonotonic", "--", name, ], timeout, ) .await?; let values: HashMap<_, _> = output .stdout .lines() .filter_map(|line| line.split_once('=')) .collect(); Ok(ServiceStatus { name: name.to_owned(), active_state: values .get("ActiveState") .copied() .unwrap_or("unknown") .to_owned(), sub_state: values .get("SubState") .copied() .unwrap_or("unknown") .to_owned(), pid: values .get("MainPID") .and_then(|v| v.parse().ok()) .unwrap_or_default(), memory_bytes: values.get("MemoryCurrent").and_then(|v| v.parse().ok()), active_since_monotonic_us: values .get("ActiveEnterTimestampMonotonic") .and_then(|v| v.parse().ok()), }) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn rejects_service_outside_allowlist_without_execution() { assert!( restart("postgresql", &["nginx".into()], Duration::from_secs(1)) .await .is_err() ); assert!(restart( "nginx;reboot", &["nginx;reboot".into()], Duration::from_secs(1) ) .await .is_err()); } }