1use crate::client_connection::handle_client;
2use crate::error::{DaemonError, DaemonResult};
3use crate::pid_lockfile::PidLockfile;
4use crate::workspace_registry::WorkspaceRegistry;
5use std::fs::{create_dir_all, remove_file};
6use std::future::pending;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::time::{Duration, Instant};
11use tokio::net::UnixListener;
12use tokio::select;
13use tokio::spawn;
14use tokio::sync::{RwLock, oneshot};
15use tokio::time::sleep;
16use uuid::Uuid;
17
18#[doc = include_str!("docs/daemon.md")]
19pub struct LspDaemon {
20 socket_path: PathBuf,
21 idle_timeout: Option<Duration>,
22 workspace_registry: WorkspaceRegistry,
23}
24
25impl LspDaemon {
26 pub fn new(socket_path: PathBuf, idle_timeout: Option<Duration>, request_timeout: Duration) -> Self {
28 Self { socket_path, idle_timeout, workspace_registry: WorkspaceRegistry::new(request_timeout) }
29 }
30
31 pub async fn run(self) -> DaemonResult<()> {
33 if let Some(parent) = self.socket_path.parent() {
34 create_dir_all(parent).map_err(DaemonError::Io)?;
35 }
36
37 let _lockfile = PidLockfile::acquire(&self.socket_path.with_extension("lock"))
38 .map_err(|e| DaemonError::LockfileError(e.to_string()))?;
39
40 let _ = remove_file(&self.socket_path);
41
42 let shutdown_rx = spawn_shutdown_signal_handler();
43
44 tracing::info!("Daemon listening on {:?}", self.socket_path);
45 self.run_listener_loop(shutdown_rx).await?;
46
47 tracing::info!("Shutting down LSP servers");
48 self.workspace_registry.shutdown().await;
49
50 let _ = remove_file(&self.socket_path);
51 tracing::info!("Daemon shutdown complete");
52
53 Ok(())
54 }
55
56 async fn run_listener_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) -> DaemonResult<()> {
58 let listener = UnixListener::bind(&self.socket_path).map_err(DaemonError::BindFailed)?;
59 let client_count = Arc::new(AtomicUsize::new(0));
60 let last_activity = Arc::new(RwLock::new(Instant::now()));
61
62 loop {
63 select! {
64 biased;
65
66 _ = &mut shutdown_rx => {
67 tracing::info!("Shutting down");
68 return Ok(());
69 }
70
71 result = listener.accept() => {
72 match result {
73 Ok((stream, _)) => {
74 let client_id = Uuid::new_v4();
75 let registry = self.workspace_registry.clone();
76 let client_count = Arc::clone(&client_count);
77 let last_activity = Arc::clone(&last_activity);
78
79 client_count.fetch_add(1, Ordering::Relaxed);
80 *last_activity.write().await = Instant::now();
81
82 spawn(async move {
83 handle_client(stream, registry, client_id).await;
84 client_count.fetch_sub(1, Ordering::Relaxed);
85 *last_activity.write().await = Instant::now();
86 tracing::debug!("Client {} handler complete", client_id);
87 });
88 }
89 Err(e) => {
90 tracing::warn!("Failed to accept connection: {}", e);
91 }
92 }
93 }
94
95 () = check_idle_timeout(client_count.clone(), last_activity.clone(), self.idle_timeout) => {
96 tracing::info!("Idle timeout reached, shutting down");
97 return Ok(());
98 }
99
100 () = check_workspace_liveness(&self.workspace_registry, Duration::from_secs(10)) => {
101 tracing::info!("All workspace roots deleted, shutting down");
102 return Ok(());
103 }
104 }
105 }
106 }
107}
108
109async fn check_idle_timeout(
111 client_count: Arc<AtomicUsize>,
112 last_activity: Arc<RwLock<Instant>>,
113 timeout: Option<Duration>,
114) {
115 check_idle_timeout_with_interval(client_count, last_activity, timeout, Duration::from_secs(10)).await;
116}
117
118async fn check_idle_timeout_with_interval(
120 client_count: Arc<AtomicUsize>,
121 last_activity: Arc<RwLock<Instant>>,
122 timeout: Option<Duration>,
123 poll_interval: Duration,
124) {
125 let Some(timeout) = timeout else {
126 pending::<()>().await;
127 return;
128 };
129
130 loop {
131 sleep(poll_interval).await;
132
133 let count = client_count.load(Ordering::Relaxed);
134 if count > 0 {
135 continue;
136 }
137
138 let last = *last_activity.read().await;
139 if last.elapsed() >= timeout {
140 return;
141 }
142 }
143}
144
145fn all_roots_deleted(roots: &[PathBuf]) -> bool {
147 !roots.is_empty() && roots.iter().all(|root| !root.exists())
148}
149
150async fn check_workspace_liveness(workspace_registry: &WorkspaceRegistry, poll_interval: Duration) {
153 loop {
154 sleep(poll_interval).await;
155 let roots = workspace_registry.workspace_roots().await;
156 if all_roots_deleted(&roots) {
157 return;
158 }
159 }
160}
161
162fn spawn_shutdown_signal_handler() -> oneshot::Receiver<()> {
164 let (tx, rx) = oneshot::channel::<()>();
165
166 #[cfg(unix)]
167 {
168 use tokio::signal::unix::{SignalKind, signal};
169 spawn(async move {
170 let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
171
172 let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
173
174 select! {
175 _ = sigterm.recv() => {
176 tracing::info!("Received SIGTERM");
177 }
178 _ = sigint.recv() => {
179 tracing::info!("Received SIGINT");
180 }
181 }
182 let _ = tx.send(());
183 });
184 }
185
186 rx
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use tokio::time::timeout;
193
194 #[tokio::test]
195 async fn idle_timeout_none_never_completes() {
196 let client_count = Arc::new(AtomicUsize::new(0));
197 let last_activity = Arc::new(RwLock::new(Instant::now()));
198
199 let result = timeout(
200 Duration::from_millis(40),
201 check_idle_timeout_with_interval(client_count, last_activity, None, Duration::from_millis(5)),
202 )
203 .await;
204
205 assert!(result.is_err(), "None timeout should not complete");
206 }
207
208 #[tokio::test]
209 async fn idle_timeout_completes_when_idle_elapsed() {
210 let client_count = Arc::new(AtomicUsize::new(0));
211 let stale_activity = Instant::now()
212 .checked_sub(Duration::from_millis(50))
213 .expect("subtracting from current instant should succeed");
214 let last_activity = Arc::new(RwLock::new(stale_activity));
215
216 let result = timeout(
217 Duration::from_millis(100),
218 check_idle_timeout_with_interval(
219 client_count,
220 last_activity,
221 Some(Duration::from_millis(10)),
222 Duration::from_millis(5),
223 ),
224 )
225 .await;
226
227 assert!(result.is_ok(), "Idle timeout should complete");
228 }
229
230 #[test]
231 fn all_roots_deleted_empty_returns_false() {
232 assert!(!all_roots_deleted(&[]));
233 }
234
235 #[test]
236 fn all_roots_deleted_existing_dir_returns_false() {
237 let dir = tempfile::tempdir().unwrap();
238 assert!(!all_roots_deleted(&[dir.path().to_path_buf()]));
239 }
240
241 #[test]
242 fn all_roots_deleted_nonexistent_returns_true() {
243 let gone = PathBuf::from("/tmp/aether-lspd-test-nonexistent-dir-that-does-not-exist");
244 assert!(all_roots_deleted(&[gone]));
245 }
246
247 #[test]
248 fn all_roots_deleted_mixed_returns_false() {
249 let dir = tempfile::tempdir().unwrap();
250 let gone = PathBuf::from("/tmp/aether-lspd-test-nonexistent-dir-that-does-not-exist");
251 assert!(!all_roots_deleted(&[dir.path().to_path_buf(), gone]));
252 }
253
254 #[test]
255 fn all_roots_deleted_after_tempdir_drop() {
256 let dir = tempfile::tempdir().unwrap();
257 let root = dir.path().to_path_buf();
258 assert!(!all_roots_deleted(std::slice::from_ref(&root)));
259 drop(dir);
260 assert!(all_roots_deleted(&[root]));
261 }
262}