Skip to main content

bitrouter_runtime/daemon/
mod.rs

1mod pid;
2
3#[cfg(unix)]
4mod unix;
5#[cfg(windows)]
6mod windows;
7
8#[cfg(unix)]
9use unix as platform;
10#[cfg(windows)]
11use windows as platform;
12
13use pid::PidFile;
14
15use std::time::Duration;
16
17use crate::error::{Result, RuntimeError};
18use crate::paths::RuntimePaths;
19
20const STOP_TIMEOUT: Duration = Duration::from_secs(10);
21const STOP_POLL_INTERVAL: Duration = Duration::from_millis(100);
22
23/// Manages the lifecycle of the bitrouter daemon process via PID-file tracking
24/// and platform-specific process control.
25pub struct DaemonManager {
26    paths: RuntimePaths,
27    pid_file: PidFile,
28}
29
30impl DaemonManager {
31    pub fn new(paths: RuntimePaths) -> Self {
32        let pid_file = PidFile::new(&paths.runtime_dir);
33        Self { paths, pid_file }
34    }
35
36    /// Returns the PID of the running daemon, or `None` if it is not running.
37    /// Cleans up stale PID files when the recorded process no longer exists.
38    pub fn is_running(&self) -> Result<Option<u32>> {
39        if let Some(pid) = self.pid_file.read()? {
40            if platform::is_process_alive(pid) {
41                return Ok(Some(pid));
42            }
43            // Stale PID file — clean up
44            self.pid_file.remove()?;
45        }
46        Ok(None)
47    }
48
49    /// Spawn the daemon, returning the new PID.
50    ///
51    /// Fails if the daemon is already running.
52    pub async fn start(&self) -> Result<u32> {
53        if let Some(pid) = self.is_running()? {
54            return Err(RuntimeError::Daemon(format!(
55                "daemon is already running (pid {pid})"
56            )));
57        }
58
59        let pid = platform::spawn_daemon(&self.paths)?;
60        self.pid_file.write(pid)?;
61
62        // Brief wait to verify the process didn't exit immediately.
63        tokio::time::sleep(Duration::from_millis(200)).await;
64
65        if !platform::is_process_alive(pid) {
66            self.pid_file.remove()?;
67            return Err(RuntimeError::Daemon(
68                "daemon process exited immediately after start".into(),
69            ));
70        }
71
72        tracing::info!(pid, "daemon started");
73        Ok(pid)
74    }
75
76    /// Stop the running daemon gracefully (SIGTERM / taskkill), falling back to
77    /// a forced kill after [`STOP_TIMEOUT`].
78    pub async fn stop(&self) -> Result<()> {
79        let pid = match self.is_running()? {
80            Some(pid) => pid,
81            None => {
82                return Err(RuntimeError::Daemon("daemon is not running".into()));
83            }
84        };
85
86        platform::signal_stop(pid)?;
87
88        // Poll until the process exits or the timeout elapses.
89        let deadline = tokio::time::Instant::now() + STOP_TIMEOUT;
90        loop {
91            if !platform::is_process_alive(pid) {
92                break;
93            }
94            if tokio::time::Instant::now() >= deadline {
95                tracing::warn!(pid, "daemon did not stop gracefully, force killing");
96                platform::signal_kill(pid)?;
97                tokio::time::sleep(Duration::from_millis(500)).await;
98                break;
99            }
100            tokio::time::sleep(STOP_POLL_INTERVAL).await;
101        }
102
103        self.pid_file.remove()?;
104        tracing::info!(pid, "daemon stopped");
105        Ok(())
106    }
107
108    /// Stop any running daemon and start a fresh one, returning the new PID.
109    ///
110    /// The new process re-reads the configuration file on startup, effectively
111    /// reloading it.
112    pub async fn restart(&self) -> Result<u32> {
113        if self.is_running()?.is_some() {
114            self.stop().await?;
115        }
116        self.start().await
117    }
118}