use std::time::Duration;
use anyhow::{anyhow, Result};
use colored::Colorize;
use trusty_common::daemon_guard::{probe_once, spin_until_ready, DaemonGuardConfig};
pub async fn probe_health(port: u16) -> bool {
probe_once(&format!("http://127.0.0.1:{port}/health")).await
}
fn spawn_daemon(port: u16) -> Result<u32> {
let port_str = port.to_string();
trusty_common::daemon_guard::spawn_current_exe(&["serve", "--port", &port_str])
.map_err(|e| anyhow!("trusty-analyze daemon spawn failed: {e}"))
}
pub async fn ensure_daemon_running(port: u16) -> Result<()> {
if probe_health(port).await {
return Ok(());
}
let already_running = super::daemon::pid_file_path()
.ok()
.and_then(|p| {
let raw = std::fs::read_to_string(&p).ok()?;
raw.trim().parse::<u32>().ok()
})
.is_some();
if already_running {
eprint!(
"{} trusty-analyze daemon already starting, waiting for it to become ready…",
"◉".cyan()
);
let _ = std::io::Write::flush(&mut std::io::stderr());
} else {
eprintln!("{} Starting trusty-analyze daemon…", "◉".cyan());
spawn_daemon(port)?;
}
let cfg = DaemonGuardConfig {
health_url: format!("http://127.0.0.1:{port}/health"),
service_name: "trusty-analyze".to_string(),
startup_timeout: Duration::from_secs(30),
poll_interval: Duration::from_millis(500),
timeout_hint: format!("try `trusty-analyze serve --port {port}` manually to see the error"),
};
spin_until_ready(&cfg).await
}
pub async fn ensure_mcp_daemon_up(analyzer_url: &str) -> anyhow::Result<String> {
use trusty_common::mcp::DaemonBridgeConfig;
let base_url = analyzer_url.to_string();
let base_url_clone = base_url.clone();
let config = DaemonBridgeConfig {
service_name: "trusty-analyze".to_string(),
spawn_args: {
let port = analyzer_url
.trim_start_matches("http://")
.trim_start_matches("https://")
.rsplit(':')
.next()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(trusty_analyze::service::DEFAULT_PORT);
vec!["serve".to_string(), "--port".to_string(), port.to_string()]
},
health_path: "/health".to_string(),
base_url_fn: Box::new(
move || match trusty_common::read_daemon_addr("trusty-analyze") {
Ok(Some(addr)) if !addr.is_empty() => {
if addr.starts_with("http://") || addr.starts_with("https://") {
addr
} else {
format!("http://{addr}")
}
}
_ => base_url_clone.clone(),
},
),
startup_timeout: None,
poll_interval: None,
no_spawn: false, };
trusty_common::mcp::ensure_daemon_up(&config).await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[tokio::test]
async fn probe_health_returns_false_on_connection_refused() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let started = Instant::now();
let ok = probe_health(port).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 ensure_daemon_running_returns_ok_when_already_healthy() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
loop {
if let Ok((mut stream, _)) = listener.accept().await {
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
let _ = stream.write_all(response).await;
});
}
}
});
tokio::time::sleep(Duration::from_millis(50)).await;
let result = ensure_daemon_running(port).await;
assert!(
result.is_ok(),
"should succeed when daemon is already healthy"
);
}
#[tokio::test]
async fn probe_health_returns_false_quickly_for_free_port() {
let started = Instant::now();
let ok = probe_health(1).await;
assert!(!ok);
assert!(
started.elapsed() < Duration::from_secs(6),
"probe should be fast: {:?}",
started.elapsed()
);
}
}