use crate::config::Config;
use crate::core::{MirrorTester, MirrorUpdater};
use crate::distro::{self, DistroHandler};
use crate::storage::Database;
use crate::utils;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::RwLock;
use tokio::time::interval;
use tracing::{debug, error, info, warn};
const LOCK_FILE: &str = "/var/run/smirrors.lock";
const PID_FILE: &str = "/var/run/smirrors.pid";
pub struct Daemon {
config: Arc<RwLock<Config>>,
database: Database,
running: Arc<RwLock<bool>>,
lock_file: Option<PathBuf>,
pid_file: Option<PathBuf>,
}
impl Daemon {
pub fn new(config: Config) -> Result<Self> {
info!("Initializing SMirrors daemon");
let db_path = Config::data_dir()?.join("smirrors.db");
let database = Database::new(&db_path)
.context("Failed to initialize database")?;
Ok(Self {
config: Arc::new(RwLock::new(config)),
database,
running: Arc::new(RwLock::new(true)),
lock_file: None,
pid_file: None,
})
}
fn acquire_lock(&mut self) -> Result<()> {
let lock_path = PathBuf::from(LOCK_FILE);
if lock_path.exists() {
let pid_str = std::fs::read_to_string(&lock_path)
.context("Failed to read lock file")?;
if let Ok(pid) = pid_str.trim().parse::<i32>() {
if Self::is_process_running(pid) {
anyhow::bail!(
"Another instance is already running with PID {}",
pid
);
} else {
warn!("Stale lock file found, removing");
std::fs::remove_file(&lock_path)?;
}
}
}
let pid = std::process::id();
std::fs::write(&lock_path, pid.to_string())
.context("Failed to create lock file")?;
self.lock_file = Some(lock_path);
let pid_path = PathBuf::from(PID_FILE);
std::fs::write(&pid_path, pid.to_string())
.context("Failed to create PID file")?;
self.pid_file = Some(pid_path);
info!("Daemon lock acquired (PID: {})", pid);
Ok(())
}
fn is_process_running(pid: i32) -> bool {
use std::process::Command;
Command::new("kill")
.args(&["-0", &pid.to_string()])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn release_lock(&mut self) {
if let Some(ref lock_path) = self.lock_file {
if let Err(e) = std::fs::remove_file(lock_path) {
warn!("Failed to remove lock file: {}", e);
} else {
debug!("Lock file released");
}
}
if let Some(ref pid_path) = self.pid_file {
if let Err(e) = std::fs::remove_file(pid_path) {
warn!("Failed to remove PID file: {}", e);
}
}
}
pub async fn run(&mut self) -> Result<()> {
info!("Starting SMirrors daemon");
self.acquire_lock()
.context("Failed to acquire daemon lock")?;
let mut sigterm = signal(SignalKind::terminate())
.context("Failed to setup SIGTERM handler")?;
let mut sigint = signal(SignalKind::interrupt())
.context("Failed to setup SIGINT handler")?;
let mut sighup = signal(SignalKind::hangup())
.context("Failed to setup SIGHUP handler")?;
let mut sigusr1 = signal(SignalKind::user_defined1())
.context("Failed to setup SIGUSR1 handler")?;
let mut sigusr2 = signal(SignalKind::user_defined2())
.context("Failed to setup SIGUSR2 handler")?;
let running = Arc::clone(&self.running);
let config = Arc::clone(&self.config);
let update_interval_secs = {
let cfg = config.read().await;
utils::parse_duration(&cfg.general.update_interval)?
};
let update_interval = std::time::Duration::from_secs(update_interval_secs);
let mut update_timer = interval(update_interval);
update_timer.tick().await;
info!("Daemon started, update interval: {:?}", update_interval);
loop {
tokio::select! {
_ = sigterm.recv() => {
info!("Received SIGTERM, shutting down gracefully");
break;
}
_ = sigint.recv() => {
info!("Received SIGINT, shutting down gracefully");
break;
}
_ = sighup.recv() => {
info!("Received SIGHUP, reloading configuration");
if let Err(e) = self.reload_config().await {
error!("Failed to reload configuration: {}", e);
}
let new_interval_secs = {
let cfg = config.read().await;
utils::parse_duration(&cfg.general.update_interval)?
};
let new_interval = std::time::Duration::from_secs(new_interval_secs);
if new_interval != update_interval {
info!("Update interval changed to {:?}", new_interval);
update_timer = interval(new_interval);
update_timer.tick().await;
}
}
_ = sigusr1.recv() => {
info!("Received SIGUSR1, triggering immediate update");
if let Err(e) = self.run_update().await {
error!("Update failed: {}", e);
}
}
_ = sigusr2.recv() => {
info!("Received SIGUSR2, running health check");
self.health_check().await;
}
_ = update_timer.tick() => {
let cfg = config.read().await;
if cfg.general.auto_update {
drop(cfg); info!("Running scheduled update");
if let Err(e) = self.run_update().await {
error!("Scheduled update failed: {}", e);
}
} else {
debug!("Auto-update is disabled, skipping");
}
}
else => {
if !*running.read().await {
info!("Shutdown requested");
break;
}
}
}
}
info!("Performing graceful shutdown");
self.shutdown().await?;
Ok(())
}
async fn run_update(&self) -> Result<()> {
info!("Starting mirror update");
let start_time = std::time::Instant::now();
let config = self.config.read().await.clone();
let distro_handler = distro::detect_handler()
.context("Failed to detect distribution")?;
info!("Detected distribution: {}", distro_handler.name());
let mirrors = distro_handler
.get_available_mirrors()
.await
.context("Failed to fetch available mirrors")?;
info!("Found {} available mirrors", mirrors.len());
let tester = MirrorTester::from_config(&config)?;
let test_results = tester.test_all(mirrors, None).await;
for result in &test_results {
if let Err(e) = self.database.save_test_result(result) {
warn!("Failed to save test result: {}", e);
}
}
let successful = MirrorTester::filter_successful(test_results);
let ranked = MirrorTester::sort_by_score(successful);
info!("Successfully tested {} mirrors", ranked.len());
let mirrors_to_use: Vec<_> = ranked
.into_iter()
.take(config.testing.max_mirrors)
.map(|r| r.mirror)
.collect();
if mirrors_to_use.is_empty() {
warn!("No working mirrors found, keeping current configuration");
self.database.save_update_record(
0,
false,
Some("No working mirrors found".to_string()),
)?;
return Ok(());
}
let distro_handler_arc: Arc<dyn DistroHandler> = Arc::from(distro_handler);
let updater = MirrorUpdater::new(config.clone(), distro_handler_arc)
.context("Failed to create mirror updater")?;
let update_result = updater
.update(&Default::default())
.await
.context("Failed to update mirrors")?;
let changed_count = update_result.mirrors_selected;
self.database.save_update_record(
changed_count as i64,
true,
None,
)?;
let elapsed = start_time.elapsed();
info!(
"Mirror update completed successfully in {:.2}s ({} mirrors changed)",
elapsed.as_secs_f64(),
changed_count
);
Ok(())
}
async fn reload_config(&self) -> Result<()> {
info!("Reloading configuration");
let new_config = Config::load()
.context("Failed to load configuration")?;
let mut config = self.config.write().await;
*config = new_config;
info!("Configuration reloaded successfully");
Ok(())
}
async fn health_check(&self) {
info!("Running health check");
match self.database.get_stats() {
Ok(stats) => {
info!(
"Database OK: {} mirrors, {} test results",
stats.mirrors_count, stats.test_results_count
);
}
Err(e) => {
error!("Database health check failed: {}", e);
}
}
let config = self.config.read().await;
match config.validate() {
Ok(_) => info!("Configuration OK"),
Err(e) => error!("Configuration validation failed: {}", e),
}
info!("Health check completed");
}
async fn shutdown(&mut self) -> Result<()> {
info!("Shutting down daemon");
let mut running = self.running.write().await;
*running = false;
drop(running);
self.release_lock();
if let Err(e) = self.database.vacuum() {
warn!("Failed to vacuum database during shutdown: {}", e);
}
info!("Daemon shutdown complete");
Ok(())
}
pub async fn stop(&self) {
info!("Stop requested");
let mut running = self.running.write().await;
*running = false;
}
pub async fn is_running(&self) -> bool {
*self.running.read().await
}
}
impl Drop for Daemon {
fn drop(&mut self) {
self.release_lock();
}
}
pub async fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
let config = if let Some(path) = config_path {
Config::load_from(&path)?
} else {
Config::load()?
};
let mut daemon = Daemon::new(config)?;
daemon.run().await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_creation() {
let config = Config::default();
}
#[test]
fn test_is_process_running() {
let current_pid = std::process::id() as i32;
assert!(Daemon::is_process_running(current_pid));
assert!(!Daemon::is_process_running(99999999));
}
}