Skip to main content

ralph/commands/daemon/
start.rs

1//! Daemon start command implementation.
2//!
3//! Responsibilities:
4//! - Start the daemon as a background process on Unix systems.
5//! - Check for existing running daemon and handle stale state.
6//! - Acquire daemon lock to prevent concurrent starts.
7//! - Spawn the daemon process with proper stdio redirection.
8//! - Wait for daemon state to confirm successful startup.
9//!
10//! Not handled here:
11//! - Windows daemon management (returns error on non-Unix systems).
12//! - Daemon stop/status/logs operations (handled in other modules).
13//! - Signal handling or process lifecycle management after spawn.
14//!
15//! Invariants/assumptions:
16//! - Requires Unix platform; fails gracefully on Windows.
17//! - Daemon state file is written by the spawned serve process.
18//! - Uses DAEMON_LOCK_DIR for exclusive access during startup.
19
20use crate::cli::daemon::DaemonStartArgs;
21use crate::config::Resolved;
22use crate::lock::{PidLiveness, acquire_dir_lock};
23use anyhow::{Context, Result, bail};
24use std::fs;
25use std::time::Duration;
26
27#[cfg(unix)]
28use std::os::unix::process::CommandExt;
29
30use super::{
31    DAEMON_LOCK_DIR, DAEMON_LOG_FILE_NAME, DAEMON_STATE_FILE, daemon_pid_liveness,
32    get_daemon_state, manual_daemon_cleanup_instructions, wait_for_daemon_state_pid,
33};
34
35/// Start the daemon as a background process.
36pub fn start(resolved: &Resolved, args: DaemonStartArgs) -> Result<()> {
37    #[cfg(unix)]
38    {
39        let cache_dir = resolved.repo_root.join(".ralph/cache");
40        let daemon_lock_dir = cache_dir.join(DAEMON_LOCK_DIR);
41
42        // Check if daemon is already running
43        if let Some(state) = get_daemon_state(&cache_dir)? {
44            match daemon_pid_liveness(state.pid) {
45                PidLiveness::Running => {
46                    bail!(
47                        "Daemon is already running (PID: {}). Use `ralph daemon stop` to stop it.",
48                        state.pid
49                    );
50                }
51                PidLiveness::Indeterminate => {
52                    bail!(
53                        "Daemon PID {} liveness is indeterminate. \
54                         Preserving state/lock to prevent concurrent supervisors. \
55                         {}",
56                        state.pid,
57                        manual_daemon_cleanup_instructions(&cache_dir)
58                    );
59                }
60                PidLiveness::NotRunning => {
61                    log::warn!("Removing stale daemon state file");
62                    let state_path = cache_dir.join(DAEMON_STATE_FILE);
63                    if let Err(e) = fs::remove_file(&state_path) {
64                        log::debug!(
65                            "Failed to remove stale daemon state file {}: {}",
66                            state_path.display(),
67                            e
68                        );
69                    }
70                }
71            }
72        }
73
74        // Try to acquire the daemon lock to ensure no other daemon is starting
75        let _lock = match acquire_dir_lock(&daemon_lock_dir, "daemon-start", false) {
76            Ok(lock) => lock,
77            Err(e) => {
78                bail!(
79                    "Failed to acquire daemon lock: {}. Another daemon may be starting.",
80                    e
81                );
82            }
83        };
84
85        // Build the serve command
86        let exe = std::env::current_exe().context("Failed to get current executable path")?;
87        let mut command = std::process::Command::new(&exe);
88        command.current_dir(&resolved.repo_root);
89        command
90            .arg("daemon")
91            .arg("serve")
92            .arg("--empty-poll-ms")
93            .arg(args.empty_poll_ms.to_string())
94            .arg("--wait-poll-ms")
95            .arg(args.wait_poll_ms.to_string());
96
97        if args.notify_when_unblocked {
98            command.arg("--notify-when-unblocked");
99        }
100
101        // Set up stdio redirection
102        let log_dir = resolved.repo_root.join(".ralph/logs");
103        fs::create_dir_all(&log_dir).context("Failed to create log directory")?;
104        let log_file = std::fs::File::create(log_dir.join(DAEMON_LOG_FILE_NAME))
105            .context("Failed to create daemon log file")?;
106
107        command
108            .stdin(std::process::Stdio::null())
109            .stdout(
110                log_file
111                    .try_clone()
112                    .context("Failed to clone log file handle")?,
113            )
114            .stderr(log_file);
115
116        // Detach from terminal on Unix
117        // SAFETY: pre_exec runs between fork and exec in the child process.
118        // setsid() creates a new session and detaches from controlling terminal.
119        // This is async-signal-safe per POSIX and safe to call here.
120        unsafe {
121            command.pre_exec(|| {
122                libc::setsid();
123                Ok(())
124            });
125        }
126
127        // Spawn the daemon process
128        let child = command.spawn().context("Failed to spawn daemon process")?;
129        let pid = child.id();
130
131        if wait_for_daemon_state_pid(
132            &cache_dir,
133            pid,
134            Duration::from_secs(2),
135            Duration::from_millis(100),
136        )? {
137            println!("Daemon started successfully (PID: {})", pid);
138            Ok(())
139        } else {
140            bail!("Daemon failed to start. Check .ralph/logs/daemon.log for details.");
141        }
142    }
143
144    #[cfg(not(unix))]
145    {
146        let _ = (resolved, args);
147        bail!(
148            "Daemon mode is only supported on Unix systems. Use `ralph run loop --continuous` in a terminal or configure a Windows service."
149        );
150    }
151}