laburnum 1.17.1

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! Daemon mode for laburnum language servers.
//!
//! This module provides infrastructure for running language servers as daemon
//! processes, allowing multiple clients (IDEs and CLIs) to share a single
//! server instance.
//!
//! ## Architecture
//!
//! ```text
//! IDE → (stdio) → Bridge Process → (IPC) → Daemon (detached)
//!                                             ├─ Scheduler
//!                                             │   ├─ DaemonTask (accept loop)
//!                                             │   └─ RpcTask (one per client)
//!                                             ├─ IpcServer
//!                                             ├─ ClientRegistry
//!                                             └─ DaemonServer (setup & coordination)
//! ```

pub(crate) mod daemon_task;
pub(crate) mod idle_monitor;
pub mod server;
pub mod socket;
pub mod spawn;

use {
  crate::protocol::ipc::{
    Endpoint,
    runtime_dir::runtime_socket_dir,
  },
  std::path::PathBuf,
};
pub use {
  crate::connect::lsp::DaemonConnection,
  server::DaemonServer,
  socket::{
    DaemonStatus,
    check_daemon_status,
    cleanup_stale_socket,
    wait_for_daemon,
    write_pid_file,
  },
  spawn::{
    daemonize,
    spawn_daemon,
  },
};

const DEFAULT_SHUTDOWN_TIMEOUT: std::time::Duration =
  std::time::Duration::from_secs(5);

#[derive(Debug, Clone)]
pub struct DaemonConfig {
  pub server_name:      String,
  pub workspace_id:     String,
  pub idle_timeout:     Option<std::time::Duration>,
  pub shutdown_timeout: std::time::Duration,
}

impl DaemonConfig {
  pub fn new(
    server_name: impl Into<String>,
    workspace_id: impl Into<String>,
  ) -> Self {
    let server_name = server_name.into();
    let workspace_id = workspace_id.into();

    assert!(!server_name.is_empty(), "server_name must not be empty");
    assert!(!workspace_id.is_empty(), "workspace_id must not be empty");
    assert!(
      !server_name.contains('/') && !server_name.contains('\\'),
      "server_name must not contain path separators"
    );
    assert!(
      !workspace_id.contains('/') && !workspace_id.contains('\\'),
      "workspace_id must not contain path separators"
    );

    Self {
      server_name,
      workspace_id,
      idle_timeout: None,
      shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
    }
  }

  pub fn with_idle_timeout(mut self, timeout: std::time::Duration) -> Self {
    self.idle_timeout = Some(timeout);
    self
  }

  pub fn with_shutdown_timeout(mut self, timeout: std::time::Duration) -> Self {
    self.shutdown_timeout = timeout;
    self
  }

  pub fn runtime_dir(&self) -> PathBuf {
    runtime_socket_dir(&self.server_name, None).join(&self.workspace_id)
  }

  #[cfg(unix)]
  pub fn socket_path(&self) -> PathBuf {
    self.runtime_dir().join("lsp.sock")
  }

  #[cfg(unix)]
  pub fn pid_file_path(&self) -> PathBuf {
    self.runtime_dir().join("daemon.pid")
  }

  #[cfg(unix)]
  pub fn lock_file_path(&self) -> PathBuf {
    self.runtime_dir().join("daemon.lock")
  }

  pub fn endpoint(&self) -> Endpoint {
    #[cfg(unix)]
    {
      Endpoint::UnixSocket(self.socket_path())
    }

    #[cfg(windows)]
    {
      Endpoint::NamedPipe(format!(
        "\\\\.\\pipe\\{}-{}",
        self.server_name, self.workspace_id
      ))
    }
  }
}

pub fn compute_workspace_id(path: &std::path::Path) -> String {
  use std::hash::{
    Hash,
    Hasher,
  };

  let mut hasher = std::collections::hash_map::DefaultHasher::new();
  path.hash(&mut hasher);
  let hash = hasher.finish();

  format!("{:016x}", hash)
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_daemon_config_endpoint() {
    let config = DaemonConfig::new("test-server", "workspace123");
    let endpoint = config.endpoint();

    #[cfg(unix)]
    {
      if let Endpoint::UnixSocket(path) = endpoint {
        assert!(path.to_string_lossy().contains("workspace123"));
        assert!(path.to_string_lossy().ends_with("lsp.sock"));
      } else {
        panic!("expected UnixSocket on Unix");
      }
    }

    #[cfg(windows)]
    {
      if let Endpoint::NamedPipe(name) = endpoint {
        assert!(name.contains("test-server"));
        assert!(name.contains("workspace123"));
      } else {
        panic!("expected NamedPipe on Windows");
      }
    }
  }

  #[test]
  fn test_daemon_config_socket_path() {
    let config = DaemonConfig::new("test-server", "workspace456");

    #[cfg(unix)]
    {
      let socket_path = config.socket_path();
      assert!(socket_path.to_string_lossy().contains("workspace456"));
      assert!(socket_path.to_string_lossy().ends_with("lsp.sock"));
    }
  }

  #[test]
  fn test_daemon_config_pid_file_path() {
    let config = DaemonConfig::new("test-server", "workspace789");

    #[cfg(unix)]
    {
      let pid_path = config.pid_file_path();
      assert!(pid_path.to_string_lossy().contains("workspace789"));
      assert!(pid_path.to_string_lossy().ends_with("daemon.pid"));
    }
  }

  #[test]
  fn test_compute_workspace_id() {
    let path1 = std::path::Path::new("/home/user/project");
    let path2 = std::path::Path::new("/home/user/other");
    let path3 = std::path::Path::new("/home/user/project");

    let id1 = compute_workspace_id(path1);
    let id2 = compute_workspace_id(path2);
    let id3 = compute_workspace_id(path3);

    assert_eq!(id1.len(), 16);
    assert_ne!(id1, id2);
    assert_eq!(id1, id3);
  }

  #[test]
  fn test_daemon_config_with_idle_timeout() {
    let config = DaemonConfig::new("test", "ws")
      .with_idle_timeout(std::time::Duration::from_secs(300));

    assert_eq!(
      config.idle_timeout,
      Some(std::time::Duration::from_secs(300))
    );
  }
}