use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::net::UnixStream;
use super::client::connect_at;
use super::daemon_socket_path;
use super::ipc::{DaemonRequest, DaemonRequestPayload, DaemonResponse};
const SPAWN_TIMEOUT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(200);
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
pub async fn ensure_daemon_running() -> anyhow::Result<()> {
if has_explicit_socket_override(std::env::var_os("HALLOUMINATE_SOCKET").as_deref()) {
return Ok(());
}
let socket = daemon_socket_path();
if UnixStream::connect(&socket).await.is_ok() {
if running_daemon_version_matches(&socket).await {
return Ok(());
}
tracing::info!(
target: "hallouminate::daemon",
ours = env!("CARGO_PKG_VERSION"),
"running daemon version mismatch or unverifiable; restarting it",
);
super::lifecycle::stop().await?;
}
let log_path = crate::app::xdg::xdg_path(
"XDG_STATE_HOME",
"~/.local/state",
&["hallouminate", "daemon-bootstrap.log"],
);
if let Some(dir) = log_path.parent() {
std::fs::create_dir_all(dir)?;
}
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)?;
let log_err = log.try_clone()?;
let exe = std::env::current_exe()?;
Command::new(&exe)
.arg("daemon")
.stdin(Stdio::null())
.stdout(Stdio::from(log))
.stderr(Stdio::from(log_err))
.process_group(0)
.spawn()?;
let deadline = std::time::Instant::now() + SPAWN_TIMEOUT;
while std::time::Instant::now() < deadline {
if UnixStream::connect(&socket).await.is_ok() {
return Ok(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
anyhow::bail!(
"daemon did not start within {}s; see {}",
SPAWN_TIMEOUT.as_secs(),
log_path.display()
)
}
fn has_explicit_socket_override(env_value: Option<&std::ffi::OsStr>) -> bool {
env_value.is_some_and(|v| !v.is_empty())
}
async fn running_daemon_version_matches(socket: &Path) -> bool {
let client = match connect_at(socket).await {
Ok(c) => c,
Err(e) => {
tracing::debug!(
target: "hallouminate::daemon",
error = %e,
"version probe could not connect to the running daemon; treating as unverifiable",
);
return false;
}
};
let probe = client.call_raw(DaemonRequest {
cwd: std::env::current_dir().unwrap_or_default(),
payload: DaemonRequestPayload::Ping,
});
match tokio::time::timeout(PROBE_TIMEOUT, probe).await {
Ok(Ok(resp)) => pong_reports_our_version(&resp),
Ok(Err(e)) => {
tracing::debug!(
target: "hallouminate::daemon",
error = %e,
"version probe Ping failed; treating the daemon as unverifiable",
);
false
}
Err(_elapsed) => {
tracing::debug!(
target: "hallouminate::daemon",
timeout_secs = PROBE_TIMEOUT.as_secs(),
"version probe Ping timed out; treating the daemon as unverifiable",
);
false
}
}
}
fn pong_reports_our_version(resp: &DaemonResponse) -> bool {
matches!(
resp,
DaemonResponse::Ok { result }
if result.get("version").and_then(|v| v.as_str()) == Some(env!("CARGO_PKG_VERSION"))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unset_socket_env_does_not_bypass_spawn() {
assert!(!has_explicit_socket_override(None));
}
#[test]
fn empty_socket_env_does_not_bypass_spawn() {
assert!(!has_explicit_socket_override(Some(std::ffi::OsStr::new(
""
))));
}
#[test]
fn set_socket_env_bypasses_spawn() {
assert!(has_explicit_socket_override(Some(std::ffi::OsStr::new(
"/tmp/test.sock"
))));
}
#[test]
fn pong_with_our_version_is_a_match() {
let resp = DaemonResponse::ok(&super::super::ipc::PongResult {
version: env!("CARGO_PKG_VERSION").to_string(),
});
assert!(pong_reports_our_version(&resp), "same version must match");
}
#[test]
fn pong_with_a_different_version_is_a_mismatch() {
let resp = DaemonResponse::ok(&super::super::ipc::PongResult {
version: "0.0.0-stale".to_string(),
});
assert!(
!pong_reports_our_version(&resp),
"a different version must not match"
);
}
#[test]
fn bare_pong_string_from_old_daemon_is_a_mismatch() {
let resp = DaemonResponse::ok(&"pong");
assert!(
!pong_reports_our_version(&resp),
"legacy bare-pong daemon must be treated as skew"
);
}
#[test]
fn error_response_is_a_mismatch() {
let resp = DaemonResponse::internal("boom");
assert!(
!pong_reports_our_version(&resp),
"an error response is unverifiable → mismatch"
);
}
}