ipfrs_cli/
connectivity.rs1use std::path::Path;
15use std::time::Duration;
16
17pub const DAEMON_CHECK_TIMEOUT: Duration = Duration::from_secs(2);
19
20pub async fn check_daemon_reachable(data_dir: &str) -> bool {
33 let pid_file = Path::new(data_dir).join("daemon.pid");
34
35 if !pid_file.exists() {
36 return false;
37 }
38
39 let raw = match tokio::fs::read_to_string(&pid_file).await {
41 Ok(s) => s,
42 Err(_) => return false,
43 };
44
45 match raw.trim().parse::<u32>() {
46 Ok(pid) => is_process_alive(pid),
47 Err(_) => false,
48 }
49}
50
51fn is_process_alive(pid: u32) -> bool {
57 #[cfg(unix)]
58 {
59 let output = std::process::Command::new("kill")
62 .args(["-0", &pid.to_string()])
63 .output();
64
65 match output {
66 Ok(out) => out.status.success(),
67 Err(_) => false,
68 }
69 }
70
71 #[cfg(windows)]
72 {
73 let output = std::process::Command::new("tasklist")
75 .args(["/FI", &format!("PID eq {}", pid), "/NH"])
76 .output();
77
78 match output {
79 Ok(out) => {
80 let stdout = String::from_utf8_lossy(&out.stdout);
81 stdout.contains(&pid.to_string())
82 }
83 Err(_) => false,
84 }
85 }
86
87 #[cfg(not(any(unix, windows)))]
88 {
89 let _ = pid;
90 false
91 }
92}
93
94pub fn offline_error_message(data_dir: &str) -> String {
101 format!(
102 "IPFRS daemon is not running.\n\
103 Start it with: ipfrs daemon start --data-dir {data_dir}\n\
104 Or run foreground: ipfrs daemon run --data-dir {data_dir}\n\
105 Check status with: ipfrs daemon status"
106 )
107}
108
109pub async fn with_network_timeout<F, T>(fut: F, timeout: Duration) -> Option<T>
128where
129 F: std::future::Future<Output = T>,
130{
131 tokio::time::timeout(timeout, fut).await.ok()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use std::env::temp_dir;
138
139 #[tokio::test]
140 async fn test_missing_data_dir_returns_false() {
141 let result = check_daemon_reachable("/nonexistent/ipfrs/data/dir").await;
142 assert!(!result, "should be false when data dir does not exist");
143 }
144
145 #[tokio::test]
146 async fn test_missing_pid_file_returns_false() {
147 let dir = temp_dir().join("ipfrs_test_connectivity_no_pid");
149 let _ = std::fs::create_dir_all(&dir);
150 let result = check_daemon_reachable(&dir.to_string_lossy()).await;
151 assert!(!result, "should be false when pid file is absent");
152 let _ = std::fs::remove_dir_all(&dir);
153 }
154
155 #[tokio::test]
156 async fn test_stale_pid_returns_false() {
157 let dir = temp_dir().join("ipfrs_test_connectivity_stale");
159 let _ = std::fs::create_dir_all(&dir);
160 let pid_path = dir.join("daemon.pid");
161 std::fs::write(&pid_path, "99999999").expect("write pid file");
162 let result = check_daemon_reachable(&dir.to_string_lossy()).await;
163 assert!(!result, "should be false for a stale PID");
164 let _ = std::fs::remove_dir_all(&dir);
165 }
166
167 #[tokio::test]
168 async fn test_with_network_timeout_completes() {
169 let result = with_network_timeout(async { 7_u32 }, Duration::from_secs(1)).await;
170 assert_eq!(result, Some(7));
171 }
172
173 #[tokio::test]
174 async fn test_with_network_timeout_expires() {
175 let result = with_network_timeout(
176 async {
177 tokio::time::sleep(Duration::from_secs(10)).await;
178 42_u32
179 },
180 Duration::from_millis(50),
181 )
182 .await;
183 assert!(result.is_none(), "should time out");
184 }
185
186 #[test]
187 fn test_offline_error_message_contains_data_dir() {
188 let msg = offline_error_message("/tmp/test-ipfrs");
189 assert!(
190 msg.contains("daemon is not running"),
191 "should mention daemon not running"
192 );
193 assert!(
194 msg.contains("/tmp/test-ipfrs"),
195 "should include the data_dir path"
196 );
197 }
198
199 #[test]
200 fn test_offline_error_message_contains_start_command() {
201 let msg = offline_error_message(".ipfrs");
202 assert!(
203 msg.contains("daemon start"),
204 "should suggest 'daemon start'"
205 );
206 assert!(msg.contains("daemon run"), "should suggest 'daemon run'");
207 }
208}