Skip to main content

syncor_core/daemon/
manager.rs

1use std::fs;
2use std::io::Write;
3
4use crate::config::{LinksRegistry, SyncorConfig, SyncorPaths};
5use crate::error::Result;
6
7// ---------------------------------------------------------------------------
8// DaemonManager
9// ---------------------------------------------------------------------------
10
11/// Manages the lifecycle of the Syncor daemon process.
12///
13/// Responsibilities:
14/// - Write / remove the PID file.
15/// - Load the links registry on startup.
16/// - Run a command loop (IPC commands forwarded here once the IPC server is
17///   available; for now a placeholder channel is used).
18/// - Clean up stale PID / socket files left by crashed processes.
19pub struct DaemonManager {
20    paths: SyncorPaths,
21    config: SyncorConfig,
22}
23
24impl DaemonManager {
25    pub fn new(paths: SyncorPaths, config: SyncorConfig) -> Self {
26        Self { paths, config }
27    }
28
29    /// Run the daemon.
30    ///
31    /// 1. Write the PID file.
32    /// 2. Load the links registry.
33    /// 3. Enter the command loop (placeholder channel until the IPC server is wired in).
34    /// 4. Remove the PID file on exit.
35    pub async fn run(&self) -> Result<()> {
36        // Ensure data directories exist.
37        self.paths.ensure_dirs()?;
38
39        // --- Write PID file ---
40        let pid = std::process::id();
41        let pid_path = self.paths.pid_file();
42        {
43            let mut f = fs::File::create(&pid_path)?;
44            writeln!(f, "{}", pid)?;
45        }
46        tracing::info!("daemon started (pid {})", pid);
47
48        // --- Load links registry ---
49        let registry = LinksRegistry::load(&self.paths.links_file())?;
50        let link_count = registry.iter().count();
51        tracing::info!("loaded {} link(s) from registry", link_count);
52
53        // --- Command loop (placeholder) ---
54        // The real IPC server (daemon/server.rs) will send commands over a
55        // tokio channel once it is implemented.  For now we just park the
56        // task and log that we are ready.
57        tracing::info!(
58            "daemon ready (debounce={}s, poll={}s)",
59            self.config.debounce_secs,
60            self.config.default_poll_interval_secs,
61        );
62
63        // Use a one-shot channel as a placeholder for the future shutdown
64        // signal that the IPC server / signal handler will send.
65        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
66        let _ = rx.await; // park until shutdown signal arrives
67
68        // --- Cleanup ---
69        let _ = fs::remove_file(&pid_path);
70        tracing::info!("daemon exited cleanly");
71        Ok(())
72    }
73
74    // -----------------------------------------------------------------------
75    // Static helpers — do not require an instance
76    // -----------------------------------------------------------------------
77
78    /// Returns `true` if a daemon process described by the PID file is alive.
79    pub fn is_running(paths: &SyncorPaths) -> bool {
80        let pid_path = paths.pid_file();
81        if !pid_path.exists() {
82            return false;
83        }
84
85        // Read PID from file.
86        let pid: i32 = match fs::read_to_string(&pid_path)
87            .ok()
88            .and_then(|s| s.trim().parse().ok())
89        {
90            Some(p) => p,
91            None => return false,
92        };
93
94        // Use `kill(pid, 0)` — succeeds (returns 0) if the process exists.
95        (unsafe { libc::kill(pid, 0) }) == 0
96    }
97
98    /// Remove a stale PID file and Unix-domain socket that were left behind
99    /// by a previously crashed daemon.
100    pub fn cleanup_stale(paths: &SyncorPaths) {
101        let pid_path = paths.pid_file();
102        let sock_path = paths.socket_path();
103
104        if pid_path.exists() {
105            let _ = fs::remove_file(&pid_path);
106            tracing::debug!("removed stale pid file: {}", pid_path.display());
107        }
108        if sock_path.exists() {
109            let _ = fs::remove_file(&sock_path);
110            tracing::debug!("removed stale socket: {}", sock_path.display());
111        }
112    }
113}