use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const ENDPOINT_SIDECAR: &str = "daemon.endpoint";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DaemonEndpoint {
pub socket_path: PathBuf,
pub pid: u32,
pub pid_start_time_ms: u64,
pub protocol_version: u32,
pub leindex_version: String,
}
#[derive(Debug)]
pub enum StartupOutcome {
Won(DaemonEndpoint),
Connect(DaemonEndpoint),
}
pub fn resolve_endpoint(run_dir: &Path, protocol_version: u32) -> io::Result<StartupOutcome> {
let sidecar = run_dir.join(ENDPOINT_SIDECAR);
match read_sidecar(&sidecar)? {
Some(endpoint) => {
if endpoint.protocol_version != protocol_version {
steal_sidecar(&sidecar)?;
Ok(StartupOutcome::Won(make_winning_endpoint(
run_dir,
protocol_version,
)))
} else if pid_is_live(&endpoint) {
Ok(StartupOutcome::Connect(endpoint))
} else {
steal_sidecar(&sidecar)?;
Ok(StartupOutcome::Won(make_winning_endpoint(
run_dir,
protocol_version,
)))
}
}
None => Ok(StartupOutcome::Won(make_winning_endpoint(
run_dir,
protocol_version,
))),
}
}
fn make_winning_endpoint(run_dir: &Path, protocol_version: u32) -> DaemonEndpoint {
let pid = std::process::id();
DaemonEndpoint {
socket_path: run_dir.join("d.sock"),
pid,
pid_start_time_ms: proc_start_time_ms(pid).unwrap_or_else(fallback_start_time_ms),
protocol_version,
leindex_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
impl DaemonEndpoint {
pub fn current_process(socket_path: PathBuf, protocol_version: u32) -> Self {
let pid = std::process::id();
DaemonEndpoint {
socket_path,
pid,
pid_start_time_ms: proc_start_time_ms(pid).unwrap_or_else(fallback_start_time_ms),
protocol_version,
leindex_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
pub fn write_endpoint_sidecar(run_dir: &Path, endpoint: &DaemonEndpoint) -> io::Result<()> {
let sidecar = run_dir.join(ENDPOINT_SIDECAR);
let partial = run_dir.join(format!("{ENDPOINT_SIDECAR}.partial"));
std::fs::create_dir_all(run_dir)?;
let bytes = serde_json::to_vec(endpoint)?;
std::fs::write(&partial, &bytes)?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let f = std::fs::File::open(&partial)?;
unsafe {
libc::fsync(f.as_raw_fd());
}
}
std::fs::rename(&partial, &sidecar)?;
Ok(())
}
pub fn read_sidecar(path: &Path) -> io::Result<Option<DaemonEndpoint>> {
match std::fs::read(path) {
Ok(bytes) => match serde_json::from_slice::<DaemonEndpoint>(&bytes) {
Ok(ep) => Ok(Some(ep)),
Err(_) => Ok(None),
},
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn steal_sidecar(path: &Path) -> io::Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
fn pid_is_live(endpoint: &DaemonEndpoint) -> bool {
let actual = proc_start_time_ms(endpoint.pid);
match actual {
Some(ms) => ms == endpoint.pid_start_time_ms,
None => pid_exists(endpoint.pid),
}
}
#[cfg(target_os = "linux")]
fn proc_start_time_ms(pid: u32) -> Option<u64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let fields = stat.rsplit_once(") ")?.1;
let ticks = fields.split_whitespace().nth(19)?.parse::<u64>().ok()?;
Some(ticks * 10)
}
#[cfg(not(target_os = "linux"))]
fn proc_start_time_ms(_pid: u32) -> Option<u64> {
None
}
fn fallback_start_time_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(all(unix, not(target_os = "linux")))]
fn pid_exists(pid: u32) -> bool {
let rc = unsafe { libc::kill(pid as i32, 0) };
rc == 0 || io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(target_os = "windows")]
fn pid_exists(pid: u32) -> bool {
#[link(name = "kernel32")]
unsafe extern "system" {
fn OpenProcess(
desired_access: u32,
inherit_handle: i32,
process_id: u32,
) -> *mut core::ffi::c_void;
fn CloseHandle(handle: *mut core::ffi::c_void) -> i32;
}
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
if pid == 0 {
return false;
}
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if handle.is_null() {
return false;
}
unsafe { CloseHandle(handle) };
true
}
#[cfg(not(any(target_os = "linux", unix, target_os = "windows")))]
fn pid_exists(_pid: u32) -> bool {
false
}
#[cfg(target_os = "linux")]
fn pid_exists(_pid: u32) -> bool {
false
}
#[cfg(test)]
mod test {
use super::*;
use std::fs;
#[test]
fn test_first_caller_wins_second_connects() {
let dir = tempfile::tempdir().unwrap();
let r1 = resolve_endpoint(dir.path(), 1).unwrap();
let ep1 = match r1 {
StartupOutcome::Won(ep) => ep,
_ => panic!("expected Won on first call"),
};
write_sidecar_raw(dir.path(), &ep1).unwrap();
let r2 = resolve_endpoint(dir.path(), 1).unwrap();
assert!(
matches!(r2, StartupOutcome::Connect(_)),
"second caller must Connect to the live daemon"
);
}
#[test]
fn test_dead_pid_is_stolen() {
let dir = tempfile::tempdir().unwrap();
let ep = DaemonEndpoint {
socket_path: dir.path().join("d.sock"),
pid: 999_999, pid_start_time_ms: 0,
protocol_version: 1,
leindex_version: "test".into(),
};
write_sidecar_raw(dir.path(), &ep).unwrap();
let r = resolve_endpoint(dir.path(), 1).unwrap();
assert!(
matches!(r, StartupOutcome::Won(_)),
"dead-pid sidecar must be stolen"
);
}
#[test]
fn test_protocol_version_mismatch_is_stolen() {
let dir = tempfile::tempdir().unwrap();
let ep = DaemonEndpoint {
socket_path: dir.path().join("d.sock"),
pid: 999_999,
pid_start_time_ms: 0,
protocol_version: 1,
leindex_version: "test".into(),
};
write_sidecar_raw(dir.path(), &ep).unwrap();
let r = resolve_endpoint(dir.path(), 2).unwrap();
assert!(matches!(r, StartupOutcome::Won(_)));
}
#[test]
fn test_live_current_pid_is_connected() {
let dir = tempfile::tempdir().unwrap();
let ep = DaemonEndpoint::current_process(dir.path().join("d.sock"), 1);
write_sidecar_raw(dir.path(), &ep).unwrap();
let r = resolve_endpoint(dir.path(), 1).unwrap();
match r {
StartupOutcome::Connect(..) => {}
other => panic!(
"live current-pid sidecar must Connect, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn test_absent_sidecar_wins() {
let dir = tempfile::tempdir().unwrap();
let r = resolve_endpoint(dir.path(), 1).unwrap();
assert!(matches!(r, StartupOutcome::Won(_)));
}
#[test]
fn test_malformed_sidecar_recovers() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join(ENDPOINT_SIDECAR), b"{not valid json").unwrap();
let r = resolve_endpoint(dir.path(), 1).unwrap();
assert!(matches!(r, StartupOutcome::Won(_)));
}
fn write_sidecar_raw(dir: &Path, ep: &DaemonEndpoint) -> io::Result<()> {
fs::write(dir.join(ENDPOINT_SIDECAR), serde_json::to_vec(ep)?)
}
#[test]
fn test_write_endpoint_sidecar_atomic_write() {
let dir = tempfile::tempdir().unwrap();
let ep = DaemonEndpoint::current_process(dir.path().join("d.sock"), 1);
write_endpoint_sidecar(dir.path(), &ep).unwrap();
assert!(dir.path().join(ENDPOINT_SIDECAR).exists());
assert!(
!dir.path()
.join(format!("{ENDPOINT_SIDECAR}.partial"))
.exists()
);
let r = resolve_endpoint(dir.path(), 1).unwrap();
assert!(matches!(r, StartupOutcome::Connect(_)));
}
}