use anyhow::{Context, Result};
use std::process::{Command, Output};
use tracing::{debug, info, warn};
const SERVICE_NAME: &str = "smirrors.service";
const TIMER_NAME: &str = "smirrors.timer";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceStatus {
Running,
Stopped,
Failed,
Unknown,
}
impl std::fmt::Display for ServiceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Running => write!(f, "running"),
Self::Stopped => write!(f, "stopped"),
Self::Failed => write!(f, "failed"),
Self::Unknown => write!(f, "unknown"),
}
}
}
pub struct SystemdManager {
service_name: String,
timer_name: String,
}
impl SystemdManager {
pub fn new() -> Self {
Self {
service_name: SERVICE_NAME.to_string(),
timer_name: TIMER_NAME.to_string(),
}
}
pub fn start(&self) -> Result<()> {
info!("Starting {} service", self.service_name);
let output = Command::new("systemctl")
.args(&["start", &self.service_name])
.output()
.context("Failed to execute systemctl start")?;
self.check_command_output(output, "start")?;
info!("Service started successfully");
Ok(())
}
pub fn stop(&self) -> Result<()> {
info!("Stopping {} service", self.service_name);
let output = Command::new("systemctl")
.args(&["stop", &self.service_name])
.output()
.context("Failed to execute systemctl stop")?;
self.check_command_output(output, "stop")?;
info!("Service stopped successfully");
Ok(())
}
pub fn restart(&self) -> Result<()> {
info!("Restarting {} service", self.service_name);
let output = Command::new("systemctl")
.args(&["restart", &self.service_name])
.output()
.context("Failed to execute systemctl restart")?;
self.check_command_output(output, "restart")?;
info!("Service restarted successfully");
Ok(())
}
pub fn reload(&self) -> Result<()> {
info!("Reloading {} service configuration", self.service_name);
let output = Command::new("systemctl")
.args(&["reload", &self.service_name])
.output()
.context("Failed to execute systemctl reload")?;
self.check_command_output(output, "reload")?;
info!("Service configuration reloaded successfully");
Ok(())
}
pub fn enable(&self) -> Result<()> {
info!("Enabling {} service", self.service_name);
let output = Command::new("systemctl")
.args(&["enable", &self.service_name])
.output()
.context("Failed to execute systemctl enable")?;
self.check_command_output(output, "enable")?;
info!("Service enabled successfully");
Ok(())
}
pub fn disable(&self) -> Result<()> {
info!("Disabling {} service", self.service_name);
let output = Command::new("systemctl")
.args(&["disable", &self.service_name])
.output()
.context("Failed to execute systemctl disable")?;
self.check_command_output(output, "disable")?;
info!("Service disabled successfully");
Ok(())
}
pub fn status(&self) -> ServiceStatus {
debug!("Checking status of {} service", self.service_name);
let output = match Command::new("systemctl")
.args(&["is-active", &self.service_name])
.output()
{
Ok(output) => output,
Err(e) => {
warn!("Failed to check service status: {}", e);
return ServiceStatus::Unknown;
}
};
let status_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
match status_str.as_str() {
"active" => ServiceStatus::Running,
"inactive" | "dead" => ServiceStatus::Stopped,
"failed" => ServiceStatus::Failed,
_ => ServiceStatus::Unknown,
}
}
pub fn is_running(&self) -> bool {
self.status() == ServiceStatus::Running
}
pub fn is_enabled(&self) -> bool {
let output = match Command::new("systemctl")
.args(&["is-enabled", &self.service_name])
.output()
{
Ok(output) => output,
Err(e) => {
warn!("Failed to check if service is enabled: {}", e);
return false;
}
};
output.status.success()
}
pub fn get_status_info(&self) -> Result<String> {
debug!("Getting detailed status for {}", self.service_name);
let output = Command::new("systemctl")
.args(&["status", &self.service_name, "--no-pager"])
.output()
.context("Failed to execute systemctl status")?;
let status_info = String::from_utf8_lossy(&output.stdout).to_string();
Ok(status_info)
}
pub fn start_timer(&self) -> Result<()> {
info!("Starting {} timer", self.timer_name);
let output = Command::new("systemctl")
.args(&["start", &self.timer_name])
.output()
.context("Failed to execute systemctl start timer")?;
self.check_command_output(output, "start timer")?;
info!("Timer started successfully");
Ok(())
}
pub fn stop_timer(&self) -> Result<()> {
info!("Stopping {} timer", self.timer_name);
let output = Command::new("systemctl")
.args(&["stop", &self.timer_name])
.output()
.context("Failed to execute systemctl stop timer")?;
self.check_command_output(output, "stop timer")?;
info!("Timer stopped successfully");
Ok(())
}
pub fn enable_timer(&self) -> Result<()> {
info!("Enabling {} timer", self.timer_name);
let output = Command::new("systemctl")
.args(&["enable", &self.timer_name])
.output()
.context("Failed to execute systemctl enable timer")?;
self.check_command_output(output, "enable timer")?;
info!("Timer enabled successfully");
Ok(())
}
pub fn disable_timer(&self) -> Result<()> {
info!("Disabling {} timer", self.timer_name);
let output = Command::new("systemctl")
.args(&["disable", &self.timer_name])
.output()
.context("Failed to execute systemctl disable timer")?;
self.check_command_output(output, "disable timer")?;
info!("Timer disabled successfully");
Ok(())
}
pub fn get_logs(&self, lines: usize) -> Result<String> {
debug!("Retrieving {} log lines", lines);
let output = Command::new("journalctl")
.args(&[
"-u",
&self.service_name,
"-n",
&lines.to_string(),
"--no-pager",
])
.output()
.context("Failed to execute journalctl")?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to retrieve logs: {}", error);
}
let logs = String::from_utf8_lossy(&output.stdout).to_string();
Ok(logs)
}
pub fn follow_logs(&self) -> Result<std::process::Child> {
debug!("Starting to follow logs");
let child = Command::new("journalctl")
.args(&["-u", &self.service_name, "-f", "--no-pager"])
.spawn()
.context("Failed to spawn journalctl for log following")?;
Ok(child)
}
pub fn trigger_update(&self) -> Result<()> {
info!("Triggering immediate update");
let output = Command::new("systemctl")
.args(&["kill", "-s", "SIGUSR1", &self.service_name])
.output()
.context("Failed to send SIGUSR1 signal")?;
self.check_command_output(output, "trigger update")?;
info!("Update triggered successfully");
Ok(())
}
pub fn health_check(&self) -> Result<()> {
info!("Running health check");
let output = Command::new("systemctl")
.args(&["kill", "-s", "SIGUSR2", &self.service_name])
.output()
.context("Failed to send SIGUSR2 signal")?;
self.check_command_output(output, "health check")?;
info!("Health check triggered successfully");
Ok(())
}
pub fn daemon_reload() -> Result<()> {
info!("Reloading systemd daemon configuration");
let output = Command::new("systemctl")
.args(&["daemon-reload"])
.output()
.context("Failed to execute systemctl daemon-reload")?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to reload daemon: {}", error);
}
info!("Daemon configuration reloaded successfully");
Ok(())
}
pub fn is_systemd_available() -> bool {
Command::new("systemctl")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn validate_unit_file(unit_file: &str) -> Result<()> {
debug!("Validating unit file: {}", unit_file);
let output = Command::new("systemd-analyze")
.args(&["verify", unit_file])
.output()
.context("Failed to execute systemd-analyze")?;
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Unit file validation failed: {}", error);
}
info!("Unit file is valid");
Ok(())
}
fn check_command_output(&self, output: Output, operation: &str) -> Result<()> {
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to {}: {}", operation, error);
}
Ok(())
}
}
impl Default for SystemdManager {
fn default() -> Self {
Self::new()
}
}
pub fn install_unit_files(service_content: &str, timer_content: &str) -> Result<()> {
info!("Installing systemd unit files");
let systemd_dir = std::path::PathBuf::from("/etc/systemd/system");
std::fs::create_dir_all(&systemd_dir)
.context("Failed to create systemd directory")?;
let service_path = systemd_dir.join(SERVICE_NAME);
std::fs::write(&service_path, service_content)
.context("Failed to write service file")?;
let timer_path = systemd_dir.join(TIMER_NAME);
std::fs::write(&timer_path, timer_content)
.context("Failed to write timer file")?;
SystemdManager::daemon_reload()?;
info!("Systemd unit files installed successfully");
Ok(())
}
pub fn uninstall_unit_files() -> Result<()> {
info!("Uninstalling systemd unit files");
let systemd_dir = std::path::PathBuf::from("/etc/systemd/system");
let service_path = systemd_dir.join(SERVICE_NAME);
if service_path.exists() {
std::fs::remove_file(&service_path)
.context("Failed to remove service file")?;
}
let timer_path = systemd_dir.join(TIMER_NAME);
if timer_path.exists() {
std::fs::remove_file(&timer_path)
.context("Failed to remove timer file")?;
}
SystemdManager::daemon_reload()?;
info!("Systemd unit files uninstalled successfully");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_systemd_manager_creation() {
let manager = SystemdManager::new();
assert_eq!(manager.service_name, SERVICE_NAME);
assert_eq!(manager.timer_name, TIMER_NAME);
}
#[test]
fn test_service_status_display() {
assert_eq!(ServiceStatus::Running.to_string(), "running");
assert_eq!(ServiceStatus::Stopped.to_string(), "stopped");
assert_eq!(ServiceStatus::Failed.to_string(), "failed");
assert_eq!(ServiceStatus::Unknown.to_string(), "unknown");
}
#[test]
fn test_is_systemd_available() {
let _ = SystemdManager::is_systemd_available();
}
}