use std::path::Path;
use std::time::{Duration, Instant};
use anyhow::Result;
use tokio::time::timeout;
use crate::daemon::client::DaemonClient;
const PING_TIMEOUT: Duration = Duration::from_millis(250);
const POLL_INTERVAL: Duration = Duration::from_millis(100);
const POLL_BUDGET: Duration = Duration::from_secs(5);
#[cfg(target_os = "macos")]
pub(super) fn launch(socket: &Path) -> Result<()> {
crate::daemon::launchd::install_and_load(socket)
}
#[cfg(target_os = "linux")]
pub(super) fn launch(socket: &Path) -> Result<()> {
use crate::daemon::systemd;
if systemd::is_available() {
match systemd::install_and_load(socket) {
Ok(()) => return Ok(()),
Err(e) => tracing::warn!(
"systemd auto-start unavailable ({e}); falling back to a detached spawn"
),
}
}
spawn_detached(socket)
}
#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
pub(super) fn launch(socket: &Path) -> Result<()> {
spawn_detached(socket)
}
#[cfg(not(target_os = "macos"))]
fn spawn_detached(socket: &Path) -> Result<()> {
use std::os::unix::process::CommandExt;
use std::process::Stdio;
use anyhow::Context;
use crate::daemon::paths;
let exe = std::env::current_exe().context("could not resolve the current executable")?;
if let Some(dir) = socket.parent() {
paths::ensure_dir_0700(dir)?;
}
let log_path = paths::log_path_for_socket(socket);
let log = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(&log_path)
.with_context(|| format!("failed to open daemon log {}", log_path.display()))?;
paths::ensure_handle_0600(&log)
.with_context(|| format!("failed to set 0600 on {}", log_path.display()))?;
let log_err = log
.try_clone()
.with_context(|| format!("failed to clone handle for {}", log_path.display()))?;
let mut command = std::process::Command::new(exe);
command
.arg("daemon")
.arg("run")
.arg("--socket")
.arg(socket)
.stdin(Stdio::null())
.stdout(log)
.stderr(log_err);
#[allow(unsafe_code)]
unsafe {
command.pre_exec(|| {
if nix::libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
command
.spawn()
.context("failed to spawn the daemon process")?;
Ok(())
}
async fn pings_alive(client: &DaemonClient) -> bool {
matches!(timeout(PING_TIMEOUT, client.ping()).await, Ok(Ok(())))
}
async fn poll_until(socket: &Path, want_alive: bool, budget: Duration) -> bool {
let client = DaemonClient::new(socket);
let deadline = Instant::now() + budget;
loop {
if pings_alive(&client).await == want_alive {
return true;
}
if Instant::now() >= deadline {
return false;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
pub(super) async fn wait_until_ready(socket: &Path) -> Result<()> {
if poll_until(socket, true, POLL_BUDGET).await {
Ok(())
} else {
anyhow::bail!(
"daemon did not become ready within 5s (socket {})",
socket.display()
)
}
}
#[cfg(not(target_os = "linux"))]
pub(super) async fn wait_until_down(socket: &Path) -> Result<()> {
if poll_until(socket, false, POLL_BUDGET).await {
Ok(())
} else {
anyhow::bail!(
"daemon did not stop within 5s (socket {})",
socket.display()
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn wait_until_down_returns_when_no_daemon() {
let dir = tempfile::tempdir().unwrap();
wait_until_down(&dir.path().join("absent.sock"))
.await
.unwrap();
}
#[tokio::test]
async fn poll_until_alive_honours_its_budget() {
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("never.sock");
let budget = Duration::from_millis(150);
let start = Instant::now();
let reached = poll_until(&socket, true, budget).await;
let elapsed = start.elapsed();
assert!(!reached, "no daemon should ever be reported alive");
assert!(
elapsed >= budget && elapsed < Duration::from_secs(2),
"poll should end near the {budget:?} budget, took {elapsed:?}"
);
}
}