use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use crate::lsp::protocol::{DaemonRequest, DaemonResponse, read_message, write_message};
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(())
}
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(in crate::lsp) 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");
}
}
}
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(in crate::lsp) struct DaemonIpc {
req_fd: File, resp_fd: File, }
impl DaemonIpc {
pub fn setup(daemon_dir: &Path) -> Result<Self> {
let req_path = daemon_dir.join("lsp.req");
let resp_path = daemon_dir.join("lsp.resp");
let lock_path = daemon_dir.join("lsp.lock");
std::fs::remove_file(&req_path).ok();
std::fs::remove_file(&resp_path).ok();
create_fifo(&req_path, 0o600).context("Failed to create request FIFO")?;
create_fifo(&resp_path, 0o600).context("Failed to create response FIFO")?;
File::create(&lock_path).context("Failed to create lock file")?;
let req_fd = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(&req_path)
.context("Failed to open request FIFO")?;
let resp_fd = OpenOptions::new()
.read(true)
.write(true)
.open(&resp_path)
.context("Failed to open response FIFO")?;
Ok(Self { req_fd, resp_fd })
}
pub fn poll_request(&mut self, timeout_ms: i32) -> Result<Option<DaemonRequest>> {
let mut pfd = libc::pollfd {
fd: self.req_fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let n = poll_retry(&mut pfd, timeout_ms)?;
if n > 0 && (pfd.revents & libc::POLLIN) != 0 {
set_nonblocking(&self.req_fd, false)?;
let request: DaemonRequest = match read_message(&mut &self.req_fd) {
Ok(req) => req,
Err(e) => {
set_nonblocking(&self.req_fd, true)?;
return Err(e).context("Failed to read request");
}
};
set_nonblocking(&self.req_fd, true)?;
Ok(Some(request))
} else {
Ok(None)
}
}
pub fn send_response(&mut self, response: &DaemonResponse) -> Result<()> {
write_message(&mut self.resp_fd, response)
}
}
pub(in crate::lsp) 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(in crate::lsp) fn cleanup_ipc_files(dir: &Path) {
for name in ["lsp.req", "lsp.resp", "lsp.lock"] {
std::fs::remove_file(dir.join(name)).ok();
}
}
pub(in crate::lsp) fn ready_indicator(dir: &Path) -> PathBuf {
dir.join("lsp.req")
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}