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        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    /// Main listener loop that handles connections and shutdown signals.
59    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
111/// Wait until idle timeout is reached
112async 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
120/// Wait until idle timeout is reached, polling at a configurable interval.
121async 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
147/// Returns `true` when all roots are non-existent and the list is non-empty.
148fn all_roots_deleted(roots: &[PathBuf]) -> bool {
149    !roots.is_empty() && roots.iter().all(|root| !root.exists())
150}
151
152/// Resolves when every workspace root managed by `lsp_manager` has been deleted
153/// from disk. Polls at `poll_interval`.
154async 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
164/// Spawn a task to handle shutdown signals (SIGTERM, SIGINT)
165fn 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}