use std::io::ErrorKind;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::Context;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use tracing::{debug, info, warn};
use super::protocol::{
DaemonRequest, DaemonResponse, LspNotifyParams, LspRequestParams, SpawnParams,
SyncDocumentParams, WaitNotifyParams,
};
use super::socket::socket_path_for_workspace;
const DAEMON_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
pub struct DaemonClient {
#[allow(dead_code)]
socket_path: PathBuf,
reader: BufReader<tokio::io::ReadHalf<UnixStream>>,
writer: tokio::io::WriteHalf<UnixStream>,
next_id: u64,
owns_daemon: bool,
}
impl DaemonClient {
pub async fn connect_or_start(workspace_root: &str) -> Result<Self, anyhow::Error> {
let socket_path = socket_path_for_workspace(workspace_root);
match UnixStream::connect(&socket_path).await {
Ok(stream) => {
info!(path = %socket_path.display(), "Connected to existing daemon");
let (rh, wh) = tokio::io::split(stream);
return Ok(Self {
socket_path,
reader: BufReader::new(rh),
writer: wh,
next_id: 1,
owns_daemon: false,
});
}
Err(_) => {
}
}
info!(path = %socket_path.display(), "No daemon found, auto-starting");
spawn_background_daemon(&socket_path, workspace_root)?;
let start = std::time::Instant::now();
while start.elapsed() < DAEMON_STARTUP_TIMEOUT {
tokio::time::sleep(Duration::from_millis(200)).await;
match UnixStream::connect(&socket_path).await {
Ok(stream) => {
info!("Connected to freshly spawned daemon");
let (rh, wh) = tokio::io::split(stream);
return Ok(Self {
socket_path,
reader: BufReader::new(rh),
writer: wh,
next_id: 1,
owns_daemon: true,
});
}
Err(e)
if e.kind() == ErrorKind::ConnectionRefused
|| e.kind() == ErrorKind::NotFound =>
{
continue;
}
Err(e) => {
warn!(error = %e, "Unexpected error connecting to daemon");
continue;
}
}
}
anyhow::bail!("Daemon failed to start within {DAEMON_STARTUP_TIMEOUT:?}");
}
pub async fn connect_explicit(socket: &PathBuf) -> Result<Self, anyhow::Error> {
let stream = UnixStream::connect(socket)
.await
.with_context(|| format!("Cannot connect to daemon at {:?}", socket))?;
let (rh, wh) = tokio::io::split(stream);
Ok(Self {
socket_path: socket.clone(),
reader: BufReader::new(rh),
writer: wh,
next_id: 1,
owns_daemon: false,
})
}
pub async fn try_connect(workspace_root: &str) -> Result<Option<Self>, anyhow::Error> {
let socket_path = socket_path_for_workspace(workspace_root);
match UnixStream::connect(&socket_path).await {
Ok(stream) => {
let (rh, wh) = tokio::io::split(stream);
Ok(Some(Self {
socket_path,
reader: BufReader::new(rh),
writer: wh,
next_id: 1,
owns_daemon: false,
}))
}
Err(_) => Ok(None),
}
}
pub async fn spawn_session(&mut self, params: &SpawnParams) -> Result<String, anyhow::Error> {
let resp = self
.request("lsp/spawn", &serde_json::to_value(params)?)
.await?;
if let Some(err) = resp.error {
anyhow::bail!("spawn failed: {err}");
}
Ok(resp.result["session_key"]
.as_str()
.context("missing session_key")?
.to_string())
}
pub async fn lsp_request(
&mut self,
session_key: &str,
method: &str,
params: Value,
) -> Result<Value, anyhow::Error> {
let req = LspRequestParams {
session_key: session_key.to_string(),
method: method.to_string(),
params,
};
let resp = self
.request("lsp/request", &serde_json::to_value(&req)?)
.await?;
if let Some(err) = resp.error {
anyhow::bail!("LSP request '{}' failed: {err}", method);
}
Ok(resp.result)
}
pub async fn lsp_notify(
&mut self,
session_key: &str,
method: &str,
params: Value,
) -> Result<(), anyhow::Error> {
let req = LspNotifyParams {
session_key: session_key.to_string(),
method: method.to_string(),
params,
};
let resp = self
.request("lsp/notify", &serde_json::to_value(&req)?)
.await?;
if let Some(err) = resp.error {
anyhow::bail!("LSP notify '{}' failed: {err}", method);
}
Ok(())
}
pub async fn lsp_wait_notify(
&mut self,
session_key: &str,
method: &str,
filter_uri: Option<&str>,
timeout_ms: Option<u64>,
) -> Result<Value, anyhow::Error> {
let req = WaitNotifyParams {
session_key: session_key.to_string(),
method: method.to_string(),
filter_uri: filter_uri.map(|s| s.to_string()),
timeout_ms,
};
let resp = self
.request("lsp/wait_notify", &serde_json::to_value(&req)?)
.await?;
if let Some(err) = resp.error {
anyhow::bail!("wait_notify '{}' failed: {err}", method);
}
Ok(resp.result)
}
pub async fn lsp_sync_document(
&mut self,
session_key: &str,
uri: &str,
language_id: &str,
content: &str,
) -> Result<(), anyhow::Error> {
let req = SyncDocumentParams {
session_key: session_key.to_string(),
uri: uri.to_string(),
language_id: language_id.to_string(),
content: content.to_string(),
};
let resp = self
.request("lsp/sync_document", &serde_json::to_value(&req)?)
.await?;
if let Some(err) = resp.error {
anyhow::bail!("LSP sync_document failed: {err}");
}
Ok(())
}
pub async fn get_status(&mut self) -> Result<Value, anyhow::Error> {
let resp = self
.request("daemon/status", &serde_json::json!({}))
.await?;
if let Some(err) = resp.error {
anyhow::bail!("status failed: {err}");
}
Ok(resp.result)
}
pub async fn shutdown(&mut self) -> Result<(), anyhow::Error> {
let _ = self
.request("daemon/shutdown", &serde_json::json!({}))
.await;
Ok(())
}
async fn request(
&mut self,
method: &str,
params: &Value,
) -> Result<DaemonResponse, anyhow::Error> {
let id = self.next_id;
self.next_id += 1;
let req = DaemonRequest {
id,
method: method.to_string(),
params: params.clone(),
};
let json = format!("{}\n", serde_json::to_string(&req)?);
self.writer.write_all(json.as_bytes()).await?;
self.writer.flush().await?;
loop {
let mut line = String::new();
tokio::time::timeout(Duration::from_secs(30), self.reader.read_line(&mut line))
.await
.context("Timeout waiting for daemon response")?
.context("Daemon connection closed")?;
let resp: DaemonResponse = serde_json::from_str(line.trim())?;
if resp.id == id {
return Ok(resp);
}
warn!(
expected = id,
got = resp.id,
error = ?resp.error,
"Drained orphan/out-of-order daemon response from a cancelled or \
earlier request; protocol stays in sync",
);
}
}
}
impl Drop for DaemonClient {
fn drop(&mut self) {
if !self.owns_daemon {
debug!("DaemonClient dropped (owns_daemon=false)");
return;
}
let socket = self.socket_path.clone();
debug!(
?socket,
"DaemonClient dropped (owns_daemon=true); requesting shutdown"
);
std::thread::Builder::new()
.name("lspz-daemon-shutdown".into())
.spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
warn!(error = %e, "Failed to build runtime for daemon shutdown");
return;
}
};
rt.block_on(async {
match DaemonClient::connect_explicit(&socket).await {
Ok(mut client) => {
if let Err(e) = client.shutdown().await {
warn!(error = %e, "daemon/shutdown after Drop failed");
}
}
Err(e) => {
debug!(error = %e, "Could not connect to shut down owned daemon");
}
}
});
})
.ok();
}
}
fn spawn_background_daemon(
socket_path: &PathBuf,
workspace_root: &str,
) -> Result<(), anyhow::Error> {
let exe = std::env::current_exe().context("Could not determine lspz binary path")?;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("daemon")
.arg("--socket")
.arg(socket_path)
.env("LSPZ_DAEMON_WORKSPACE", workspace_root)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
libc::setsid();
Ok(())
});
}
}
let _child = cmd.spawn().context("Failed to spawn lspz daemon process")?;
Ok(())
}