ralph/commands/daemon/
start.rs1use 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
35pub 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 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 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 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 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 unsafe {
121 command.pre_exec(|| {
122 libc::setsid();
123 Ok(())
124 });
125 }
126
127 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}