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}