1use std::fs;
2use std::io::Write;
3use std::path::PathBuf;
4use std::process::Command;
5
6use anyhow::{Context, Result};
7
8use crate::ipc;
9
10fn data_dir() -> PathBuf {
11 dirs::data_local_dir()
12 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
13 .join("lean-ctx")
14}
15
16pub fn daemon_pid_path() -> PathBuf {
17 data_dir().join("daemon.pid")
18}
19
20pub fn daemon_addr() -> ipc::DaemonAddr {
21 ipc::DaemonAddr::default_for_current_os()
22}
23
24pub fn is_daemon_running() -> bool {
25 let pid_path = daemon_pid_path();
26 let Ok(contents) = fs::read_to_string(&pid_path) else {
27 return false;
28 };
29 let Ok(pid) = contents.trim().parse::<u32>() else {
30 return false;
31 };
32 if ipc::process::is_alive(pid) {
33 return true;
34 }
35 let _ = fs::remove_file(&pid_path);
36 ipc::cleanup(&daemon_addr());
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
45fn acquire_start_lock() -> Option<fs::File> {
54 use fs2::FileExt;
55 let lock_path = data_dir().join("daemon.start.lock");
56 if let Some(parent) = lock_path.parent() {
57 let _ = fs::create_dir_all(parent);
58 }
59 let file = fs::OpenOptions::new()
60 .create(true)
61 .write(true)
62 .truncate(false)
63 .open(&lock_path)
64 .ok()?;
65 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
66 loop {
67 match file.try_lock_exclusive() {
68 Ok(()) => return Some(file),
69 Err(_) if std::time::Instant::now() < deadline => {
70 std::thread::sleep(std::time::Duration::from_millis(50));
71 }
72 Err(_) => return None,
73 }
74 }
75}
76
77pub fn start_daemon(args: &[String]) -> Result<()> {
78 let _start_lock = acquire_start_lock();
80
81 if is_daemon_running() {
82 let pid = read_daemon_pid().unwrap_or(0);
83 anyhow::bail!("Daemon already running (PID {pid}). Use --stop to stop it first.");
84 }
85
86 ipc::cleanup(&daemon_addr());
87
88 if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
89 crate::config_io::cleanup_legacy_backups(&data_dir);
90 }
91
92 let exe_str = crate::core::portable_binary::resolve_portable_binary();
93 let exe = std::path::PathBuf::from(&exe_str);
94
95 let mut cmd_args = vec!["serve".to_string()];
96 for arg in args {
97 if arg == "--daemon" || arg == "-d" {
98 continue;
99 }
100 cmd_args.push(arg.clone());
101 }
102 cmd_args.push("--_foreground-daemon".to_string());
103
104 let log_dir = data_dir();
105 let _ = fs::create_dir_all(&log_dir);
106 let stderr_log = log_dir.join("daemon-stderr.log");
107 let stderr_file = fs::OpenOptions::new()
108 .create(true)
109 .write(true)
110 .truncate(true)
111 .open(&stderr_log);
112 let stderr_cfg = match stderr_file {
113 Ok(f) => std::process::Stdio::from(f),
114 Err(_) => std::process::Stdio::inherit(),
115 };
116
117 let mut cmd = Command::new(&exe);
118 cmd.args(&cmd_args)
119 .stdin(std::process::Stdio::null())
120 .stdout(std::process::Stdio::null())
121 .stderr(stderr_cfg);
122 if crate::core::pathutil::process_is_tcc_standalone() {
128 cmd.env("LEAN_CTX_TCC_STANDALONE", "1");
129 }
130 let child = ipc::process::spawn_detached(&mut cmd)
133 .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
134
135 let pid = child.id();
136 write_pid_file(pid)?;
137
138 std::thread::sleep(std::time::Duration::from_millis(200));
139
140 if !ipc::process::is_alive(pid) {
141 let _ = fs::remove_file(daemon_pid_path());
142 let stderr_content = fs::read_to_string(&stderr_log).unwrap_or_default();
143 let stderr_trimmed = stderr_content.trim();
144 if stderr_trimmed.is_empty() {
145 anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
146 }
147 anyhow::bail!("Daemon process exited immediately:\n{stderr_trimmed}");
148 }
149
150 let addr = daemon_addr();
151 if crate::core::protocol::meta_visible() {
152 eprintln!(
153 "lean-ctx daemon started (PID {pid})\n Endpoint: {}\n PID file: {}",
154 addr.display(),
155 daemon_pid_path().display()
156 );
157 }
158
159 Ok(())
160}
161
162pub fn stop_daemon() -> Result<()> {
163 let pid_path = daemon_pid_path();
164
165 let Some(pid) = read_daemon_pid() else {
166 eprintln!("No daemon PID file found. Nothing to stop.");
167 return Ok(());
168 };
169
170 if !ipc::process::is_alive(pid) {
171 eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
172 ipc::cleanup(&daemon_addr());
173 let _ = fs::remove_file(&pid_path);
174 return Ok(());
175 }
176
177 let http_shutdown_ok = try_http_shutdown();
178
179 if http_shutdown_ok {
180 for _ in 0..30 {
181 std::thread::sleep(std::time::Duration::from_millis(100));
182 if !ipc::process::is_alive(pid) {
183 break;
184 }
185 }
186 }
187
188 if ipc::process::is_alive(pid) {
189 let _ = ipc::process::terminate_gracefully(pid);
190 for _ in 0..20 {
191 std::thread::sleep(std::time::Duration::from_millis(100));
192 if !ipc::process::is_alive(pid) {
193 break;
194 }
195 }
196 }
197
198 if ipc::process::is_alive(pid) {
199 eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
200 let _ = ipc::process::force_kill(pid);
201 std::thread::sleep(std::time::Duration::from_millis(200));
202 }
203
204 let _ = fs::remove_file(&pid_path);
205 ipc::cleanup(&daemon_addr());
206 eprintln!("lean-ctx daemon stopped (PID {pid}).");
207
208 let orphans = ipc::process::find_pids_by_name("lean-ctx");
209 if !orphans.is_empty() {
210 eprintln!(" Cleaning up {} orphan process(es)…", orphans.len());
211 ipc::process::kill_all_by_name("lean-ctx");
212 }
213
214 Ok(())
215}
216
217fn try_http_shutdown() -> bool {
218 let Ok(rt) = tokio::runtime::Runtime::new() else {
219 return false;
220 };
221
222 rt.block_on(async {
223 crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
224 .await
225 .is_ok()
226 })
227}
228
229pub fn daemon_status() -> String {
230 let addr = daemon_addr();
231 if let Some(pid) = read_daemon_pid() {
232 if ipc::process::is_alive(pid) {
233 let listening = addr.is_listening();
234 return format!(
235 "Daemon running (PID {pid})\n Endpoint: {} ({})\n PID file: {}",
236 addr.display(),
237 if listening { "ready" } else { "missing" },
238 daemon_pid_path().display()
239 );
240 }
241 return format!("Daemon not running (stale PID file for PID {pid})");
242 }
243 "Daemon not running".to_string()
244}
245
246fn write_pid_file(pid: u32) -> Result<()> {
247 let pid_path = daemon_pid_path();
248 if let Some(parent) = pid_path.parent() {
249 fs::create_dir_all(parent)
250 .with_context(|| format!("cannot create dir: {}", parent.display()))?;
251 }
252 let mut f = fs::File::create(&pid_path)
253 .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
254 write!(f, "{pid}")?;
255 Ok(())
256}
257
258pub fn init_foreground_daemon() -> Result<()> {
266 crate::core::layout_pin::heal();
267 let pid = std::process::id();
268 write_pid_file(pid)?;
269 Ok(())
270}
271
272pub fn cleanup_daemon_files() {
274 let _ = fs::remove_file(daemon_pid_path());
275 ipc::cleanup(&daemon_addr());
276}