use std::path::Path;
use std::time::Duration;
pub const DAEMON_CHECK_TIMEOUT: Duration = Duration::from_secs(2);
pub async fn check_daemon_reachable(data_dir: &str) -> bool {
let pid_file = Path::new(data_dir).join("daemon.pid");
if !pid_file.exists() {
return false;
}
let raw = match tokio::fs::read_to_string(&pid_file).await {
Ok(s) => s,
Err(_) => return false,
};
match raw.trim().parse::<u32>() {
Ok(pid) => is_process_alive(pid),
Err(_) => false,
}
}
fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let output = std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.output();
match output {
Ok(out) => out.status.success(),
Err(_) => false,
}
}
#[cfg(windows)]
{
let output = std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid), "/NH"])
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
stdout.contains(&pid.to_string())
}
Err(_) => false,
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
false
}
}
pub fn offline_error_message(data_dir: &str) -> String {
format!(
"IPFRS daemon is not running.\n\
Start it with: ipfrs daemon start --data-dir {data_dir}\n\
Or run foreground: ipfrs daemon run --data-dir {data_dir}\n\
Check status with: ipfrs daemon status"
)
}
pub async fn with_network_timeout<F, T>(fut: F, timeout: Duration) -> Option<T>
where
F: std::future::Future<Output = T>,
{
tokio::time::timeout(timeout, fut).await.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use std::env::temp_dir;
#[tokio::test]
async fn test_missing_data_dir_returns_false() {
let result = check_daemon_reachable("/nonexistent/ipfrs/data/dir").await;
assert!(!result, "should be false when data dir does not exist");
}
#[tokio::test]
async fn test_missing_pid_file_returns_false() {
let dir = temp_dir().join("ipfrs_test_connectivity_no_pid");
let _ = std::fs::create_dir_all(&dir);
let result = check_daemon_reachable(&dir.to_string_lossy()).await;
assert!(!result, "should be false when pid file is absent");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_stale_pid_returns_false() {
let dir = temp_dir().join("ipfrs_test_connectivity_stale");
let _ = std::fs::create_dir_all(&dir);
let pid_path = dir.join("daemon.pid");
std::fs::write(&pid_path, "99999999").expect("write pid file");
let result = check_daemon_reachable(&dir.to_string_lossy()).await;
assert!(!result, "should be false for a stale PID");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_with_network_timeout_completes() {
let result = with_network_timeout(async { 7_u32 }, Duration::from_secs(1)).await;
assert_eq!(result, Some(7));
}
#[tokio::test]
async fn test_with_network_timeout_expires() {
let result = with_network_timeout(
async {
tokio::time::sleep(Duration::from_secs(10)).await;
42_u32
},
Duration::from_millis(50),
)
.await;
assert!(result.is_none(), "should time out");
}
#[test]
fn test_offline_error_message_contains_data_dir() {
let msg = offline_error_message("/tmp/test-ipfrs");
assert!(
msg.contains("daemon is not running"),
"should mention daemon not running"
);
assert!(
msg.contains("/tmp/test-ipfrs"),
"should include the data_dir path"
);
}
#[test]
fn test_offline_error_message_contains_start_command() {
let msg = offline_error_message(".ipfrs");
assert!(
msg.contains("daemon start"),
"should suggest 'daemon start'"
);
assert!(msg.contains("daemon run"), "should suggest 'daemon run'");
}
}