Skip to main content

ralph/commands/daemon/
stop.rs

1//! Daemon stop command implementation.
2//!
3//! Responsibilities:
4//! - Stop a running Ralph daemon process gracefully.
5//! - Clean up daemon state and lock files after stopping.
6//! - Handle cases where daemon is not running or state is stale.
7//!
8//! Non-scope:
9//! - Starting or restarting the daemon (handled by start command).
10//! - Windows service management (Unix-only implementation).
11//!
12//! Invariants/assumptions:
13//! - Daemon uses a dedicated lock at `.ralph/cache/daemon.lock`.
14//! - Daemon state is stored at `.ralph/cache/daemon.json`.
15//! - Stop signal is created via `crate::signal::create_stop_signal`.
16
17use crate::config::Resolved;
18use crate::lock::PidLiveness;
19use anyhow::{Context, Result, bail};
20use std::fs;
21use std::time::Duration;
22
23use super::{
24    DAEMON_LOCK_DIR, DAEMON_STATE_FILE, daemon_pid_liveness, get_daemon_state,
25    manual_daemon_cleanup_instructions,
26};
27
28/// Stop the daemon gracefully.
29pub fn stop(resolved: &Resolved) -> Result<()> {
30    let cache_dir = resolved.repo_root.join(".ralph/cache");
31
32    // Check if daemon is running
33    let state = match get_daemon_state(&cache_dir)? {
34        Some(state) => state,
35        None => {
36            println!("Daemon is not running");
37            return Ok(());
38        }
39    };
40
41    match daemon_pid_liveness(state.pid) {
42        PidLiveness::NotRunning => {
43            println!("Daemon is not running (removing stale state file)");
44            let state_path = cache_dir.join(DAEMON_STATE_FILE);
45            if let Err(e) = fs::remove_file(&state_path) {
46                log::debug!(
47                    "Failed to remove stale daemon state file {}: {}",
48                    state_path.display(),
49                    e
50                );
51            }
52            let lock_path = cache_dir.join(DAEMON_LOCK_DIR);
53            if let Err(e) = fs::remove_dir_all(&lock_path) {
54                log::debug!(
55                    "Failed to remove stale daemon lock dir {}: {}",
56                    lock_path.display(),
57                    e
58                );
59            }
60            return Ok(());
61        }
62        PidLiveness::Indeterminate => {
63            bail!(
64                "Daemon PID {} liveness is indeterminate; preserving state/lock to avoid concurrent supervisors. \
65                 {}",
66                state.pid,
67                manual_daemon_cleanup_instructions(&cache_dir)
68            );
69        }
70        PidLiveness::Running => {}
71    }
72
73    // Create stop signal
74    crate::signal::create_stop_signal(&cache_dir).context("Failed to create stop signal")?;
75    println!("Stop signal sent to daemon (PID: {})", state.pid);
76
77    // Wait up to 10 seconds for the daemon to exit
78    println!("Waiting for daemon to stop...");
79    for _ in 0..100 {
80        std::thread::sleep(Duration::from_millis(100));
81        if matches!(daemon_pid_liveness(state.pid), PidLiveness::NotRunning) {
82            println!("Daemon stopped successfully");
83            let state_path = cache_dir.join(DAEMON_STATE_FILE);
84            if let Err(e) = fs::remove_file(&state_path) {
85                log::debug!(
86                    "Failed to remove daemon state file after stop {}: {}",
87                    state_path.display(),
88                    e
89                );
90            }
91            return Ok(());
92        }
93    }
94
95    // Daemon didn't stop in time
96    bail!(
97        "Daemon did not stop within 10 seconds. PID: {}. You may need to kill it manually with `kill -9 {}`",
98        state.pid,
99        state.pid
100    );
101}