Skip to main content

lean_ctx/
daemon.rs

1use std::fs;
2use std::io::Write;
3use std::path::PathBuf;
4use std::process::Command;
5
6use anyhow::{Context, Result};
7
8fn data_dir() -> PathBuf {
9    dirs::data_local_dir()
10        .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
11        .join("lean-ctx")
12}
13
14pub fn daemon_pid_path() -> PathBuf {
15    data_dir().join("daemon.pid")
16}
17
18pub fn daemon_socket_path() -> PathBuf {
19    data_dir().join("daemon.sock")
20}
21
22pub fn is_daemon_running() -> bool {
23    let pid_path = daemon_pid_path();
24    let Ok(contents) = fs::read_to_string(&pid_path) else {
25        return false;
26    };
27    let Ok(pid) = contents.trim().parse::<u32>() else {
28        return false;
29    };
30    if process_alive(pid) {
31        return true;
32    }
33    // PID is stale — proactively clean up so nothing tries to connect
34    // to the dead socket or re-reads the stale PID.
35    let _ = fs::remove_file(&pid_path);
36    cleanup_stale_socket();
37    false
38}
39
40pub fn read_daemon_pid() -> Option<u32> {
41    let contents = fs::read_to_string(daemon_pid_path()).ok()?;
42    contents.trim().parse::<u32>().ok()
43}
44
45pub fn start_daemon(args: &[String]) -> Result<()> {
46    if is_daemon_running() {
47        let pid = read_daemon_pid().unwrap_or(0);
48        anyhow::bail!("Daemon already running (PID {pid}). Use --stop to stop it first.");
49    }
50
51    cleanup_stale_socket();
52
53    let exe = std::env::current_exe().context("cannot determine own executable path")?;
54
55    let mut cmd_args = vec!["serve".to_string()];
56    for arg in args {
57        if arg == "--daemon" || arg == "-d" {
58            continue;
59        }
60        cmd_args.push(arg.clone());
61    }
62    cmd_args.push("--_foreground-daemon".to_string());
63
64    let child = Command::new(&exe)
65        .args(&cmd_args)
66        .stdin(std::process::Stdio::null())
67        .stdout(std::process::Stdio::null())
68        .stderr(std::process::Stdio::null())
69        .spawn()
70        .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
71
72    let pid = child.id();
73    write_pid_file(pid)?;
74
75    std::thread::sleep(std::time::Duration::from_millis(200));
76
77    if !process_alive(pid) {
78        let _ = fs::remove_file(daemon_pid_path());
79        anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
80    }
81
82    eprintln!(
83        "lean-ctx daemon started (PID {pid})\n  Socket: {}\n  PID file: {}",
84        daemon_socket_path().display(),
85        daemon_pid_path().display()
86    );
87
88    Ok(())
89}
90
91pub fn stop_daemon() -> Result<()> {
92    let pid_path = daemon_pid_path();
93
94    let Some(pid) = read_daemon_pid() else {
95        eprintln!("No daemon PID file found. Nothing to stop.");
96        return Ok(());
97    };
98
99    if !process_alive(pid) {
100        eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
101        cleanup_stale_socket();
102        let _ = fs::remove_file(&pid_path);
103        return Ok(());
104    }
105
106    send_sigterm(pid)?;
107
108    for _ in 0..30 {
109        std::thread::sleep(std::time::Duration::from_millis(100));
110        if !process_alive(pid) {
111            break;
112        }
113    }
114
115    if process_alive(pid) {
116        eprintln!("Daemon (PID {pid}) did not stop gracefully, sending SIGKILL.");
117        send_sigkill(pid)?;
118        std::thread::sleep(std::time::Duration::from_millis(100));
119    }
120
121    let _ = fs::remove_file(&pid_path);
122    cleanup_stale_socket();
123    eprintln!("lean-ctx daemon stopped (PID {pid}).");
124    Ok(())
125}
126
127pub fn daemon_status() -> String {
128    if let Some(pid) = read_daemon_pid() {
129        if process_alive(pid) {
130            let sock = daemon_socket_path();
131            let sock_exists = sock.exists();
132            return format!(
133                "Daemon running (PID {pid})\n  Socket: {} ({})\n  PID file: {}",
134                sock.display(),
135                if sock_exists { "ready" } else { "missing" },
136                daemon_pid_path().display()
137            );
138        }
139        return format!("Daemon not running (stale PID file for PID {pid})");
140    }
141    "Daemon not running".to_string()
142}
143
144fn write_pid_file(pid: u32) -> Result<()> {
145    let pid_path = daemon_pid_path();
146    if let Some(parent) = pid_path.parent() {
147        fs::create_dir_all(parent)
148            .with_context(|| format!("cannot create dir: {}", parent.display()))?;
149    }
150    let mut f = fs::File::create(&pid_path)
151        .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
152    write!(f, "{pid}")?;
153    Ok(())
154}
155
156fn cleanup_stale_socket() {
157    let sock = daemon_socket_path();
158    if sock.exists() {
159        let _ = fs::remove_file(&sock);
160    }
161}
162
163fn process_alive(pid: u32) -> bool {
164    unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
165}
166
167fn send_sigterm(pid: u32) -> Result<()> {
168    let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
169    if ret != 0 {
170        anyhow::bail!(
171            "Failed to send SIGTERM to PID {pid}: {}",
172            std::io::Error::last_os_error()
173        );
174    }
175    Ok(())
176}
177
178fn send_sigkill(pid: u32) -> Result<()> {
179    let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
180    if ret != 0 {
181        anyhow::bail!(
182            "Failed to send SIGKILL to PID {pid}: {}",
183            std::io::Error::last_os_error()
184        );
185    }
186    Ok(())
187}
188
189/// Write the current process's PID and setup signal handler for cleanup.
190/// Called from the foreground-daemon process after fork.
191pub fn init_foreground_daemon() -> Result<()> {
192    let pid = std::process::id();
193    write_pid_file(pid)?;
194    Ok(())
195}
196
197/// Cleanup PID file and socket on shutdown.
198pub fn cleanup_daemon_files() {
199    let _ = fs::remove_file(daemon_pid_path());
200    cleanup_stale_socket();
201}