Skip to main content

aether_lspd/
daemon.rs

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    /// Create a daemon with socket, idle-timeout, and per-request timeout settings.
27    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    /// Run the daemon until shutdown.
32    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    /// Main listener loop that handles connections and shutdown signals.
57    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
109/// Wait until idle timeout is reached
110async 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
118/// Wait until idle timeout is reached, polling at a configurable interval.
119async 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
145/// Returns `true` when all roots are non-existent and the list is non-empty.
146fn all_roots_deleted(roots: &[PathBuf]) -> bool {
147    !roots.is_empty() && roots.iter().all(|root| !root.exists())
148}
149
150/// Resolves when every workspace root managed by `lsp_manager` has been deleted
151/// from disk. Polls at `poll_interval`.
152async 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
162/// Spawn a task to handle shutdown signals (SIGTERM, SIGINT)
163fn 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}