use anyhow::{anyhow, Result};
use colored::Colorize;
use std::time::Duration;
use trusty_common::daemon_guard::{probe_once, spin_until_ready, DaemonGuardConfig};
const READY_TIMEOUT: Duration = Duration::from_secs(60);
async fn probe_health(base: &str) -> bool {
probe_once(&format!("{base}/health")).await
}
#[allow(dead_code)]
pub(crate) fn spawn_daemon() -> Result<u32> {
spawn_daemon_with_device(None)
}
pub(crate) fn spawn_daemon_with_device(device: Option<&str>) -> Result<u32> {
let mut args = vec!["start", "--foreground"];
let device_str;
if let Some(dev) = device {
args.push("--device");
device_str = dev.to_string();
args.push(&device_str);
}
trusty_common::daemon_guard::spawn_current_exe(&args)
.map_err(|e| anyhow!("trusty-search daemon spawn failed: {e}"))
}
pub async fn ensure_daemon_running(base: &str) -> Result<()> {
ensure_daemon_running_with_device(base, None).await
}
pub async fn ensure_daemon_running_with_device(base: &str, device: Option<&str>) -> Result<()> {
if probe_health(base).await {
return Ok(());
}
let already_running = crate::service::running_daemon_pid().is_some();
if already_running {
eprintln!(
"{} trusty-search daemon already running, waiting for it to become ready…",
"◉".cyan()
);
} else {
match device {
Some(dev) => eprintln!(
"{} Starting trusty-search daemon (--device {dev})…",
"◉".cyan()
),
None => eprintln!("{} Starting trusty-search daemon…", "◉".cyan()),
}
spawn_daemon_with_device(device)?;
}
let cfg = DaemonGuardConfig {
health_url: format!("{base}/health"),
service_name: "trusty-search".to_string(),
startup_timeout: READY_TIMEOUT,
poll_interval: Duration::from_millis(500),
timeout_hint: "try `trusty-search start` manually to see the error".to_string(),
};
spin_until_ready(&cfg).await
}
pub async fn ensure_daemon_running_or_exit(base: &str) -> Result<()> {
ensure_daemon_running(base).await
}
pub async fn ensure_daemon_running_for_indexing(base: &str) -> Result<()> {
let device = resolve_indexing_device();
let device_opt = if device.eq_ignore_ascii_case("auto") {
None
} else {
Some(device.as_str())
};
ensure_daemon_running_with_device(base, device_opt).await
}
fn resolve_indexing_device() -> String {
match std::env::var("TRUSTY_INDEX_DEVICE") {
Ok(v) if !v.is_empty() => v.to_ascii_lowercase(),
_ => "auto".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
#[tokio::test]
async fn probe_health_returns_false_on_connection_refused() {
let base = "http://127.0.0.1:65535";
let started = Instant::now();
let ok = probe_health(base).await;
assert!(!ok, "probe should fail against an unbound port");
assert!(
started.elapsed() < Duration::from_secs(6),
"probe took too long: {:?}",
started.elapsed()
);
}
#[tokio::test]
async fn probe_health_returns_false_on_bad_url() {
let ok = probe_health("not-a-valid-url").await;
assert!(!ok);
}
#[tokio::test]
async fn probe_health_respects_short_timeout() {
let started = Instant::now();
let _ = probe_health("http://127.0.0.1:1").await;
assert!(
started.elapsed() < Duration::from_secs(6),
"probe took too long: {:?}",
started.elapsed()
);
}
use std::sync::Mutex;
static INDEX_DEVICE_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn resolve_indexing_device_defaults_to_auto() {
let _guard = INDEX_DEVICE_ENV_LOCK.lock().unwrap();
let prev = std::env::var("TRUSTY_INDEX_DEVICE").ok();
unsafe { std::env::remove_var("TRUSTY_INDEX_DEVICE") };
assert_eq!(resolve_indexing_device(), "auto");
unsafe {
match prev {
Some(v) => std::env::set_var("TRUSTY_INDEX_DEVICE", v),
None => std::env::remove_var("TRUSTY_INDEX_DEVICE"),
}
}
}
#[test]
fn resolve_indexing_device_honours_env_override() {
let _guard = INDEX_DEVICE_ENV_LOCK.lock().unwrap();
let prev = std::env::var("TRUSTY_INDEX_DEVICE").ok();
unsafe { std::env::set_var("TRUSTY_INDEX_DEVICE", "GPU") };
assert_eq!(resolve_indexing_device(), "gpu");
unsafe { std::env::set_var("TRUSTY_INDEX_DEVICE", "auto") };
assert_eq!(resolve_indexing_device(), "auto");
unsafe {
match prev {
Some(v) => std::env::set_var("TRUSTY_INDEX_DEVICE", v),
None => std::env::remove_var("TRUSTY_INDEX_DEVICE"),
}
}
}
}