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