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 process_alive(pid)
31}
32
33pub fn read_daemon_pid() -> Option<u32> {
34 let contents = fs::read_to_string(daemon_pid_path()).ok()?;
35 contents.trim().parse::<u32>().ok()
36}
37
38pub fn start_daemon(args: &[String]) -> Result<()> {
39 if is_daemon_running() {
40 let pid = read_daemon_pid().unwrap_or(0);
41 anyhow::bail!("Daemon already running (PID {pid}). Use --stop to stop it first.");
42 }
43
44 cleanup_stale_socket();
45
46 let exe = std::env::current_exe().context("cannot determine own executable path")?;
47
48 let mut cmd_args = vec!["serve".to_string()];
49 for arg in args {
50 if arg == "--daemon" || arg == "-d" {
51 continue;
52 }
53 cmd_args.push(arg.clone());
54 }
55 cmd_args.push("--_foreground-daemon".to_string());
56
57 let child = Command::new(&exe)
58 .args(&cmd_args)
59 .stdin(std::process::Stdio::null())
60 .stdout(std::process::Stdio::null())
61 .stderr(std::process::Stdio::null())
62 .spawn()
63 .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
64
65 let pid = child.id();
66 write_pid_file(pid)?;
67
68 std::thread::sleep(std::time::Duration::from_millis(200));
69
70 if !process_alive(pid) {
71 let _ = fs::remove_file(daemon_pid_path());
72 anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
73 }
74
75 eprintln!(
76 "lean-ctx daemon started (PID {pid})\n Socket: {}\n PID file: {}",
77 daemon_socket_path().display(),
78 daemon_pid_path().display()
79 );
80
81 Ok(())
82}
83
84pub fn stop_daemon() -> Result<()> {
85 let pid_path = daemon_pid_path();
86
87 let Some(pid) = read_daemon_pid() else {
88 eprintln!("No daemon PID file found. Nothing to stop.");
89 return Ok(());
90 };
91
92 if !process_alive(pid) {
93 eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
94 cleanup_stale_socket();
95 let _ = fs::remove_file(&pid_path);
96 return Ok(());
97 }
98
99 send_sigterm(pid)?;
100
101 for _ in 0..30 {
102 std::thread::sleep(std::time::Duration::from_millis(100));
103 if !process_alive(pid) {
104 break;
105 }
106 }
107
108 if process_alive(pid) {
109 eprintln!("Daemon (PID {pid}) did not stop gracefully, sending SIGKILL.");
110 send_sigkill(pid)?;
111 std::thread::sleep(std::time::Duration::from_millis(100));
112 }
113
114 let _ = fs::remove_file(&pid_path);
115 cleanup_stale_socket();
116 eprintln!("lean-ctx daemon stopped (PID {pid}).");
117 Ok(())
118}
119
120pub fn daemon_status() -> String {
121 if let Some(pid) = read_daemon_pid() {
122 if process_alive(pid) {
123 let sock = daemon_socket_path();
124 let sock_exists = sock.exists();
125 return format!(
126 "Daemon running (PID {pid})\n Socket: {} ({})\n PID file: {}",
127 sock.display(),
128 if sock_exists { "ready" } else { "missing" },
129 daemon_pid_path().display()
130 );
131 }
132 return format!("Daemon not running (stale PID file for PID {pid})");
133 }
134 "Daemon not running".to_string()
135}
136
137fn write_pid_file(pid: u32) -> Result<()> {
138 let pid_path = daemon_pid_path();
139 if let Some(parent) = pid_path.parent() {
140 fs::create_dir_all(parent)
141 .with_context(|| format!("cannot create dir: {}", parent.display()))?;
142 }
143 let mut f = fs::File::create(&pid_path)
144 .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
145 write!(f, "{pid}")?;
146 Ok(())
147}
148
149fn cleanup_stale_socket() {
150 let sock = daemon_socket_path();
151 if sock.exists() {
152 let _ = fs::remove_file(&sock);
153 }
154}
155
156fn process_alive(pid: u32) -> bool {
157 unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
158}
159
160fn send_sigterm(pid: u32) -> Result<()> {
161 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
162 if ret != 0 {
163 anyhow::bail!(
164 "Failed to send SIGTERM to PID {pid}: {}",
165 std::io::Error::last_os_error()
166 );
167 }
168 Ok(())
169}
170
171fn send_sigkill(pid: u32) -> Result<()> {
172 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
173 if ret != 0 {
174 anyhow::bail!(
175 "Failed to send SIGKILL to PID {pid}: {}",
176 std::io::Error::last_os_error()
177 );
178 }
179 Ok(())
180}
181
182pub fn init_foreground_daemon() -> Result<()> {
185 let pid = std::process::id();
186 write_pid_file(pid)?;
187 Ok(())
188}
189
190pub fn cleanup_daemon_files() {
192 let _ = fs::remove_file(daemon_pid_path());
193 cleanup_stale_socket();
194}