Skip to main content

gid_core/
lsp_daemon.rs

1//! LSP Daemon — Persistent language server process manager.
2//!
3//! Keeps rust-analyzer (and other LSP servers) alive between `gid extract` calls
4//! so the expensive cold-start analysis (~5-10 min for large projects) only happens once.
5//!
6//! Architecture:
7//! - First `gid extract --lsp` spawns LSP processes and saves their PIDs to `.gid/lsp-daemon/`
8//! - Subsequent calls reuse the running processes via a proxy that multiplexes stdio
9//! - `gid lsp stop` or timeout (1 hour idle) shuts them down
10//!
11//! Since LSP servers use stdio transport (not sockets), we can't share a running process's
12//! stdin/stdout across invocations. Instead we keep the LspClient in-process and expose
13//! a file-based lock so only one `gid extract` runs LSP at a time, while the client
14//! persists via a long-running background process.
15//!
16//! Simpler approach chosen: **socket-based daemon process**.
17//! A background process owns the LSP servers and accepts commands over a Unix socket.
18//! Each `gid extract` connects to the daemon socket and sends definition/reference queries.
19
20use std::collections::HashMap;
21use std::io::{BufRead, BufReader, Write};
22use std::os::unix::net::{UnixListener, UnixStream};
23use std::path::{Path, PathBuf};
24use std::time::{Duration, Instant};
25
26use anyhow::{bail, Context, Result};
27use serde::{Deserialize, Serialize};
28
29use crate::lsp_client::{LspClient, LspLocation, LspServerConfig};
30
31// ═══════════════════════════════════════════════════════════════════════════════
32// Protocol: messages between client and daemon over Unix socket
33// ═══════════════════════════════════════════════════════════════════════════════
34
35#[derive(Debug, Serialize, Deserialize)]
36#[serde(tag = "type", rename_all = "snake_case")]
37enum DaemonRequest {
38    /// Check if daemon is alive and has a ready LSP for this language.
39    Ping { lang_id: String },
40    /// Open a file in the LSP server.
41    OpenFile {
42        lang_id: String,
43        rel_path: String,
44        content: String,
45    },
46    /// Get definition at position.
47    GetDefinition {
48        lang_id: String,
49        rel_path: String,
50        line: u32,
51        character: u32,
52    },
53    /// Get references at position.
54    GetReferences {
55        lang_id: String,
56        rel_path: String,
57        line: u32,
58        character: u32,
59        include_declaration: bool,
60    },
61    /// Get implementations at position.
62    GetImplementations {
63        lang_id: String,
64        rel_path: String,
65        line: u32,
66        character: u32,
67    },
68    /// Close a file.
69    CloseFile {
70        lang_id: String,
71        rel_path: String,
72    },
73    /// Shut down a specific language server.
74    ShutdownLang { lang_id: String },
75    /// Shut down all servers and exit daemon.
76    ShutdownAll,
77    /// Get status of all managed servers.
78    Status,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82#[serde(tag = "type", rename_all = "snake_case")]
83enum DaemonResponse {
84    Ok,
85    Pong {
86        ready: bool,
87        uptime_secs: u64,
88    },
89    Definition {
90        location: Option<LocationDto>,
91    },
92    References {
93        locations: Vec<LocationDto>,
94    },
95    Implementations {
96        locations: Vec<LocationDto>,
97    },
98    Status {
99        servers: Vec<ServerStatus>,
100    },
101    Error {
102        message: String,
103    },
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107struct LocationDto {
108    file_path: String,
109    line: u32,
110    character: u32,
111}
112
113impl From<LspLocation> for LocationDto {
114    fn from(loc: LspLocation) -> Self {
115        Self {
116            file_path: loc.file_path,
117            line: loc.line,
118            character: loc.character,
119        }
120    }
121}
122
123impl From<LocationDto> for LspLocation {
124    fn from(dto: LocationDto) -> Self {
125        Self {
126            file_path: dto.file_path,
127            line: dto.line,
128            character: dto.character,
129        }
130    }
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134struct ServerStatus {
135    lang_id: String,
136    ready: bool,
137    uptime_secs: u64,
138    files_opened: usize,
139}
140
141// ═══════════════════════════════════════════════════════════════════════════════
142// Daemon server — runs as a background process
143// ═══════════════════════════════════════════════════════════════════════════════
144
145struct ManagedServer {
146    client: LspClient,
147    started_at: Instant,
148    last_used: Instant,
149    files_opened: usize,
150}
151
152/// The daemon process that owns LSP servers.
153pub struct LspDaemon {
154    servers: HashMap<String, ManagedServer>,
155    project_root: PathBuf,
156    socket_path: PathBuf,
157    idle_timeout: Duration,
158}
159
160/// Get the daemon socket path for a project.
161pub fn daemon_socket_path(project_root: &Path) -> PathBuf {
162    project_root.join(".gid").join("lsp-daemon.sock")
163}
164
165/// Get the daemon PID file path.
166pub fn daemon_pid_path(project_root: &Path) -> PathBuf {
167    project_root.join(".gid").join("lsp-daemon.pid")
168}
169
170impl LspDaemon {
171    /// Create a new daemon for the given project root.
172    pub fn new(project_root: &Path) -> Self {
173        let socket_path = daemon_socket_path(project_root);
174        Self {
175            servers: HashMap::new(),
176            project_root: project_root.to_path_buf(),
177            socket_path,
178            idle_timeout: Duration::from_secs(3600), // 1 hour idle timeout
179        }
180    }
181
182    /// Start a language server if not already running.
183    pub fn ensure_server(&mut self, lang_id: &str) -> Result<()> {
184        if self.servers.contains_key(lang_id) {
185            return Ok(());
186        }
187
188        let configs = LspServerConfig::detect_available();
189        let config = configs
190            .iter()
191            .find(|c| c.language_id == lang_id)
192            .ok_or_else(|| anyhow::anyhow!("No LSP server available for {}", lang_id))?;
193
194        eprintln!("[LSP daemon] Starting {} server for {}...", lang_id, self.project_root.display());
195        let client = LspClient::start(config, &self.project_root)?;
196
197        self.servers.insert(
198            lang_id.to_string(),
199            ManagedServer {
200                client,
201                started_at: Instant::now(),
202                last_used: Instant::now(),
203                files_opened: 0,
204            },
205        );
206
207        Ok(())
208    }
209
210    /// Wait for a specific server to be ready.
211    pub fn wait_ready(&mut self, lang_id: &str) -> Result<()> {
212        let server = self.servers.get_mut(lang_id)
213            .ok_or_else(|| anyhow::anyhow!("No server for {}", lang_id))?;
214        server.client.wait_until_ready(Duration::from_secs(600))
215    }
216
217    /// Handle a single request.
218    fn handle_request(&mut self, req: DaemonRequest) -> DaemonResponse {
219        match req {
220            DaemonRequest::Ping { lang_id } => {
221                let ready = self.servers.get(&lang_id).is_some();
222                let uptime = self.servers.get(&lang_id)
223                    .map(|s| s.started_at.elapsed().as_secs())
224                    .unwrap_or(0);
225                DaemonResponse::Pong { ready, uptime_secs: uptime }
226            }
227
228            DaemonRequest::OpenFile { lang_id, rel_path, content } => {
229                if let Err(e) = self.ensure_server(&lang_id) {
230                    return DaemonResponse::Error { message: e.to_string() };
231                }
232                let server = self.servers.get_mut(&lang_id).unwrap();
233                server.last_used = Instant::now();
234                match server.client.open_file(&rel_path, &content, &lang_id) {
235                    Ok(()) => {
236                        server.files_opened += 1;
237                        DaemonResponse::Ok
238                    }
239                    Err(e) => DaemonResponse::Error { message: e.to_string() },
240                }
241            }
242
243            DaemonRequest::GetDefinition { lang_id, rel_path, line, character } => {
244                let server = match self.servers.get_mut(&lang_id) {
245                    Some(s) => s,
246                    None => return DaemonResponse::Error {
247                        message: format!("No server for {}", lang_id),
248                    },
249                };
250                server.last_used = Instant::now();
251                match server.client.get_definition(&rel_path, line, character) {
252                    Ok(loc) => DaemonResponse::Definition {
253                        location: loc.map(LocationDto::from),
254                    },
255                    Err(e) => DaemonResponse::Error { message: e.to_string() },
256                }
257            }
258
259            DaemonRequest::GetReferences { lang_id, rel_path, line, character, include_declaration } => {
260                let server = match self.servers.get_mut(&lang_id) {
261                    Some(s) => s,
262                    None => return DaemonResponse::Error {
263                        message: format!("No server for {}", lang_id),
264                    },
265                };
266                server.last_used = Instant::now();
267                match server.client.get_references(&rel_path, line, character, include_declaration) {
268                    Ok(locs) => DaemonResponse::References {
269                        locations: locs.into_iter().map(LocationDto::from).collect(),
270                    },
271                    Err(e) => DaemonResponse::Error { message: e.to_string() },
272                }
273            }
274
275            DaemonRequest::GetImplementations { lang_id, rel_path, line, character } => {
276                let server = match self.servers.get_mut(&lang_id) {
277                    Some(s) => s,
278                    None => return DaemonResponse::Error {
279                        message: format!("No server for {}", lang_id),
280                    },
281                };
282                server.last_used = Instant::now();
283                match server.client.get_implementations(&rel_path, line, character) {
284                    Ok(locs) => DaemonResponse::Implementations {
285                        locations: locs.into_iter().map(LocationDto::from).collect(),
286                    },
287                    Err(e) => DaemonResponse::Error { message: e.to_string() },
288                }
289            }
290
291            DaemonRequest::CloseFile { lang_id, rel_path } => {
292                if let Some(server) = self.servers.get_mut(&lang_id) {
293                    server.last_used = Instant::now();
294                    let _ = server.client.close_file(&rel_path);
295                }
296                DaemonResponse::Ok
297            }
298
299            DaemonRequest::ShutdownLang { lang_id } => {
300                if let Some(server) = self.servers.remove(&lang_id) {
301                    let _ = server.client.shutdown();
302                }
303                DaemonResponse::Ok
304            }
305
306            DaemonRequest::ShutdownAll => {
307                let keys: Vec<String> = self.servers.keys().cloned().collect();
308                for key in keys {
309                    if let Some(server) = self.servers.remove(&key) {
310                        let _ = server.client.shutdown();
311                    }
312                }
313                DaemonResponse::Ok
314            }
315
316            DaemonRequest::Status => {
317                let servers = self.servers.iter().map(|(lang_id, server)| {
318                    ServerStatus {
319                        lang_id: lang_id.clone(),
320                        ready: true,
321                        uptime_secs: server.started_at.elapsed().as_secs(),
322                        files_opened: server.files_opened,
323                    }
324                }).collect();
325                DaemonResponse::Status { servers }
326            }
327        }
328    }
329
330    /// Run the daemon, listening on a Unix socket.
331    pub fn run(&mut self) -> Result<()> {
332        // Clean up stale socket
333        if self.socket_path.exists() {
334            std::fs::remove_file(&self.socket_path)?;
335        }
336
337        // Ensure .gid directory exists
338        if let Some(parent) = self.socket_path.parent() {
339            std::fs::create_dir_all(parent)?;
340        }
341
342        // Write PID file
343        let pid_path = daemon_pid_path(&self.project_root);
344        std::fs::write(&pid_path, std::process::id().to_string())?;
345
346        let listener = UnixListener::bind(&self.socket_path)
347            .with_context(|| format!("bind socket: {}", self.socket_path.display()))?;
348
349        // Set non-blocking so we can check idle timeout
350        listener.set_nonblocking(true)?;
351
352        eprintln!("[LSP daemon] Listening on {}", self.socket_path.display());
353
354        let mut last_activity = Instant::now();
355
356        loop {
357            // Check idle timeout
358            if !self.servers.is_empty() {
359                let all_idle = self.servers.values()
360                    .all(|s| s.last_used.elapsed() > self.idle_timeout);
361                if all_idle {
362                    eprintln!("[LSP daemon] All servers idle for {}s, shutting down",
363                        self.idle_timeout.as_secs());
364                    break;
365                }
366            } else if last_activity.elapsed() > Duration::from_secs(300) {
367                // No servers and no activity for 5 min
368                eprintln!("[LSP daemon] No servers and idle for 5 min, shutting down");
369                break;
370            }
371
372            match listener.accept() {
373                Ok((stream, _)) => {
374                    last_activity = Instant::now();
375                    if let Err(e) = self.handle_connection(stream) {
376                        eprintln!("[LSP daemon] Connection error: {}", e);
377                    }
378                }
379                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
380                    // No connection pending, sleep briefly
381                    std::thread::sleep(Duration::from_millis(100));
382                }
383                Err(e) => {
384                    eprintln!("[LSP daemon] Accept error: {}", e);
385                    break;
386                }
387            }
388        }
389
390        // Cleanup
391        self.handle_request(DaemonRequest::ShutdownAll);
392        let _ = std::fs::remove_file(&self.socket_path);
393        let _ = std::fs::remove_file(&pid_path);
394
395        eprintln!("[LSP daemon] Stopped.");
396        Ok(())
397    }
398
399    fn handle_connection(&mut self, stream: UnixStream) -> Result<()> {
400        stream.set_read_timeout(Some(Duration::from_secs(60)))?;
401        stream.set_write_timeout(Some(Duration::from_secs(60)))?;
402
403        let mut reader = BufReader::new(stream.try_clone()?);
404        let mut writer = stream;
405
406        // Read requests until connection closes
407        loop {
408            let mut line = String::new();
409            match reader.read_line(&mut line) {
410                Ok(0) => break, // Connection closed
411                Ok(_) => {}
412                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock
413                    || e.kind() == std::io::ErrorKind::TimedOut => break,
414                Err(e) => return Err(e.into()),
415            }
416
417            let line = line.trim();
418            if line.is_empty() {
419                continue;
420            }
421
422            let req: DaemonRequest = match serde_json::from_str(line) {
423                Ok(r) => r,
424                Err(e) => {
425                    let resp = DaemonResponse::Error {
426                        message: format!("Invalid request: {}", e),
427                    };
428                    let mut resp_line = serde_json::to_string(&resp)?;
429                    resp_line.push('\n');
430                    writer.write_all(resp_line.as_bytes())?;
431                    writer.flush()?;
432                    continue;
433                }
434            };
435
436            let is_shutdown_all = matches!(req, DaemonRequest::ShutdownAll);
437            let resp = self.handle_request(req);
438
439            let mut resp_line = serde_json::to_string(&resp)?;
440            resp_line.push('\n');
441            writer.write_all(resp_line.as_bytes())?;
442            writer.flush()?;
443
444            if is_shutdown_all {
445                return Ok(());
446            }
447        }
448
449        Ok(())
450    }
451}
452
453// ═══════════════════════════════════════════════════════════════════════════════
454// Daemon client — used by `gid extract` to talk to the daemon
455// ═══════════════════════════════════════════════════════════════════════════════
456
457/// Client that connects to a running LSP daemon via Unix socket.
458/// Implements the same interface as LspClient for drop-in use.
459pub struct DaemonLspClient {
460    stream: BufReader<UnixStream>,
461    writer: UnixStream,
462    lang_id: String,
463}
464
465impl DaemonLspClient {
466    /// Connect to a running daemon.
467    pub fn connect(project_root: &Path, lang_id: &str) -> Result<Self> {
468        let socket_path = daemon_socket_path(project_root);
469        let stream = UnixStream::connect(&socket_path)
470            .with_context(|| format!("connect to LSP daemon at {}", socket_path.display()))?;
471        stream.set_read_timeout(Some(Duration::from_secs(120)))?;
472        stream.set_write_timeout(Some(Duration::from_secs(30)))?;
473
474        Ok(Self {
475            stream: BufReader::new(stream.try_clone()?),
476            writer: stream,
477            lang_id: lang_id.to_string(),
478        })
479    }
480
481    fn send_recv(&mut self, req: DaemonRequest) -> Result<DaemonResponse> {
482        let mut line = serde_json::to_string(&req)?;
483        line.push('\n');
484        self.writer.write_all(line.as_bytes())?;
485        self.writer.flush()?;
486
487        let mut resp_line = String::new();
488        self.stream.read_line(&mut resp_line)?;
489
490        let resp: DaemonResponse = serde_json::from_str(resp_line.trim())?;
491        Ok(resp)
492    }
493
494    /// Check if the daemon has a ready server for our language.
495    pub fn ping(&mut self) -> Result<bool> {
496        match self.send_recv(DaemonRequest::Ping { lang_id: self.lang_id.clone() })? {
497            DaemonResponse::Pong { ready, .. } => Ok(ready),
498            DaemonResponse::Error { message } => bail!("Daemon error: {}", message),
499            _ => bail!("Unexpected response to ping"),
500        }
501    }
502
503    /// Open a file.
504    pub fn open_file(&mut self, rel_path: &str, content: &str) -> Result<()> {
505        match self.send_recv(DaemonRequest::OpenFile {
506            lang_id: self.lang_id.clone(),
507            rel_path: rel_path.to_string(),
508            content: content.to_string(),
509        })? {
510            DaemonResponse::Ok => Ok(()),
511            DaemonResponse::Error { message } => bail!("{}", message),
512            _ => bail!("Unexpected response"),
513        }
514    }
515
516    /// Get definition.
517    pub fn get_definition(&mut self, rel_path: &str, line: u32, character: u32) -> Result<Option<LspLocation>> {
518        match self.send_recv(DaemonRequest::GetDefinition {
519            lang_id: self.lang_id.clone(),
520            rel_path: rel_path.to_string(),
521            line,
522            character,
523        })? {
524            DaemonResponse::Definition { location } => Ok(location.map(LspLocation::from)),
525            DaemonResponse::Error { message } => bail!("{}", message),
526            _ => bail!("Unexpected response"),
527        }
528    }
529
530    /// Get references.
531    pub fn get_references(
532        &mut self,
533        rel_path: &str,
534        line: u32,
535        character: u32,
536        include_declaration: bool,
537    ) -> Result<Vec<LspLocation>> {
538        match self.send_recv(DaemonRequest::GetReferences {
539            lang_id: self.lang_id.clone(),
540            rel_path: rel_path.to_string(),
541            line,
542            character,
543            include_declaration,
544        })? {
545            DaemonResponse::References { locations } => {
546                Ok(locations.into_iter().map(LspLocation::from).collect())
547            }
548            DaemonResponse::Error { message } => bail!("{}", message),
549            _ => bail!("Unexpected response"),
550        }
551    }
552
553    /// Get implementations.
554    pub fn get_implementations(&mut self, rel_path: &str, line: u32, character: u32) -> Result<Vec<LspLocation>> {
555        match self.send_recv(DaemonRequest::GetImplementations {
556            lang_id: self.lang_id.clone(),
557            rel_path: rel_path.to_string(),
558            line,
559            character,
560        })? {
561            DaemonResponse::Implementations { locations } => {
562                Ok(locations.into_iter().map(LspLocation::from).collect())
563            }
564            DaemonResponse::Error { message } => bail!("{}", message),
565            _ => bail!("Unexpected response"),
566        }
567    }
568
569    /// Close a file.
570    pub fn close_file(&mut self, rel_path: &str) -> Result<()> {
571        let _ = self.send_recv(DaemonRequest::CloseFile {
572            lang_id: self.lang_id.clone(),
573            rel_path: rel_path.to_string(),
574        });
575        Ok(())
576    }
577}
578
579// ═══════════════════════════════════════════════════════════════════════════════
580// Helpers for gid extract integration
581// ═══════════════════════════════════════════════════════════════════════════════
582
583/// Check if a daemon is already running for this project.
584pub fn is_daemon_running(project_root: &Path) -> bool {
585    let socket_path = daemon_socket_path(project_root);
586
587    if !socket_path.exists() {
588        return false;
589    }
590
591    // Try connecting — if it works, daemon is alive
592    if UnixStream::connect(&socket_path).is_ok() {
593        return true;
594    }
595
596    // Stale socket, clean up
597    let _ = std::fs::remove_file(&socket_path);
598    let _ = std::fs::remove_file(&daemon_pid_path(project_root));
599    false
600}
601
602/// Start daemon as a background process (fork).
603/// Returns Ok(true) if we started a new daemon, Ok(false) if one was already running.
604pub fn ensure_daemon(project_root: &Path) -> Result<bool> {
605    if is_daemon_running(project_root) {
606        return Ok(false);
607    }
608
609    eprintln!("[LSP] Starting daemon for {}...", project_root.display());
610
611    // Fork a background process
612    // We use std::process::Command to spawn ourselves with a special flag
613    // But since we can't easily re-exec, we'll use the simpler approach:
614    // spawn a thread that runs the daemon in the background
615    let root = project_root.to_path_buf();
616    std::thread::spawn(move || {
617        let mut daemon = LspDaemon::new(&root);
618        if let Err(e) = daemon.run() {
619            eprintln!("[LSP daemon] Error: {}", e);
620        }
621    });
622
623    // Wait for socket to appear
624    let socket_path = daemon_socket_path(project_root);
625    let deadline = Instant::now() + Duration::from_secs(5);
626    while Instant::now() < deadline {
627        if socket_path.exists() {
628            return Ok(true);
629        }
630        std::thread::sleep(Duration::from_millis(100));
631    }
632
633    bail!("Daemon did not start within 5 seconds")
634}
635
636/// Start the daemon, wait for LSP server to be ready, then return a client.
637/// This is the main entry point for `gid extract --lsp`.
638pub fn get_or_start_daemon_client(
639    project_root: &Path,
640    lang_id: &str,
641) -> Result<DaemonLspClient> {
642    ensure_daemon(project_root)?;
643    DaemonLspClient::connect(project_root, lang_id)
644}
645
646/// Stop the daemon for a project.
647pub fn stop_daemon(project_root: &Path) -> Result<()> {
648    if !is_daemon_running(project_root) {
649        return Ok(());
650    }
651
652    let socket_path = daemon_socket_path(project_root);
653    if let Ok(stream) = UnixStream::connect(&socket_path) {
654        stream.set_write_timeout(Some(Duration::from_secs(5)))?;
655        let mut writer = stream;
656        let req = serde_json::to_string(&DaemonRequest::ShutdownAll)?;
657        let _ = writer.write_all(format!("{}\n", req).as_bytes());
658        let _ = writer.flush();
659    }
660
661    // Wait for cleanup
662    std::thread::sleep(Duration::from_millis(500));
663
664    // Force cleanup if needed
665    let _ = std::fs::remove_file(&socket_path);
666    let _ = std::fs::remove_file(&daemon_pid_path(project_root));
667
668    Ok(())
669}