use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result};
use tracing::{debug, info, warn};
use super::endpoint::{DaemonEndpoint, ENDPOINT_SIDECAR, StartupOutcome, resolve_endpoint};
use super::handshake::DAEMON_PROTOCOL_VERSION;
const SOCKET_WAIT_TIMEOUT: Duration = Duration::from_secs(10);
const SOCKET_POLL_INTERVAL: Duration = Duration::from_millis(100);
#[doc(hidden)]
pub async fn spawn_bounded_socket_wait(socket_path: &Path, timeout: Duration) -> bool {
wait_for_socket(socket_path, timeout).await
}
pub async fn spawn_and_wait(run_dir: &Path) -> Result<DaemonEndpoint> {
let socket_path = run_dir.join("d.sock");
let leindexd_path = resolve_leindexd_path()?;
info!(
"spawning leindexd: {} --socket {} --idle-timeout-secs 300",
leindexd_path.display(),
socket_path.display()
);
let mut child = tokio::process::Command::new(&leindexd_path)
.arg("--socket")
.arg(&socket_path)
.arg("--idle-timeout-secs")
.arg("300")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.with_context(|| format!("failed to spawn leindexd at {}", leindexd_path.display()))?;
let appeared = wait_for_socket(&socket_path, SOCKET_WAIT_TIMEOUT).await;
if !appeared {
let _ = child.kill().await;
anyhow::bail!(
"leindexd did not create socket at {} within {:?}",
socket_path.display(),
SOCKET_WAIT_TIMEOUT
);
}
debug!("daemon socket appeared at {}", socket_path.display());
let endpoint = wait_for_sidecar(run_dir, Duration::from_secs(5)).await?;
std::mem::forget(child);
Ok(endpoint)
}
fn resolve_leindexd_path() -> Result<PathBuf> {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let sibling = dir.join("leindexd");
if sibling.exists() {
return Ok(sibling);
}
}
}
which::which("leindexd")
.with_context(|| "leindexd binary not found in PATH or alongside leindex")
}
async fn wait_for_socket(socket_path: &Path, timeout: Duration) -> bool {
let deadline = tokio::time::Instant::now() + timeout;
loop {
if socket_path.exists() {
return true;
}
if tokio::time::Instant::now() >= deadline {
return false;
}
tokio::time::sleep(SOCKET_POLL_INTERVAL).await;
}
}
async fn wait_for_sidecar(run_dir: &Path, timeout: Duration) -> Result<DaemonEndpoint> {
let sidecar = run_dir.join(ENDPOINT_SIDECAR);
let deadline = tokio::time::Instant::now() + timeout;
loop {
if tokio::time::Instant::now() >= deadline {
anyhow::bail!(
"endpoint sidecar {} did not appear within {:?}",
sidecar.display(),
timeout
);
}
match resolve_endpoint(run_dir, DAEMON_PROTOCOL_VERSION) {
Ok(StartupOutcome::Connect(ep)) => return Ok(ep),
Ok(StartupOutcome::Won(_)) => {
debug!("endpoint not yet published; waiting...");
}
Err(e) => {
warn!("endpoint resolution error during spawn wait: {e}");
}
}
tokio::time::sleep(SOCKET_POLL_INTERVAL).await;
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::cli::daemon::endpoint::{DaemonEndpoint, write_endpoint_sidecar};
use std::fs;
use std::os::unix::net::UnixListener;
#[test]
fn test_resolve_leindexd_path_does_not_panic() {
let _ = resolve_leindexd_path();
}
#[tokio::test]
async fn test_wait_for_socket_returns_true_when_present() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("test.sock");
fs::write(&sock, b"").unwrap();
let result = wait_for_socket(&sock, Duration::from_secs(1)).await;
assert!(result, "wait_for_socket must return true for existing file");
}
#[tokio::test]
async fn test_wait_for_socket_times_out() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("nonexistent.sock");
let start = tokio::time::Instant::now();
let result = wait_for_socket(&sock, Duration::from_millis(300)).await;
let elapsed = start.elapsed();
assert!(!result, "wait_for_socket must return false on timeout");
assert!(
elapsed >= Duration::from_millis(200),
"must wait at least ~300ms before timing out, got {elapsed:?}"
);
assert!(
elapsed < Duration::from_secs(2),
"must NOT wait unreasonably long, got {elapsed:?}"
);
}
#[tokio::test]
async fn test_wait_for_socket_detects_appearing_file() {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("late.sock");
let sock_clone = sock.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
fs::write(&sock_clone, b"").unwrap();
});
let result = wait_for_socket(&sock, Duration::from_secs(5)).await;
assert!(
result,
"wait_for_socket must detect the late-appearing file"
);
}
#[tokio::test]
async fn test_wait_for_sidecar_reads_published_endpoint() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path();
let ep = DaemonEndpoint::current_process(run_dir.join("d.sock"), DAEMON_PROTOCOL_VERSION);
write_endpoint_sidecar(run_dir, &ep).unwrap();
let result = wait_for_sidecar(run_dir, Duration::from_secs(5)).await;
assert!(
result.is_ok(),
"wait_for_sidecar must succeed: {:?}",
result
);
let resolved = result.unwrap();
assert_eq!(resolved.pid, ep.pid);
assert_eq!(resolved.protocol_version, DAEMON_PROTOCOL_VERSION);
}
#[tokio::test]
async fn test_wait_for_sidecar_times_out() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path();
let result = wait_for_sidecar(run_dir, Duration::from_millis(300)).await;
assert!(
result.is_err(),
"wait_for_sidecar must time out when sidecar never appears"
);
}
#[tokio::test]
async fn test_wait_for_sidecar_detects_late_endpoint() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path().to_path_buf();
let run_dir_clone = run_dir.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
let ep = DaemonEndpoint::current_process(
run_dir_clone.join("d.sock"),
DAEMON_PROTOCOL_VERSION,
);
write_endpoint_sidecar(&run_dir_clone, &ep).unwrap();
});
let result = wait_for_sidecar(&run_dir, Duration::from_secs(5)).await;
assert!(result.is_ok(), "wait_for_sidecar must detect late endpoint");
}
#[tokio::test]
async fn test_spawn_and_wait_waits_for_socket_and_sidecar() {
let dir = tempfile::tempdir().unwrap();
let run_dir = dir.path().to_path_buf();
let socket_path = run_dir.join("d.sock");
let sock_clone = socket_path.clone();
let run_dir_clone = run_dir.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
let _listener = UnixListener::bind(&sock_clone);
let ep = DaemonEndpoint::current_process(sock_clone.clone(), DAEMON_PROTOCOL_VERSION);
write_endpoint_sidecar(&run_dir_clone, &ep).unwrap();
tokio::time::sleep(Duration::from_secs(5)).await;
});
let appeared = wait_for_socket(&socket_path, Duration::from_secs(5)).await;
assert!(appeared, "socket must appear within timeout");
let endpoint = wait_for_sidecar(&run_dir, Duration::from_secs(5))
.await
.expect("sidecar must appear");
assert_eq!(endpoint.protocol_version, DAEMON_PROTOCOL_VERSION);
assert_eq!(endpoint.socket_path, socket_path);
}
}