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 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 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 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
135fn all_roots_deleted(roots: &[PathBuf]) -> bool {
137 !roots.is_empty() && roots.iter().all(|root| !root.exists())
138}
139
140async 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
151fn 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}