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 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 !ipc::process::is_alive(pid) {
78 let _ = fs::remove_file(daemon_pid_path());
79 anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
80 }
81
82 let addr = daemon_addr();
83 eprintln!(
84 "lean-ctx daemon started (PID {pid})\n Endpoint: {}\n PID file: {}",
85 addr.display(),
86 daemon_pid_path().display()
87 );
88
89 Ok(())
90}
91
92pub fn stop_daemon() -> Result<()> {
93 let pid_path = daemon_pid_path();
94
95 let Some(pid) = read_daemon_pid() else {
96 eprintln!("No daemon PID file found. Nothing to stop.");
97 return Ok(());
98 };
99
100 if !ipc::process::is_alive(pid) {
101 eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
102 ipc::cleanup(&daemon_addr());
103 let _ = fs::remove_file(&pid_path);
104 return Ok(());
105 }
106
107 let http_shutdown_ok = try_http_shutdown();
109
110 if http_shutdown_ok {
112 for _ in 0..30 {
113 std::thread::sleep(std::time::Duration::from_millis(100));
114 if !ipc::process::is_alive(pid) {
115 break;
116 }
117 }
118 }
119
120 if ipc::process::is_alive(pid) {
122 let _ = ipc::process::terminate_gracefully(pid);
123 for _ in 0..20 {
124 std::thread::sleep(std::time::Duration::from_millis(100));
125 if !ipc::process::is_alive(pid) {
126 break;
127 }
128 }
129 }
130
131 if ipc::process::is_alive(pid) {
133 eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
134 let _ = ipc::process::force_kill(pid);
135 std::thread::sleep(std::time::Duration::from_millis(100));
136 }
137
138 let _ = fs::remove_file(&pid_path);
139 ipc::cleanup(&daemon_addr());
140 eprintln!("lean-ctx daemon stopped (PID {pid}).");
141 Ok(())
142}
143
144fn try_http_shutdown() -> bool {
145 let Ok(rt) = tokio::runtime::Runtime::new() else {
146 return false;
147 };
148
149 rt.block_on(async {
150 crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
151 .await
152 .is_ok()
153 })
154}
155
156pub fn daemon_status() -> String {
157 let addr = daemon_addr();
158 if let Some(pid) = read_daemon_pid() {
159 if ipc::process::is_alive(pid) {
160 let listening = addr.is_listening();
161 return format!(
162 "Daemon running (PID {pid})\n Endpoint: {} ({})\n PID file: {}",
163 addr.display(),
164 if listening { "ready" } else { "missing" },
165 daemon_pid_path().display()
166 );
167 }
168 return format!("Daemon not running (stale PID file for PID {pid})");
169 }
170 "Daemon not running".to_string()
171}
172
173fn write_pid_file(pid: u32) -> Result<()> {
174 let pid_path = daemon_pid_path();
175 if let Some(parent) = pid_path.parent() {
176 fs::create_dir_all(parent)
177 .with_context(|| format!("cannot create dir: {}", parent.display()))?;
178 }
179 let mut f = fs::File::create(&pid_path)
180 .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
181 write!(f, "{pid}")?;
182 Ok(())
183}
184
185pub fn init_foreground_daemon() -> Result<()> {
187 let pid = std::process::id();
188 write_pid_file(pid)?;
189 Ok(())
190}
191
192pub fn cleanup_daemon_files() {
194 let _ = fs::remove_file(daemon_pid_path());
195 ipc::cleanup(&daemon_addr());
196}