use anyhow::{Context, Result};
use std::path::Path;
const DAEMON_ADDR_FILENAME: &str = "http_addr";
pub fn write_daemon_addr(app_name: &str, addr: &str) -> Result<()> {
let dir = crate::data_dir::resolve_data_dir(app_name)?;
let path = dir.join(DAEMON_ADDR_FILENAME);
std::fs::write(&path, addr).with_context(|| format!("write daemon addr to {}", path.display()))
}
pub fn read_daemon_addr(app_name: &str) -> Result<Option<String>> {
let dir = crate::data_dir::resolve_data_dir(app_name)?;
let path = dir.join(DAEMON_ADDR_FILENAME);
match std::fs::read_to_string(&path) {
Ok(s) => Ok(Some(s.trim().to_string())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(anyhow::Error::new(e))
.with_context(|| format!("read daemon addr from {}", path.display())),
}
}
pub fn remove_daemon_addr(app_name: &str) -> Result<()> {
let dir = crate::data_dir::resolve_data_dir(app_name)?;
let path = dir.join(DAEMON_ADDR_FILENAME);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(anyhow::Error::new(e))
.with_context(|| format!("remove daemon addr at {}", path.display())),
}
}
pub async fn check_already_running(addr_file: &Path, health_path: &str) -> Option<String> {
let raw = match std::fs::read_to_string(addr_file) {
Ok(s) => s,
Err(_) => return None,
};
let addr = raw.trim();
if addr.is_empty() {
let _ = std::fs::remove_file(addr_file);
return None;
}
let url = format!("http://{addr}");
if crate::health_probe::probe_health(&url, health_path).await {
Some(url)
} else {
let _ = std::fs::remove_file(addr_file);
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_dir::{DATA_DIR_OVERRIDE_ENV, ENV_LOCK};
use std::path::PathBuf;
fn tempfile_like_dir() -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let p = std::env::temp_dir().join(format!("trusty-common-test-{pid}-{nanos}"));
std::fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn daemon_addr_round_trips() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile_like_dir();
unsafe {
std::env::set_var(DATA_DIR_OVERRIDE_ENV, &tmp);
}
let app = format!(
"trusty-test-daemon-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
write_daemon_addr(&app, "127.0.0.1:12345").unwrap();
let got = read_daemon_addr(&app).unwrap();
unsafe {
std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
}
assert_eq!(got.as_deref(), Some("127.0.0.1:12345"));
}
#[test]
fn read_daemon_addr_missing_returns_none() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile_like_dir();
unsafe {
std::env::set_var(DATA_DIR_OVERRIDE_ENV, &tmp);
}
let app = format!(
"trusty-test-daemon-missing-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let got = read_daemon_addr(&app).unwrap();
unsafe {
std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
}
assert!(got.is_none(), "expected None when file absent, got {got:?}");
}
#[tokio::test]
async fn check_already_running_returns_none_when_file_missing() {
let tmp = tempfile_like_dir();
let missing = tmp.join("does-not-exist");
let got = check_already_running(&missing, "/health").await;
assert!(got.is_none());
}
#[tokio::test]
async fn check_already_running_returns_none_when_file_empty() {
let tmp = tempfile_like_dir();
let path = tmp.join("http_addr");
std::fs::write(&path, " \n ").unwrap();
let got = check_already_running(&path, "/health").await;
assert!(got.is_none());
assert!(
!path.exists(),
"empty address file should be cleaned up by check_already_running"
);
}
#[tokio::test]
async fn check_already_running_returns_none_when_address_dead() {
let tmp = tempfile_like_dir();
let path = tmp.join("http_addr");
std::fs::write(&path, "127.0.0.1:1\n").unwrap();
let got = check_already_running(&path, "/health").await;
assert!(got.is_none(), "dead address should map to None");
assert!(
!path.exists(),
"stale address file should be cleaned up by check_already_running"
);
}
#[tokio::test]
async fn check_already_running_returns_url_when_health_ok() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let local = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
if let Ok((mut sock, _)) = listener.accept().await {
let mut buf = [0u8; 1024];
let _ = sock.read(&mut buf).await;
let _ = sock
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
.await;
let _ = sock.shutdown().await;
}
});
let tmp = tempfile_like_dir();
let path = tmp.join("http_addr");
std::fs::write(&path, format!("{local}\n")).unwrap();
let got = check_already_running(&path, "/health").await;
assert_eq!(got.as_deref(), Some(format!("http://{local}").as_str()));
assert!(
path.exists(),
"address file must be preserved when the daemon is healthy"
);
let _ = server.await;
}
#[test]
fn daemon_addr_remove_cleans_up() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile_like_dir();
unsafe {
std::env::set_var(DATA_DIR_OVERRIDE_ENV, &tmp);
}
let app = format!(
"trusty-test-daemon-remove-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
write_daemon_addr(&app, "127.0.0.1:12345").unwrap();
remove_daemon_addr(&app).unwrap();
let got = read_daemon_addr(&app).unwrap();
unsafe {
std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
}
assert!(
got.is_none(),
"addr file should be gone after remove, got {got:?}"
);
}
#[test]
fn daemon_addr_remove_nonexistent_ok() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile_like_dir();
unsafe {
std::env::set_var(DATA_DIR_OVERRIDE_ENV, &tmp);
}
let app = format!(
"trusty-test-daemon-remove-never-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
);
let result = remove_daemon_addr(&app);
unsafe {
std::env::remove_var(DATA_DIR_OVERRIDE_ENV);
}
assert!(result.is_ok(), "removing non-existent addr must succeed");
}
}