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(not(target_os = "macos"))]
pub(super) fn launch(socket: &Path) -> Result<()> {
use anyhow::Context;
let exe = std::env::current_exe().context("could not resolve the current executable")?;
std::process::Command::new(exe)
.arg("daemon")
.arg("run")
.arg("--socket")
.arg(socket)
.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()
)
}
}
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::*;
#[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:?}"
);
}
}