use crate::config::ConfigParser;
use crate::constants::{time, windows_tasks};
use crate::error::{BakerError, Result};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct DaemonStatus {
pub installed: bool,
pub backup: bool,
pub restore: bool,
pub schedule: Option<String>,
pub exercise_restore: Option<bool>,
pub platform: String,
}
pub struct DaemonManager {
backup_service_name: String,
restore_service_name: String,
executable_path: PathBuf,
config_path: Option<String>,
}
impl DaemonManager {
pub fn new(config_path: Option<&str>) -> Self {
let executable_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("baker"));
Self {
backup_service_name: "com.baker.backup".to_string(),
restore_service_name: "com.baker.restore".to_string(),
executable_path,
config_path: config_path.map(String::from),
}
}
pub fn is_installed(&self) -> DaemonStatus {
let platform = std::env::consts::OS.to_string();
let mut status = DaemonStatus {
installed: false,
backup: false,
restore: false,
schedule: None,
exercise_restore: None,
platform: platform.clone(),
};
if let Ok(parser) = ConfigParser::new(self.config_path.as_deref()) {
if let Some(schedule) = &parser.get_config().daemon_schedule {
status.schedule = Some(schedule.every.clone());
status.exercise_restore = Some(schedule.exercise_restore);
}
}
match platform.as_str() {
"macos" => {
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.unwrap_or_default();
let backup_plist_path = plist_dir.join(format!("{}.plist", self.backup_service_name));
let restore_plist_path =
plist_dir.join(format!("{}.plist", self.restore_service_name));
status.backup = backup_plist_path.exists();
status.restore = restore_plist_path.exists();
status.installed = status.backup || status.restore;
}
"linux" => {
let systemd_dir = dirs::home_dir()
.map(|h| h.join(".config/systemd/user"))
.unwrap_or_default();
let backup_service_path = systemd_dir.join("baker-backup.service");
let backup_timer_path = systemd_dir.join("baker-backup.timer");
let restore_service_path = systemd_dir.join("baker-restore.service");
let restore_timer_path = systemd_dir.join("baker-restore.timer");
status.backup = backup_service_path.exists() && backup_timer_path.exists();
status.restore = restore_service_path.exists() && restore_timer_path.exists();
status.installed = status.backup || status.restore;
}
"windows" => {
status.backup = Command::new("schtasks")
.args(["/Query", "/TN", windows_tasks::BACKUP])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
status.restore = Command::new("schtasks")
.args(["/Query", "/TN", windows_tasks::RESTORE])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
status.installed = status.backup || status.restore;
}
_ => {}
}
status
}
pub fn install(&self) -> Result<()> {
let platform = std::env::consts::OS;
println!("Installing baker daemon for {}...", platform);
let parser = ConfigParser::new(self.config_path.as_deref())?;
let config = parser.get_config();
let schedule = config.daemon_schedule.as_ref().ok_or_else(|| {
BakerError::Daemon("daemon_schedule is not configured in config file".to_string())
})?;
println!(" Schedule: {}", schedule.every);
if schedule.exercise_restore {
println!(" Exercise restore: enabled");
}
match platform {
"macos" => self.install_macos(schedule)?,
"linux" => self.install_linux(schedule)?,
"windows" => self.install_windows(schedule)?,
_ => return Err(BakerError::UnsupportedPlatform(platform.to_string())),
}
println!("\nDaemon installed successfully!");
println!("\nTo check status: baker daemon-status");
Ok(())
}
pub fn uninstall(&self) -> Result<()> {
let platform = std::env::consts::OS;
println!("Uninstalling baker daemon for {}...", platform);
match platform {
"macos" => self.uninstall_macos()?,
"linux" => self.uninstall_linux()?,
"windows" => self.uninstall_windows()?,
_ => return Err(BakerError::UnsupportedPlatform(platform.to_string())),
}
println!("\nDaemon uninstalled successfully!");
Ok(())
}
pub fn status(&self) -> Result<()> {
let platform = std::env::consts::OS;
println!("Checking daemon status for {}...\n", platform);
match platform {
"macos" => self.status_macos()?,
"linux" => self.status_linux()?,
"windows" => self.status_windows()?,
_ => return Err(BakerError::UnsupportedPlatform(platform.to_string())),
}
Ok(())
}
fn parse_interval(&self, interval: &str) -> Result<u64> {
let re = regex::Regex::new(r"^(\d+)(s|m|h|d)$").unwrap();
let caps = re
.captures(interval)
.ok_or_else(|| BakerError::InvalidInterval(interval.to_string()))?;
let value: u64 = caps[1].parse().map_err(|_| {
BakerError::InvalidInterval(interval.to_string())
})?;
let unit = &caps[2];
Ok(match unit {
"s" => value,
"m" => value * time::SECONDS_PER_MINUTE,
"h" => value * time::SECONDS_PER_HOUR,
"d" => value * time::SECONDS_PER_DAY,
_ => return Err(BakerError::InvalidInterval(interval.to_string())),
})
}
fn generate_macos_plist(
&self,
command: &str,
service_name: &str,
interval_seconds: u64,
) -> String {
let logs_dir = dirs::home_dir()
.map(|h| h.join("Library/Logs"))
.unwrap_or_default();
let mut program_args = vec![
format!(" <string>{}</string>", self.executable_path.display()),
format!(" <string>{}</string>", command),
];
if let Some(config_path) = &self.config_path {
program_args.push(" <string>--config</string>".to_string());
program_args.push(format!(" <string>{}</string>", config_path));
}
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{}</string>
<key>ProgramArguments</key>
<array>
{}
</array>
<key>StartInterval</key>
<integer>{}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>{}</string>
<key>StandardErrorPath</key>
<string>{}</string>
</dict>
</plist>"#,
service_name,
program_args.join("\n"),
interval_seconds,
logs_dir.join(format!("baker-{}.log", command)).display(),
logs_dir.join(format!("baker-{}.error.log", command)).display()
)
}
fn generate_systemd_service(&self, command: &str) -> String {
let config_arg = self
.config_path
.as_ref()
.map(|p| format!("--config \"{}\"", p))
.unwrap_or_default();
let label = command.chars().next().unwrap().to_uppercase().to_string()
+ &command[1..];
format!(
r#"[Unit]
Description=Baker {} Service
After=network.target
[Service]
Type=oneshot
ExecStart={} {} {}
[Install]
WantedBy=default.target"#,
label,
self.executable_path.display(),
command,
config_arg
)
}
fn generate_systemd_timer(&self, command: &str, interval: &str) -> String {
let label = command.chars().next().unwrap().to_uppercase().to_string()
+ &command[1..];
format!(
r#"[Unit]
Description=Baker {} Timer
Requires=baker-{}.service
[Timer]
OnBootSec=5min
OnUnitActiveSec={}
Persistent=true
[Install]
WantedBy=timers.target"#,
label, command, interval
)
}
fn install_macos(&self, schedule: &crate::config::DaemonSchedule) -> Result<()> {
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let plist_path = plist_dir.join(format!("{}.plist", self.backup_service_name));
let logs_dir = dirs::home_dir()
.map(|h| h.join("Library/Logs"))
.unwrap_or_default();
fs::create_dir_all(&plist_dir)?;
fs::create_dir_all(&logs_dir)?;
let interval_seconds = self.parse_interval(&schedule.every)?;
let plist_content =
self.generate_macos_plist("backup", &self.backup_service_name, interval_seconds);
fs::write(&plist_path, plist_content)?;
println!(" Created backup plist: {}", plist_path.display());
println!(" Logs: {}", logs_dir.join("baker-backup.log").display());
if schedule.exercise_restore {
self.install_macos_restore(schedule)?;
}
let load_result = Command::new("launchctl")
.args(["load", plist_path.to_str().unwrap_or("")])
.output();
if load_result.map(|o| o.status.success()).unwrap_or(false) {
println!(" Loaded backup agent");
} else {
println!(" Warning: Failed to load backup agent. You may need to load it manually:");
println!(" launchctl load {}", plist_path.display());
}
Ok(())
}
fn install_macos_restore(&self, schedule: &crate::config::DaemonSchedule) -> Result<()> {
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let plist_path = plist_dir.join(format!("{}.plist", self.restore_service_name));
let interval_seconds = self.parse_interval(&schedule.every)?;
let plist_content =
self.generate_macos_plist("restore", &self.restore_service_name, interval_seconds);
fs::write(&plist_path, plist_content)?;
println!(" Created restore plist: {}", plist_path.display());
let load_result = Command::new("launchctl")
.args(["load", plist_path.to_str().unwrap_or("")])
.output();
if load_result.map(|o| o.status.success()).unwrap_or(false) {
println!(" Loaded restore agent");
} else {
println!(" Warning: Failed to load restore agent");
}
Ok(())
}
fn uninstall_macos(&self) -> Result<()> {
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let backup_plist_path = plist_dir.join(format!("{}.plist", self.backup_service_name));
let restore_plist_path = plist_dir.join(format!("{}.plist", self.restore_service_name));
let mut removed = false;
if backup_plist_path.exists() {
let _ = Command::new("launchctl")
.args(["unload", backup_plist_path.to_str().unwrap_or("")])
.output();
fs::remove_file(&backup_plist_path)?;
println!(" Removed: {}", backup_plist_path.display());
removed = true;
}
if restore_plist_path.exists() {
let _ = Command::new("launchctl")
.args(["unload", restore_plist_path.to_str().unwrap_or("")])
.output();
fs::remove_file(&restore_plist_path)?;
println!(" Removed: {}", restore_plist_path.display());
removed = true;
}
if !removed {
println!(" Daemon not installed");
}
Ok(())
}
fn status_macos(&self) -> Result<()> {
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let backup_plist_path = plist_dir.join(format!("{}.plist", self.backup_service_name));
let restore_plist_path = plist_dir.join(format!("{}.plist", self.restore_service_name));
let backup_installed = backup_plist_path.exists();
let restore_installed = restore_plist_path.exists();
if !backup_installed && !restore_installed {
println!("No daemons installed");
return Ok(());
}
if backup_installed {
println!("Backup daemon installed");
println!(" Plist: {}", backup_plist_path.display());
let list_result = Command::new("launchctl")
.args(["list"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains(&self.backup_service_name))
.unwrap_or(false);
if list_result {
println!(" Status: Running");
} else {
println!(" Status: Not loaded");
println!(" Load with: launchctl load {}", backup_plist_path.display());
}
let log_path = dirs::home_dir()
.map(|h| h.join("Library/Logs/baker-backup.log"))
.unwrap_or_default();
if log_path.exists() {
println!(" Logs: {}", log_path.display());
}
}
if restore_installed {
println!("\nRestore daemon installed");
println!(" Plist: {}", restore_plist_path.display());
let list_result = Command::new("launchctl")
.args(["list"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains(&self.restore_service_name))
.unwrap_or(false);
if list_result {
println!(" Status: Running");
} else {
println!(" Status: Not loaded");
println!(" Load with: launchctl load {}", restore_plist_path.display());
}
let log_path = dirs::home_dir()
.map(|h| h.join("Library/Logs/baker-restore.log"))
.unwrap_or_default();
if log_path.exists() {
println!(" Logs: {}", log_path.display());
}
}
Ok(())
}
fn install_linux(&self, schedule: &crate::config::DaemonSchedule) -> Result<()> {
let systemd_dir = dirs::home_dir()
.map(|h| h.join(".config/systemd/user"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let backup_service_path = systemd_dir.join("baker-backup.service");
let backup_timer_path = systemd_dir.join("baker-backup.timer");
fs::create_dir_all(&systemd_dir)?;
fs::write(&backup_service_path, self.generate_systemd_service("backup"))?;
println!(" Created backup service: {}", backup_service_path.display());
fs::write(
&backup_timer_path,
self.generate_systemd_timer("backup", &schedule.every),
)?;
println!(" Created backup timer: {}", backup_timer_path.display());
if schedule.exercise_restore {
self.install_linux_restore(schedule)?;
}
let reload = Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output();
let enable = Command::new("systemctl")
.args(["--user", "enable", "baker-backup.timer"])
.output();
let start = Command::new("systemctl")
.args(["--user", "start", "baker-backup.timer"])
.output();
if reload.is_ok() && enable.is_ok() && start.is_ok() {
println!(" Enabled and started backup timer");
if schedule.exercise_restore {
let _ = Command::new("systemctl")
.args(["--user", "enable", "baker-restore.timer"])
.output();
let _ = Command::new("systemctl")
.args(["--user", "start", "baker-restore.timer"])
.output();
println!(" Enabled and started restore timer");
}
} else {
println!("\n Warning: Failed to enable timers automatically.");
println!(" Run these commands manually:");
println!(" systemctl --user daemon-reload");
println!(" systemctl --user enable --now baker-backup.timer");
if schedule.exercise_restore {
println!(" systemctl --user enable --now baker-restore.timer");
}
}
Ok(())
}
fn install_linux_restore(&self, schedule: &crate::config::DaemonSchedule) -> Result<()> {
let systemd_dir = dirs::home_dir()
.map(|h| h.join(".config/systemd/user"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let restore_service_path = systemd_dir.join("baker-restore.service");
let restore_timer_path = systemd_dir.join("baker-restore.timer");
fs::write(&restore_service_path, self.generate_systemd_service("restore"))?;
println!(" Created restore service: {}", restore_service_path.display());
fs::write(
&restore_timer_path,
self.generate_systemd_timer("restore", &schedule.every),
)?;
println!(" Created restore timer: {}", restore_timer_path.display());
Ok(())
}
fn uninstall_linux(&self) -> Result<()> {
let systemd_dir = dirs::home_dir()
.map(|h| h.join(".config/systemd/user"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let backup_service_path = systemd_dir.join("baker-backup.service");
let backup_timer_path = systemd_dir.join("baker-backup.timer");
let restore_service_path = systemd_dir.join("baker-restore.service");
let restore_timer_path = systemd_dir.join("baker-restore.timer");
let mut removed = false;
if backup_timer_path.exists() || backup_service_path.exists() {
let _ = Command::new("systemctl")
.args(["--user", "stop", "baker-backup.timer"])
.output();
let _ = Command::new("systemctl")
.args(["--user", "disable", "baker-backup.timer"])
.output();
if backup_service_path.exists() {
fs::remove_file(&backup_service_path)?;
println!(" Removed: {}", backup_service_path.display());
}
if backup_timer_path.exists() {
fs::remove_file(&backup_timer_path)?;
println!(" Removed: {}", backup_timer_path.display());
}
removed = true;
}
if restore_timer_path.exists() || restore_service_path.exists() {
let _ = Command::new("systemctl")
.args(["--user", "stop", "baker-restore.timer"])
.output();
let _ = Command::new("systemctl")
.args(["--user", "disable", "baker-restore.timer"])
.output();
if restore_service_path.exists() {
fs::remove_file(&restore_service_path)?;
println!(" Removed: {}", restore_service_path.display());
}
if restore_timer_path.exists() {
fs::remove_file(&restore_timer_path)?;
println!(" Removed: {}", restore_timer_path.display());
}
removed = true;
}
if removed {
let _ = Command::new("systemctl")
.args(["--user", "daemon-reload"])
.output();
} else {
println!(" Daemon not installed");
}
Ok(())
}
fn status_linux(&self) -> Result<()> {
let systemd_dir = dirs::home_dir()
.map(|h| h.join(".config/systemd/user"))
.ok_or_else(|| BakerError::Daemon("Cannot find home directory".to_string()))?;
let backup_service_path = systemd_dir.join("baker-backup.service");
let backup_timer_path = systemd_dir.join("baker-backup.timer");
let restore_service_path = systemd_dir.join("baker-restore.service");
let restore_timer_path = systemd_dir.join("baker-restore.timer");
let backup_installed = backup_service_path.exists() && backup_timer_path.exists();
let restore_installed = restore_service_path.exists() && restore_timer_path.exists();
if !backup_installed && !restore_installed {
println!("No daemons installed");
return Ok(());
}
if backup_installed {
println!("Backup daemon installed");
println!(" Service: {}", backup_service_path.display());
println!(" Timer: {}", backup_timer_path.display());
println!("\nBackup timer status:");
if let Ok(output) = Command::new("systemctl")
.args(["--user", "status", "baker-backup.timer"])
.output()
{
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
println!(" Warning: Failed to get backup timer status");
}
}
if restore_installed {
println!("\nRestore daemon installed");
println!(" Service: {}", restore_service_path.display());
println!(" Timer: {}", restore_timer_path.display());
println!("\nRestore timer status:");
if let Ok(output) = Command::new("systemctl")
.args(["--user", "status", "baker-restore.timer"])
.output()
{
println!("{}", String::from_utf8_lossy(&output.stdout));
} else {
println!(" Warning: Failed to get restore timer status");
}
}
Ok(())
}
fn install_windows(&self, schedule: &crate::config::DaemonSchedule) -> Result<()> {
let interval_seconds = self.parse_interval(&schedule.every)?;
let interval_minutes = (interval_seconds / time::SECONDS_PER_MINUTE).max(1);
let config_arg = self
.config_path
.as_ref()
.map(|p| format!("--config \"{}\"", p))
.unwrap_or_default();
let backup_result = Command::new("schtasks")
.args([
"/Create",
"/TN",
windows_tasks::BACKUP,
"/TR",
&format!(
"\"{}\" backup {}",
self.executable_path.display(),
config_arg
),
"/SC",
"MINUTE",
"/MO",
&interval_minutes.to_string(),
"/F",
])
.output();
if backup_result.map(|o| o.status.success()).unwrap_or(false) {
println!(" Created backup scheduled task: {}", windows_tasks::BACKUP);
println!(" Interval: every {} minute(s)", interval_minutes);
} else {
return Err(BakerError::Daemon(
"Failed to create backup scheduled task. Make sure you have appropriate privileges."
.to_string(),
));
}
if schedule.exercise_restore {
let restore_result = Command::new("schtasks")
.args([
"/Create",
"/TN",
windows_tasks::RESTORE,
"/TR",
&format!(
"\"{}\" restore {}",
self.executable_path.display(),
config_arg
),
"/SC",
"MINUTE",
"/MO",
&interval_minutes.to_string(),
"/F",
])
.output();
if restore_result.map(|o| o.status.success()).unwrap_or(false) {
println!(" Created restore scheduled task: {}", windows_tasks::RESTORE);
} else {
println!(" Warning: Failed to create restore task");
}
}
Ok(())
}
fn uninstall_windows(&self) -> Result<()> {
let backup_deleted = Command::new("schtasks")
.args(["/Delete", "/TN", windows_tasks::BACKUP, "/F"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if backup_deleted {
println!(" Deleted backup scheduled task: {}", windows_tasks::BACKUP);
} else {
println!(" Backup task not found: {}", windows_tasks::BACKUP);
}
let restore_deleted = Command::new("schtasks")
.args(["/Delete", "/TN", windows_tasks::RESTORE, "/F"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if restore_deleted {
println!(" Deleted restore scheduled task: {}", windows_tasks::RESTORE);
}
if !backup_deleted && !restore_deleted {
println!(" Daemon not installed");
}
Ok(())
}
fn status_windows(&self) -> Result<()> {
let backup_result = Command::new("schtasks")
.args(["/Query", "/TN", windows_tasks::BACKUP, "/FO", "LIST", "/V"])
.output();
let backup_installed = backup_result
.as_ref()
.map(|o| o.status.success())
.unwrap_or(false);
if backup_installed {
println!("Backup daemon installed\n");
println!("Backup task details:");
if let Ok(output) = backup_result {
println!("{}", String::from_utf8_lossy(&output.stdout));
}
} else {
println!("Backup daemon not installed");
}
let restore_result = Command::new("schtasks")
.args(["/Query", "/TN", windows_tasks::RESTORE, "/FO", "LIST", "/V"])
.output();
let restore_installed = restore_result
.as_ref()
.map(|o| o.status.success())
.unwrap_or(false);
if restore_installed {
println!("\nRestore daemon installed\n");
println!("Restore task details:");
if let Ok(output) = restore_result {
println!("{}", String::from_utf8_lossy(&output.stdout));
}
} else if backup_installed {
println!("\nRestore daemon not installed");
}
if !backup_installed && !restore_installed {
println!("No daemons installed");
}
Ok(())
}
}