use std::{fs, path::PathBuf};
use nix::{
sys::signal::{self, Signal},
unistd::Pid,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, error, info};
use tracing_appender::rolling;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use super::*;
#[cfg(target_os = "linux")] pub mod linux;
#[cfg(target_os = "linux")] pub use linux::*;
#[cfg(target_os = "macos")] pub mod macos;
#[cfg(target_os = "macos")] pub use macos::*;
#[derive(Subcommand, Clone, Copy)]
pub enum DaemonCommands {
Start,
Stop,
Restart,
Install,
Uninstall,
Status,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Daemon {
pub pid_file: PathBuf,
pub working_dir: PathBuf,
pub log_dir: PathBuf,
}
impl Default for Daemon {
fn default() -> Self {
Self {
pid_file: PathBuf::from(DEFAULT_PID_FILE),
working_dir: PathBuf::from(DEFAULT_WORKING_DIR),
log_dir: PathBuf::from(DEFAULT_LOG_DIR),
}
}
}
impl Daemon {
pub fn new() -> Self { Self::default() }
pub fn start(&self) -> Result<()> {
fs::create_dir_all(&self.working_dir)?;
fs::create_dir_all(&self.log_dir)?;
let file_appender = rolling::RollingFileAppender::builder()
.rotation(rolling::Rotation::DAILY)
.filename_prefix("learnerd")
.filename_suffix("log")
.build(&self.log_dir)?;
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(file_appender)
.with_ansi(false)
.with_thread_ids(true)
.with_target(true)
.with_file(true)
.with_line_number(true);
let stdout_layer = tracing_subscriber::fmt::layer().with_ansi(false).with_target(true);
tracing_subscriber::registry()
.with(file_layer)
.with(stdout_layer)
.with(EnvFilter::new("debug"))
.init();
info!("Starting learnerd daemon");
debug!("Using config: {:?}", self);
info!("Daemon started successfully");
self.run()
}
pub fn stop(&self) -> Result<()> {
if let Ok(pid) = fs::read_to_string(&self.pid_file) {
let pid: i32 = pid.trim().parse().map_err(|e: std::num::ParseIntError| {
LearnerdError::Daemon(format!("pid.trim().parse() gave error: {}", e))
})?;
#[cfg(unix)]
{
if let Err(e) = signal::kill(Pid::from_raw(pid), Signal::SIGTERM) {
error!("Failed to send SIGTERM to process: {}", e);
return Err(LearnerdError::Daemon(format!("Failed to stop daemon: {}", e)));
}
}
if let Err(e) = fs::remove_file(&self.pid_file) {
error!("Failed to remove PID file: {}", e);
}
Ok(())
} else {
error!("PID file not found");
Err(LearnerdError::Daemon("Daemon not running".to_string()))
}
}
pub fn restart(&self) -> Result<()> {
self.stop()?;
std::thread::sleep(std::time::Duration::from_secs(1));
self.start()
}
pub fn install(&self) -> Result<()> { install_system_daemon(self) }
pub fn uninstall(&self) -> Result<()> { uninstall_system_daemon() }
fn run(&self) -> Result<()> {
info!("Daemon running");
loop {
std::thread::sleep(std::time::Duration::from_secs(5));
debug!("Daemon heartbeat");
}
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
fn setup_test_daemon() -> (Daemon, tempfile::TempDir) {
let test_dir = tempdir().expect("Failed to create temp directory");
let daemon = Daemon {
pid_file: test_dir.path().join("test.pid"),
working_dir: test_dir.path().join("work"),
log_dir: test_dir.path().join("logs"),
};
(daemon, test_dir)
}
#[test]
fn test_daemon_directory_creation() {
let (daemon, _temp) = setup_test_daemon();
let daemon_clone = daemon.clone();
let _handle = std::thread::spawn(move || daemon.start());
std::thread::sleep(std::time::Duration::from_secs(5));
assert!(daemon_clone.working_dir.exists(), "Working directory should be created");
assert!(daemon_clone.log_dir.exists(), "Log directory should be created");
}
}