use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::net::UnixStream;
use super::daemon_socket_path;
const SPAWN_TIMEOUT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(200);
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() {
return Ok(());
}
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())
}
#[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"
))));
}
}