use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::proto::{self, Ack, Hello, WIRE_VERSION};
pub const SOCKET_NAME: &str = "leindexd.sock";
pub const LOCK_NAME: &str = "leindexd.lock";
const SPAWN_WAIT: Duration = Duration::from_secs(5);
const ACK_WAIT: Duration = Duration::from_secs(3);
const SPAWN_RETRY_COOLDOWN: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub enum Outcome {
Done(i32),
Fallback(String),
}
pub fn eligible_project(args: &[String]) -> Option<Option<PathBuf>> {
let mut project = None;
let mut saw_mcp = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"mcp" if !saw_mcp => saw_mcp = true,
"--stdio" => {}
"--project" | "-p" => project = Some(PathBuf::from(iter.next()?)),
other => {
let value = other.strip_prefix("--project=")?;
project = Some(PathBuf::from(value));
}
}
}
Some(project)
}
pub fn run_dir() -> Option<PathBuf> {
crate::config::resolve_leindex_home().map(|home| home.join("run"))
}
pub fn run(project: Option<PathBuf>) -> Outcome {
if !crate::feature_flags::FeatureFlag::DaemonClient.is_enabled() {
return Outcome::Fallback("daemon client disabled".into());
}
let Some(dir) = run_dir() else {
return Outcome::Fallback("no LeIndex home directory".into());
};
if let Err(error) = std::fs::create_dir_all(&dir) {
return Outcome::Fallback(format!("cannot create {}: {error}", dir.display()));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(error) = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)) {
return Outcome::Fallback(format!(
"cannot restrict {} to 0700: {error}",
dir.display()
));
}
}
let socket = dir.join(SOCKET_NAME);
let cwd = project
.or_else(|| std::env::current_dir().ok())
.and_then(|path| path.canonicalize().ok())
.filter(|path| proto::is_projectish_cwd(path))
.map(|path| path.to_string_lossy().into_owned());
let mut replaced = false;
let mut next_spawn_allowed = Instant::now();
let deadline = Instant::now() + SPAWN_WAIT + ACK_WAIT;
loop {
match connect_and_greet(&socket, cwd.as_deref()) {
Ok((stream, ack)) => {
if !ack.ok {
return Outcome::Fallback(format!(
"daemon refused this client: {}",
ack.error.unwrap_or_default()
));
}
if is_stale(&ack) {
if ack.clients > 1 || replaced {
return Outcome::Fallback(format!(
"daemon {} (pid {}) is out of date but has other clients",
ack.version, ack.pid
));
}
replace(&stream, &socket, ack.pid);
drop(stream);
replaced = true;
next_spawn_allowed = Instant::now() + SPAWN_RETRY_COOLDOWN;
continue;
}
return Outcome::Done(forward(stream));
}
Err(error) => {
let now = Instant::now();
if now >= deadline {
return Outcome::Fallback(format!("daemon unavailable: {error}"));
}
if now >= next_spawn_allowed {
match spawn_daemon(&socket) {
Ok(()) => next_spawn_allowed = Instant::now() + SPAWN_RETRY_COOLDOWN,
Err(reason) => return Outcome::Fallback(reason),
}
}
std::thread::sleep(Duration::from_millis(5));
}
}
}
}
fn connect_and_greet(socket: &Path, cwd: Option<&str>) -> Result<(UnixStream, Ack), String> {
let mut stream = UnixStream::connect(socket).map_err(|error| error.to_string())?;
stream
.set_read_timeout(Some(ACK_WAIT))
.map_err(|error| error.to_string())?;
let hello = Hello {
wire: WIRE_VERSION,
version: env!("CARGO_PKG_VERSION").to_string(),
cwd: cwd.map(str::to_string),
pid: std::process::id(),
};
stream
.write_all(proto::hello_line(&hello).as_bytes())
.map_err(|error| error.to_string())?;
let mut line = Vec::with_capacity(160);
let mut byte = [0u8; 1];
loop {
match stream.read(&mut byte) {
Ok(0) => return Err("daemon closed the connection".into()),
Ok(_) if byte[0] == b'\n' => break,
Ok(_) => {
line.push(byte[0]);
if line.len() > 4096 {
return Err("oversized ack".into());
}
}
Err(error) => return Err(error.to_string()),
}
}
let ack = proto::parse_ack(&String::from_utf8_lossy(&line))
.ok_or_else(|| "daemon sent no ack (older version?)".to_string())?;
stream
.set_read_timeout(None)
.map_err(|error| error.to_string())?;
Ok((stream, ack))
}
fn is_stale(ack: &Ack) -> bool {
if ack.wire != WIRE_VERSION || ack.version != env!("CARGO_PKG_VERSION") {
return true;
}
daemon_binary()
.and_then(|path| std::fs::metadata(path).ok())
.and_then(|meta| meta.modified().ok())
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
.is_some_and(|since_epoch| since_epoch.as_millis() as u64 > ack.started_ms)
}
fn daemon_binary() -> Option<PathBuf> {
if let Some(explicit) = std::env::var_os("LEINDEXD_BIN") {
return Some(PathBuf::from(explicit));
}
let exe = std::env::current_exe().ok()?;
let sibling = exe.parent()?.join("leindexd");
sibling.is_file().then_some(sibling)
}
fn spawn_daemon(socket: &Path) -> Result<(), String> {
use std::os::unix::process::CommandExt;
let binary = daemon_binary().ok_or_else(|| "leindexd binary not found".to_string())?;
let mut command = std::process::Command::new(&binary);
command
.arg("--socket")
.arg(socket)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
unsafe {
command.pre_exec(|| {
libc::setsid();
Ok(())
});
}
let child = command
.spawn()
.map_err(|error| format!("cannot start {}: {error}", binary.display()))?;
std::thread::spawn(move || {
let mut child = child;
let _ = child.wait();
});
Ok(())
}
fn replace(stream: &UnixStream, socket: &Path, pid: u32) {
if !pid_is_the_connected_daemon(stream, pid) {
return;
}
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
let deadline = Instant::now() + Duration::from_secs(3);
while Instant::now() < deadline {
if UnixStream::connect(socket).is_err() {
return;
}
std::thread::sleep(Duration::from_millis(10));
}
}
fn pid_is_the_connected_daemon(stream: &UnixStream, pid: u32) -> bool {
if pid == 0 || pid > i32::MAX as u32 {
return false;
}
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
let mut cred = libc::ucred {
pid: 0,
uid: 0,
gid: 0,
};
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
stream.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut cred as *mut libc::ucred as *mut libc::c_void,
&mut len,
)
};
if rc != 0 || cred.pid != pid as libc::pid_t {
return false;
}
}
#[cfg(target_os = "macos")]
{
use std::os::unix::io::AsRawFd;
let mut cred: libc::xucred = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::xucred>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
stream.as_raw_fd(),
0,
libc::LOCAL_PEERCRED,
&mut cred as *mut libc::xucred as *mut libc::c_void,
&mut len,
)
};
let euid = unsafe { libc::geteuid() };
if rc != 0 || cred.cr_uid != euid {
return false;
}
if unsafe { libc::kill(pid as libc::pid_t, 0) } != 0 {
return false;
}
}
#[cfg(target_os = "linux")]
{
exe_matches_daemon_binary(
std::fs::read_link(format!("/proc/{pid}/exe"))
.ok()
.as_deref(),
)
}
#[cfg(target_os = "macos")]
{
let mut buffer = [0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
let len = unsafe {
libc::proc_pidpath(
pid as libc::c_int,
buffer.as_mut_ptr() as *mut libc::c_void,
buffer.len() as u32,
)
};
if len <= 0 {
return false;
}
let path =
std::path::PathBuf::from(String::from_utf8_lossy(&buffer[..len as usize]).into_owned());
exe_matches_daemon_binary(Some(path.as_path()))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (stream, pid);
return false;
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn exe_matches_daemon_binary(exe: Option<&Path>) -> bool {
let Some(exe) = exe else {
return false;
};
let mut exe = exe.to_path_buf();
if exe.as_os_str().to_string_lossy().ends_with(" (deleted)") {
let stripped = exe.as_os_str().to_string_lossy();
exe = PathBuf::from(stripped.trim_end_matches(" (deleted)").to_string());
}
match daemon_binary() {
Some(expected) => {
match (
std::fs::canonicalize(&exe),
std::fs::canonicalize(&expected),
) {
(Ok(actual), Ok(expected)) => actual == expected,
_ => exe == expected,
}
}
None => exe.file_name().and_then(|name| name.to_str()) == Some("leindexd"),
}
}
fn forward(stream: UnixStream) -> i32 {
let Ok(mut upstream) = stream.try_clone() else {
return 1;
};
let mut downstream = stream;
std::thread::spawn(move || {
let mut stdin = std::io::stdin().lock();
let mut buffer = vec![0u8; 64 * 1024];
loop {
match stdin.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(read) => {
if upstream.write_all(&buffer[..read]).is_err() {
return;
}
}
}
}
let _ = upstream.shutdown(std::net::Shutdown::Write);
});
let mut stdout = std::io::stdout().lock();
let mut buffer = vec![0u8; 64 * 1024];
loop {
match downstream.read(&mut buffer) {
Ok(0) => return 0,
Ok(read) => {
if stdout.write_all(&buffer[..read]).is_err() || stdout.flush().is_err() {
return 0;
}
}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => return 1,
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
#[test]
fn test_plain_stdio_launches_are_eligible() {
assert_eq!(eligible_project(&args(&[])), Some(None));
assert_eq!(eligible_project(&args(&["mcp"])), Some(None));
assert_eq!(eligible_project(&args(&["mcp", "--stdio"])), Some(None));
assert_eq!(eligible_project(&args(&["--stdio"])), Some(None));
}
#[test]
fn test_project_flag_forms_are_carried() {
let expected = Some(Some(PathBuf::from("/w/p")));
assert_eq!(
eligible_project(&args(&["--project", "/w/p", "mcp"])),
expected
);
assert_eq!(eligible_project(&args(&["mcp", "-p", "/w/p"])), expected);
assert_eq!(
eligible_project(&args(&["--project=/w/p", "mcp"])),
expected
);
}
#[test]
fn test_other_commands_and_flags_are_not_eligible() {
assert_eq!(eligible_project(&args(&["index", "."])), None);
assert_eq!(
eligible_project(&args(&["mcp", "--socket", "/tmp/x"])),
None
);
assert_eq!(
eligible_project(&args(&["mcp", "--mcp-idle-timeout-secs", "5"])),
None
);
assert_eq!(
eligible_project(&args(&["--project"])),
None,
"flag without a value"
);
}
#[test]
fn test_stale_detection_on_version_and_wire() {
let mut ack = Ack {
wire: WIRE_VERSION,
version: env!("CARGO_PKG_VERSION").to_string(),
pid: 1,
started_ms: u64::MAX,
clients: 1,
ok: true,
error: None,
};
assert!(
!is_stale(&ack),
"same version, started after any binary mtime"
);
ack.version = "0.0.1".into();
assert!(is_stale(&ack));
ack.version = env!("CARGO_PKG_VERSION").to_string();
ack.wire = WIRE_VERSION + 1;
assert!(is_stale(&ack));
}
}