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 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
189pub fn init_foreground_daemon() -> Result<()> {
192 let pid = std::process::id();
193 write_pid_file(pid)?;
194 Ok(())
195}
196
197pub fn cleanup_daemon_files() {
199 let _ = fs::remove_file(daemon_pid_path());
200 cleanup_stale_socket();
201}