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