use std::time::Duration;
pub fn daemon_base_url() -> String {
if let Some(addr) = read_http_addr_file() {
if address_reachable_blocking(&addr) {
return format!("http://{addr}");
}
}
let port = daemon_port_path()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| s.trim().parse::<u16>().ok())
.unwrap_or(trusty_search::service::DEFAULT_PORT);
let live_addr = format!("127.0.0.1:{port}");
if address_reachable_blocking(&live_addr) {
let _ = refresh_http_addr_file(&live_addr);
}
format!("http://{live_addr}")
}
fn address_reachable_blocking(host_port: &str) -> bool {
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
let Ok(mut iter) = host_port.to_socket_addrs() else {
return false;
};
let Some(addr): Option<SocketAddr> = iter.next() else {
return false;
};
TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok()
}
fn refresh_http_addr_file(host_port: &str) -> std::io::Result<()> {
use std::io::Write;
let Some(path) = http_addr_path() else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no $HOME",
));
};
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("addr.tmp");
{
let mut f = std::fs::File::create(&tmp)?;
writeln!(f, "{host_port}")?;
f.sync_all()?;
}
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn read_http_addr_file() -> Option<String> {
let path = http_addr_path()?;
let raw = std::fs::read_to_string(&path).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub fn http_addr_path() -> Option<std::path::PathBuf> {
dirs::home_dir().map(|h| h.join(".trusty-search").join("http_addr"))
}
pub fn mcp_http_addr_path() -> Option<std::path::PathBuf> {
dirs::home_dir().map(|h| h.join(".trusty-search").join("mcp_http_addr"))
}
pub fn daemon_port_path() -> Option<std::path::PathBuf> {
dirs::data_local_dir().map(|d| d.join("trusty-search").join("daemon.port"))
}
pub async fn port_reachable(host: &str, port: u16) -> bool {
let addr = format!("{}:{}", host, port);
tokio::time::timeout(
Duration::from_millis(500),
tokio::net::TcpStream::connect(&addr),
)
.await
.ok()
.and_then(|r| r.ok())
.is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn address_reachable_returns_false_for_dead_port() {
let start = std::time::Instant::now();
assert!(!address_reachable_blocking("127.0.0.1:1"));
assert!(
start.elapsed() < Duration::from_millis(1500),
"probe took too long: {:?}",
start.elapsed()
);
}
#[test]
fn address_reachable_returns_false_for_garbage_input() {
assert!(!address_reachable_blocking("not-a-host:port"));
assert!(!address_reachable_blocking(""));
assert!(!address_reachable_blocking("127.0.0.1"));
}
#[test]
fn address_reachable_returns_true_for_live_listener() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
assert!(address_reachable_blocking(&addr.to_string()));
}
#[test]
fn http_addr_and_mcp_http_addr_paths_are_distinct() {
let http = http_addr_path();
let mcp = mcp_http_addr_path();
if let (Some(h), Some(m)) = (http, mcp) {
assert_ne!(h, m);
assert!(h.ends_with("http_addr"));
assert!(m.ends_with("mcp_http_addr"));
}
}
}