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