use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use super::protocol::{DaemonRequest, DaemonResponse, read_message, write_message};
pub(super) fn create_fifo(path: &Path, mode: libc::mode_t) -> Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path =
CString::new(path.as_os_str().as_bytes()).context("FIFO path contains null byte")?;
let ret = unsafe { libc::mkfifo(c_path.as_ptr(), mode) };
if ret != 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EEXIST) {
return Err(err).with_context(|| format!("mkfifo failed: {}", path.display()));
}
}
Ok(())
}
pub(super) fn flock_exclusive(file: &File) -> Result<()> {
let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if ret != 0 {
return Err(std::io::Error::last_os_error()).context("flock(LOCK_EX) failed");
}
Ok(())
}
pub(super) fn poll_retry(pfd: &mut libc::pollfd, timeout_ms: libc::c_int) -> Result<libc::c_int> {
loop {
let n = unsafe { libc::poll(pfd, 1, timeout_ms) };
if n >= 0 {
return Ok(n);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINTR) {
return Err(err).context("poll() failed");
}
}
}
pub(super) fn set_nonblocking(file: &File, nonblock: bool) -> Result<()> {
let fd = file.as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags == -1 {
return Err(std::io::Error::last_os_error()).context("fcntl(F_GETFL) failed");
}
let new_flags = if nonblock {
flags | libc::O_NONBLOCK
} else {
flags & !libc::O_NONBLOCK
};
if libc::fcntl(fd, libc::F_SETFL, new_flags) == -1 {
return Err(std::io::Error::last_os_error()).context("fcntl(F_SETFL) failed");
}
}
Ok(())
}
pub fn daemon_dir(target_dir: &Path, workspace_root: &Path) -> PathBuf {
let canonical = workspace_root
.canonicalize()
.unwrap_or_else(|_| workspace_root.to_path_buf());
let hash = short_hash(&canonical);
target_dir.join("cargo-brief-lsp").join(hash)
}
pub fn ensure_daemon(target_dir: &Path, workspace_root: &Path, verbose: bool) -> Result<PathBuf> {
let dir = daemon_dir(target_dir, workspace_root);
let pid_file = dir.join("lsp.pid");
let req_fifo = dir.join("lsp.req");
if req_fifo.exists()
&& pid_file.exists()
&& let Ok(pid_str) = std::fs::read_to_string(&pid_file)
&& let Ok(pid) = pid_str.trim().parse::<u32>()
&& process_alive(pid)
{
if verbose {
eprintln!("[lsp] daemon already running (PID {pid})");
}
return Ok(dir);
}
if pid_file.exists()
&& let Ok(pid_str) = std::fs::read_to_string(&pid_file)
&& let Ok(pid) = pid_str.trim().parse::<u32>()
&& !process_alive(pid)
{
if verbose {
eprintln!("[lsp] cleaning up stale daemon (PID {pid})");
}
cleanup_daemon_files(&dir);
}
std::fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create daemon dir: {}", dir.display()))?;
let log_path = dir.join("lsp.log");
if verbose {
eprintln!("[lsp] spawning daemon for {}", workspace_root.display());
}
let mut child = spawn_daemon(workspace_root, &dir, &log_path)?;
wait_for_daemon(&dir, Duration::from_secs(120), &mut child, &log_path)?;
Ok(dir)
}
pub fn send_command(
daemon_dir: &Path,
request: DaemonRequest,
timeout: Duration,
) -> Result<DaemonResponse> {
let lock_path = daemon_dir.join("lsp.lock");
let req_path = daemon_dir.join("lsp.req");
let resp_path = daemon_dir.join("lsp.resp");
let lock_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.context("Failed to open lock file")?;
flock_exclusive(&lock_file)?;
let mut req_fd = OpenOptions::new()
.write(true)
.open(&req_path)
.context("Failed to open request FIFO")?;
write_message(&mut req_fd, &request)?;
drop(req_fd);
let resp_fd = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(&resp_path)
.context("Failed to open response FIFO")?;
let mut drain_buf = [0u8; 4096];
loop {
let n = unsafe {
libc::read(
resp_fd.as_raw_fd(),
drain_buf.as_mut_ptr() as *mut libc::c_void,
drain_buf.len(),
)
};
if n <= 0 {
break;
}
}
let timeout_ms: libc::c_int = timeout.as_millis().try_into().unwrap_or(libc::c_int::MAX);
let mut pfd = libc::pollfd {
fd: resp_fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let n = poll_retry(&mut pfd, timeout_ms)?;
if n == 0 {
bail!(
"Timed out waiting for daemon response ({}s)",
timeout.as_secs()
);
}
set_nonblocking(&resp_fd, false)?;
let mut resp_fd = resp_fd;
let response: DaemonResponse = read_message(&mut resp_fd)?;
Ok(response)
}
pub(super) fn cleanup_daemon_files(dir: &Path) {
for name in ["lsp.pid", "lsp.req", "lsp.resp", "lsp.lock", "lsp.log"] {
std::fs::remove_file(dir.join(name)).ok();
}
}
fn spawn_daemon(workspace_root: &Path, daemon_dir: &Path, log_path: &Path) -> Result<Child> {
use std::os::unix::process::CommandExt;
let exe = std::env::current_exe().context("Failed to get current executable path")?;
let ws_str = workspace_root
.to_str()
.context("Non-UTF8 workspace root path")?;
let dir_str = daemon_dir.to_str().context("Non-UTF8 daemon dir path")?;
let log_file = File::create(log_path)
.with_context(|| format!("Failed to create daemon log: {}", log_path.display()))?;
let child = Command::new(exe)
.args([
"__lsp-daemon",
"--workspace-root",
ws_str,
"--daemon-dir",
dir_str,
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(log_file))
.process_group(0)
.spawn()
.context("Failed to spawn LSP daemon process")?;
Ok(child)
}
fn wait_for_daemon(
daemon_dir: &Path,
timeout: Duration,
child: &mut Child,
log_path: &Path,
) -> Result<()> {
let start = Instant::now();
let mut interval = Duration::from_millis(50);
let pid = child.id();
let req_fifo = daemon_dir.join("lsp.req");
while start.elapsed() < timeout {
if req_fifo.exists() {
return Ok(());
}
if let Ok(Some(_status)) = child.try_wait() {
let tail = read_log_tail(log_path, 20);
let log_section = if tail.is_empty() {
"(no log output)".to_string()
} else {
tail
};
bail!(
"LSP daemon (PID {pid}) died during startup.\n\
Daemon log:\n{log_section}"
);
}
std::thread::sleep(interval);
interval = (interval * 2).min(Duration::from_millis(500));
}
let tail = read_log_tail(log_path, 20);
let log_section = if tail.is_empty() {
"(no log output)".to_string()
} else {
tail
};
bail!(
"Timed out waiting for LSP daemon after {}s.\n\
Daemon dir: {}\n\
Daemon log:\n{log_section}",
timeout.as_secs(),
daemon_dir.display()
)
}
fn read_log_tail(path: &Path, max_lines: usize) -> String {
let Ok(content) = std::fs::read_to_string(path) else {
return String::new();
};
let lines: Vec<&str> = content.lines().collect();
let start = lines.len().saturating_sub(max_lines);
lines[start..].join("\n")
}
fn process_alive(pid: u32) -> bool {
let Ok(pid) = libc::pid_t::try_from(pid) else {
return false;
};
unsafe { libc::kill(pid, 0) == 0 }
}
fn short_hash(path: &Path) -> String {
let bytes = path.as_os_str().as_encoded_bytes();
let mut hash: u64 = 0xcbf29ce484222325;
for &b in bytes {
hash ^= b as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
format!("{hash:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_deterministic() {
let h1 = short_hash(Path::new("/home/user/project"));
let h2 = short_hash(Path::new("/home/user/project"));
assert_eq!(h1, h2);
}
#[test]
fn hash_differs_for_different_paths() {
let h1 = short_hash(Path::new("/home/user/project-a"));
let h2 = short_hash(Path::new("/home/user/project-b"));
assert_ne!(h1, h2);
}
#[test]
fn hash_is_16_hex_chars() {
let h = short_hash(Path::new("/some/path"));
assert_eq!(h.len(), 16);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn log_tail_more_than_max() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
let content: String = (1..=30).map(|i| format!("line {i}\n")).collect();
std::fs::write(&path, &content).unwrap();
let tail = read_log_tail(&path, 20);
let lines: Vec<&str> = tail.lines().collect();
assert_eq!(lines.len(), 20);
assert_eq!(lines[0], "line 11");
assert_eq!(lines[19], "line 30");
}
#[test]
fn log_tail_fewer_than_max() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.log");
std::fs::write(&path, "line 1\nline 2\nline 3\n").unwrap();
let tail = read_log_tail(&path, 20);
let lines: Vec<&str> = tail.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "line 1");
}
#[test]
fn log_tail_nonexistent_file() {
let tail = read_log_tail(Path::new("/nonexistent/file.log"), 20);
assert!(tail.is_empty());
}
#[test]
fn log_tail_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.log");
std::fs::write(&path, "").unwrap();
let tail = read_log_tail(&path, 20);
assert!(tail.is_empty());
}
#[test]
fn create_fifo_creates_pipe() {
use std::os::unix::fs::FileTypeExt;
let dir = tempfile::tempdir().unwrap();
let fifo = dir.path().join("test.fifo");
create_fifo(&fifo, 0o600).unwrap();
assert!(std::fs::metadata(&fifo).unwrap().file_type().is_fifo());
}
#[test]
fn create_fifo_idempotent() {
let dir = tempfile::tempdir().unwrap();
let fifo = dir.path().join("test.fifo");
create_fifo(&fifo, 0o600).unwrap();
create_fifo(&fifo, 0o600).unwrap(); }
#[test]
fn flock_exclusive_blocks_second() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.lock");
let f1 = File::create(&path).unwrap();
flock_exclusive(&f1).unwrap();
let f2 = File::open(&path).unwrap();
let ret = unsafe { libc::flock(f2.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
assert_ne!(ret, 0);
let err = std::io::Error::last_os_error();
assert_eq!(err.raw_os_error(), Some(libc::EWOULDBLOCK));
}
#[test]
fn set_nonblocking_toggles_flag() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.file");
let f = File::create(&path).unwrap();
set_nonblocking(&f, true).unwrap();
let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) };
assert_ne!(flags & libc::O_NONBLOCK, 0);
set_nonblocking(&f, false).unwrap();
let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFL) };
assert_eq!(flags & libc::O_NONBLOCK, 0);
}
}