use anyhow::{anyhow, Result};
use colored::Colorize;
use std::io::Write;
use std::time::{Duration, Instant};
const READY_TIMEOUT: Duration = Duration::from_secs(60);
const POLL_INTERVAL: Duration = Duration::from_millis(500);
const PROBE_TIMEOUT: Duration = Duration::from_millis(750);
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
async fn probe_health(base: &str) -> bool {
let client = match reqwest::Client::builder()
.timeout(PROBE_TIMEOUT)
.connect_timeout(PROBE_TIMEOUT)
.build()
{
Ok(c) => c,
Err(_) => return false,
};
match client.get(format!("{}/health", base)).send().await {
Ok(r) => r.status().is_success(),
Err(_) => false,
}
}
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 exe = std::env::current_exe().map_err(|e| anyhow!("could not resolve current_exe: {e}"))?;
let mut cmd = std::process::Command::new(&exe);
cmd.arg("start").arg("--foreground");
if let Some(dev) = device {
cmd.arg("--device").arg(dev);
}
let child = cmd
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| {
anyhow!(
"could not spawn `{} start --foreground`: {e}",
exe.display()
)
})?;
Ok(child.id())
}
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 deadline = Instant::now() + READY_TIMEOUT;
let start = Instant::now();
let mut frame = 0usize;
loop {
let elapsed = start.elapsed().as_secs();
let glyph = SPINNER_FRAMES[frame % SPINNER_FRAMES.len()];
eprint!(
"\r{} Waiting for daemon to become ready… ({}s) ",
glyph.cyan(),
elapsed
);
let _ = std::io::stderr().flush();
frame = frame.wrapping_add(1);
tokio::time::sleep(POLL_INTERVAL).await;
if probe_health(base).await {
eprint!("\r\x1b[2K");
let _ = std::io::stderr().flush();
eprintln!(
"{} Daemon ready ({}s)",
"✓".green(),
start.elapsed().as_secs()
);
return Ok(());
}
if Instant::now() >= deadline {
eprint!("\r\x1b[2K");
let _ = std::io::stderr().flush();
return Err(anyhow!(
"daemon did not become ready within {}s at {} — \
try `trusty-search start` manually to see the error",
READY_TIMEOUT.as_secs(),
base
));
}
}
}
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::*;
#[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(3),
"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(2));
}
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"),
}
}
}
}