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, Mutex, PoisonError};
9use std::time::Duration;
10use tokio::net::UnixListener;
11use tokio::select;
12use tokio::spawn;
13use tokio::sync::oneshot;
14use tokio::time::{Instant, sleep};
15use uuid::Uuid;
16
17#[doc = include_str!("docs/daemon.md")]
18pub struct LspDaemon {
19    socket_path: PathBuf,
20    idle_timeout: Option<Duration>,
21    workspace_registry: WorkspaceRegistry,
22}
23
24impl LspDaemon {
25    /// Create a daemon with socket, idle-timeout, and per-request timeout settings.
26    pub fn new(socket_path: PathBuf, idle_timeout: Option<Duration>, request_timeout: Duration) -> Self {
27        Self { socket_path, idle_timeout, workspace_registry: WorkspaceRegistry::new(request_timeout) }
28    }
29
30    /// Run the daemon until shutdown.
31    pub async fn run(self) -> DaemonResult<()> {
32        self.run_until_shutdown(spawn_shutdown_signal_handler()).await
33    }
34
35    pub(crate) async fn run_until_shutdown(self, shutdown_rx: oneshot::Receiver<()>) -> DaemonResult<()> {
36        if let Some(parent) = self.socket_path.parent() {
37            create_dir_all(parent).map_err(DaemonError::Io)?;
38        }
39
40        let _lockfile =
41            PidLockfile::acquire(&self.socket_path.with_extension("lock")).map_err(DaemonError::LockfileError)?;
42
43        let _ = remove_file(&self.socket_path);
44
45        tracing::info!("Daemon listening on {:?}", self.socket_path);
46        self.run_listener_loop(shutdown_rx).await?;
47
48        tracing::info!("Shutting down LSP servers");
49        self.workspace_registry.shutdown().await;
50
51        let _ = remove_file(&self.socket_path);
52        tracing::info!("Daemon shutdown complete");
53
54        Ok(())
55    }
56
57    /// Main listener loop that handles connections and shutdown signals.
58    async fn run_listener_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) -> DaemonResult<()> {
59        let listener = UnixListener::bind(&self.socket_path).map_err(DaemonError::BindFailed)?;
60        let idle = Arc::new(Mutex::new(IdleState { client_count: 0, last_activity: 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 idle = Arc::clone(&idle);
77
78                            {
79                                let mut state = idle.lock().unwrap_or_else(PoisonError::into_inner);
80                                state.client_count += 1;
81                                state.last_activity = Instant::now();
82                            }
83
84                            spawn(async move {
85                                handle_client(stream, registry, client_id).await;
86                                {
87                                    let mut state = idle.lock().unwrap_or_else(PoisonError::into_inner);
88                                    state.client_count -= 1;
89                                    state.last_activity = Instant::now();
90                                }
91                                tracing::debug!("Client {} handler complete", client_id);
92                            });
93                        }
94                        Err(e) => {
95                            tracing::warn!("Failed to accept connection: {}", e);
96                        }
97                    }
98                }
99
100                () = check_idle_timeout(&idle, self.idle_timeout) => {
101                    tracing::info!("Idle timeout reached, shutting down");
102                    return Ok(());
103                }
104
105                () = check_workspace_liveness(&self.workspace_registry, Duration::from_secs(10)) => {
106                    tracing::info!("All workspace roots deleted, shutting down");
107                    return Ok(());
108                }
109            }
110        }
111    }
112}
113
114struct IdleState {
115    client_count: usize,
116    last_activity: Instant,
117}
118
119async fn check_idle_timeout(idle: &Mutex<IdleState>, timeout: Option<Duration>) {
120    let Some(timeout) = timeout else {
121        pending::<()>().await;
122        return;
123    };
124
125    loop {
126        sleep(Duration::from_secs(10)).await;
127
128        let state = idle.lock().unwrap_or_else(PoisonError::into_inner);
129        if state.client_count == 0 && state.last_activity.elapsed() >= timeout {
130            return;
131        }
132    }
133}
134
135/// Returns `true` when all roots are non-existent and the list is non-empty.
136fn all_roots_deleted(roots: &[PathBuf]) -> bool {
137    !roots.is_empty() && roots.iter().all(|root| !root.exists())
138}
139
140/// Resolves when every registered workspace root has been deleted from disk.
141async fn check_workspace_liveness(workspace_registry: &WorkspaceRegistry, poll_interval: Duration) {
142    loop {
143        sleep(poll_interval).await;
144        let roots = workspace_registry.workspace_roots();
145        if all_roots_deleted(&roots) {
146            return;
147        }
148    }
149}
150
151/// Spawn a task to handle shutdown signals (SIGTERM, SIGINT)
152fn spawn_shutdown_signal_handler() -> oneshot::Receiver<()> {
153    let (tx, rx) = oneshot::channel::<()>();
154
155    #[cfg(unix)]
156    {
157        use tokio::signal::unix::{SignalKind, signal};
158        spawn(async move {
159            let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
160
161            let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
162
163            select! {
164                _ = sigterm.recv() => {
165                    tracing::info!("Received SIGTERM");
166                }
167                _ = sigint.recv() => {
168                    tracing::info!("Received SIGINT");
169                }
170            }
171            let _ = tx.send(());
172        });
173    }
174
175    rx
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn all_roots_deleted_empty_returns_false() {
184        assert!(!all_roots_deleted(&[]));
185    }
186
187    #[test]
188    fn all_roots_deleted_existing_dir_returns_false() {
189        let dir = tempfile::tempdir().unwrap();
190        assert!(!all_roots_deleted(&[dir.path().to_path_buf()]));
191    }
192
193    #[test]
194    fn all_roots_deleted_nonexistent_returns_true() {
195        let gone = PathBuf::from("/tmp/aether-lspd-test-nonexistent-dir-that-does-not-exist");
196        assert!(all_roots_deleted(&[gone]));
197    }
198
199    #[test]
200    fn all_roots_deleted_mixed_returns_false() {
201        let dir = tempfile::tempdir().unwrap();
202        let gone = PathBuf::from("/tmp/aether-lspd-test-nonexistent-dir-that-does-not-exist");
203        assert!(!all_roots_deleted(&[dir.path().to_path_buf(), gone]));
204    }
205
206    #[test]
207    fn all_roots_deleted_after_tempdir_drop() {
208        let dir = tempfile::tempdir().unwrap();
209        let root = dir.path().to_path_buf();
210        assert!(!all_roots_deleted(std::slice::from_ref(&root)));
211        drop(dir);
212        assert!(all_roots_deleted(&[root]));
213    }
214}