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
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 ipc::cleanup(&daemon_addr());
52
53 if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
54 crate::config_io::cleanup_legacy_backups(&data_dir);
55 }
56
57 let exe_str = crate::core::portable_binary::resolve_portable_binary();
58 let exe = std::path::PathBuf::from(&exe_str);
59
60 let mut cmd_args = vec!["serve".to_string()];
61 for arg in args {
62 if arg == "--daemon" || arg == "-d" {
63 continue;
64 }
65 cmd_args.push(arg.clone());
66 }
67 cmd_args.push("--_foreground-daemon".to_string());
68
69 let log_dir = data_dir();
70 let _ = fs::create_dir_all(&log_dir);
71 let stderr_log = log_dir.join("daemon-stderr.log");
72 let stderr_file = fs::OpenOptions::new()
73 .create(true)
74 .write(true)
75 .truncate(true)
76 .open(&stderr_log);
77 let stderr_cfg = match stderr_file {
78 Ok(f) => std::process::Stdio::from(f),
79 Err(_) => std::process::Stdio::inherit(),
80 };
81
82 let child = Command::new(&exe)
83 .args(&cmd_args)
84 .stdin(std::process::Stdio::null())
85 .stdout(std::process::Stdio::null())
86 .stderr(stderr_cfg)
87 .spawn()
88 .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
89
90 let pid = child.id();
91 write_pid_file(pid)?;
92
93 std::thread::sleep(std::time::Duration::from_millis(200));
94
95 if !ipc::process::is_alive(pid) {
96 let _ = fs::remove_file(daemon_pid_path());
97 let stderr_content = fs::read_to_string(&stderr_log).unwrap_or_default();
98 let stderr_trimmed = stderr_content.trim();
99 if stderr_trimmed.is_empty() {
100 anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
101 }
102 anyhow::bail!("Daemon process exited immediately:\n{stderr_trimmed}");
103 }
104
105 let addr = daemon_addr();
106 if crate::core::protocol::meta_visible() {
107 eprintln!(
108 "lean-ctx daemon started (PID {pid})\n Endpoint: {}\n PID file: {}",
109 addr.display(),
110 daemon_pid_path().display()
111 );
112 }
113
114 Ok(())
115}
116
117pub fn stop_daemon() -> Result<()> {
118 let pid_path = daemon_pid_path();
119
120 let Some(pid) = read_daemon_pid() else {
121 eprintln!("No daemon PID file found. Nothing to stop.");
122 return Ok(());
123 };
124
125 if !ipc::process::is_alive(pid) {
126 eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
127 ipc::cleanup(&daemon_addr());
128 let _ = fs::remove_file(&pid_path);
129 return Ok(());
130 }
131
132 let http_shutdown_ok = try_http_shutdown();
133
134 if http_shutdown_ok {
135 for _ in 0..30 {
136 std::thread::sleep(std::time::Duration::from_millis(100));
137 if !ipc::process::is_alive(pid) {
138 break;
139 }
140 }
141 }
142
143 if ipc::process::is_alive(pid) {
144 let _ = ipc::process::terminate_gracefully(pid);
145 for _ in 0..20 {
146 std::thread::sleep(std::time::Duration::from_millis(100));
147 if !ipc::process::is_alive(pid) {
148 break;
149 }
150 }
151 }
152
153 if ipc::process::is_alive(pid) {
154 eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
155 let _ = ipc::process::force_kill(pid);
156 std::thread::sleep(std::time::Duration::from_millis(200));
157 }
158
159 let _ = fs::remove_file(&pid_path);
160 ipc::cleanup(&daemon_addr());
161 eprintln!("lean-ctx daemon stopped (PID {pid}).");
162
163 let orphans = ipc::process::find_pids_by_name("lean-ctx");
164 if !orphans.is_empty() {
165 eprintln!(" Cleaning up {} orphan process(es)…", orphans.len());
166 ipc::process::kill_all_by_name("lean-ctx");
167 }
168
169 Ok(())
170}
171
172fn try_http_shutdown() -> bool {
173 let Ok(rt) = tokio::runtime::Runtime::new() else {
174 return false;
175 };
176
177 rt.block_on(async {
178 crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
179 .await
180 .is_ok()
181 })
182}
183
184pub fn daemon_status() -> String {
185 let addr = daemon_addr();
186 if let Some(pid) = read_daemon_pid() {
187 if ipc::process::is_alive(pid) {
188 let listening = addr.is_listening();
189 return format!(
190 "Daemon running (PID {pid})\n Endpoint: {} ({})\n PID file: {}",
191 addr.display(),
192 if listening { "ready" } else { "missing" },
193 daemon_pid_path().display()
194 );
195 }
196 return format!("Daemon not running (stale PID file for PID {pid})");
197 }
198 "Daemon not running".to_string()
199}
200
201fn write_pid_file(pid: u32) -> Result<()> {
202 let pid_path = daemon_pid_path();
203 if let Some(parent) = pid_path.parent() {
204 fs::create_dir_all(parent)
205 .with_context(|| format!("cannot create dir: {}", parent.display()))?;
206 }
207 let mut f = fs::File::create(&pid_path)
208 .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
209 write!(f, "{pid}")?;
210 Ok(())
211}
212
213pub fn init_foreground_daemon() -> Result<()> {
215 let pid = std::process::id();
216 write_pid_file(pid)?;
217 Ok(())
218}
219
220pub fn cleanup_daemon_files() {
222 let _ = fs::remove_file(daemon_pid_path());
223 ipc::cleanup(&daemon_addr());
224}