ipfrs_cli/commands/
daemon.rs1use anyhow::Result;
12use tracing::info;
13
14use crate::output::{self, error, format_bytes, print_kv, success};
15use crate::progress;
16
17pub async fn run_daemon(data_dir: String) -> Result<()> {
19 use ipfrs::{Node, NodeConfig};
20
21 info!("Initializing IPFRS node...");
22 let mut config = NodeConfig::default();
23 config.network.data_dir = std::path::PathBuf::from(&data_dir);
24 config.storage.path = std::path::PathBuf::from(&data_dir).join("blocks");
25
26 let mut node = Node::new(config)?;
27
28 info!("Starting IPFRS node...");
29 node.start().await?;
30
31 info!("IPFRS daemon running. Press Ctrl+C to stop.");
32
33 tokio::signal::ctrl_c().await?;
35
36 info!("Shutting down...");
37 node.stop().await?;
38
39 Ok(())
40}
41
42pub async fn daemon_start(data_dir: String, pid_file: String, log_file: String) -> Result<()> {
44 use std::fs;
45 use std::process::{Command, Stdio};
46
47 let pid_path = std::path::Path::new(&pid_file);
48
49 if pid_path.exists() {
51 let pid_content = fs::read_to_string(pid_path)?;
52 if let Ok(pid) = pid_content.trim().parse::<i32>() {
53 #[cfg(unix)]
55 {
56 use std::process::Command as StdCommand;
57 let check = StdCommand::new("kill")
58 .arg("-0")
59 .arg(pid.to_string())
60 .output();
61 if check.is_ok() && check.unwrap().status.success() {
62 error("Daemon is already running");
63 print_kv("PID", &pid.to_string());
64 print_kv("PID file", &pid_file);
65 return Ok(());
66 }
67 }
68 }
69 fs::remove_file(pid_path)?;
71 }
72
73 let data_path = std::path::Path::new(&data_dir);
75 if !data_path.exists() {
76 fs::create_dir_all(data_path)?;
77 }
78
79 let pb = progress::spinner("Starting daemon in background");
80
81 let exe_path = std::env::current_exe()?;
83
84 let log_file_handle = fs::OpenOptions::new()
86 .create(true)
87 .append(true)
88 .open(&log_file)?;
89
90 let child = Command::new(exe_path)
91 .arg("daemon")
92 .arg("run")
93 .arg("--data-dir")
94 .arg(&data_dir)
95 .stdin(Stdio::null())
96 .stdout(log_file_handle.try_clone()?)
97 .stderr(log_file_handle)
98 .spawn()?;
99
100 let pid = child.id();
101
102 fs::write(&pid_file, pid.to_string())?;
104
105 progress::finish_spinner_success(&pb, "Daemon started");
106
107 success("IPFRS daemon started in background");
108 print_kv("PID", &pid.to_string());
109 print_kv("PID file", &pid_file);
110 print_kv("Log file", &log_file);
111 print_kv("Data directory", &data_dir);
112
113 Ok(())
114}
115
116pub async fn daemon_stop(pid_file: String) -> Result<()> {
118 use std::fs;
119
120 let pid_path = std::path::Path::new(&pid_file);
121
122 if !pid_path.exists() {
123 error("Daemon is not running (PID file not found)");
124 print_kv("PID file", &pid_file);
125 return Ok(());
126 }
127
128 let pid_content = fs::read_to_string(pid_path)?;
129 let pid = pid_content
130 .trim()
131 .parse::<i32>()
132 .map_err(|e| anyhow::anyhow!("Invalid PID in file: {}", e))?;
133
134 let pb = progress::spinner(&format!("Stopping daemon (PID: {})", pid));
135
136 #[cfg(unix)]
137 {
138 use std::process::Command;
139
140 let result = Command::new("kill")
142 .arg("-TERM")
143 .arg(pid.to_string())
144 .output()?;
145
146 if !result.status.success() {
147 progress::finish_spinner_error(&pb, "Failed to stop daemon");
148 error(&format!("Failed to send SIGTERM to process {}", pid));
149 error("Daemon may not be running or you may not have permission");
150 return Ok(());
151 }
152
153 let mut attempts = 0;
155 while attempts < 30 {
156 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
157
158 let check = Command::new("kill").arg("-0").arg(pid.to_string()).output();
159
160 if check.is_err() || !check.unwrap().status.success() {
161 break;
163 }
164 attempts += 1;
165 }
166
167 let check = Command::new("kill").arg("-0").arg(pid.to_string()).output();
169
170 if check.is_ok() && check.unwrap().status.success() {
171 progress::finish_spinner_error(&pb, "Daemon did not stop gracefully");
172 output::warning(&format!(
173 "Process {} is still running after {} seconds",
174 pid,
175 attempts / 10
176 ));
177 output::warning("You may need to use 'kill -9' to force termination");
178 return Ok(());
179 }
180 }
181
182 #[cfg(not(unix))]
183 {
184 progress::finish_spinner_error(&pb, "Not supported on this platform");
185 error("Daemon management is only supported on Unix-like systems");
186 return Err(anyhow::anyhow!(
187 "Daemon management not supported on this platform"
188 ));
189 }
190
191 fs::remove_file(pid_path)?;
193
194 progress::finish_spinner_success(&pb, "Daemon stopped");
195 success(&format!("Stopped daemon (PID: {})", pid));
196
197 Ok(())
198}
199
200pub async fn daemon_status(pid_file: String) -> Result<()> {
202 use std::fs;
203
204 let pid_path = std::path::Path::new(&pid_file);
205
206 if !pid_path.exists() {
207 output::info("Daemon is not running");
208 print_kv("PID file", &pid_file);
209 print_kv("Status", "stopped");
210 return Ok(());
211 }
212
213 let pid_content = fs::read_to_string(pid_path)?;
214 let pid = pid_content
215 .trim()
216 .parse::<i32>()
217 .map_err(|e| anyhow::anyhow!("Invalid PID in file: {}", e))?;
218
219 #[cfg(unix)]
220 {
221 use std::process::Command;
222
223 let check = Command::new("kill")
225 .arg("-0")
226 .arg(pid.to_string())
227 .output()?;
228
229 if check.status.success() {
230 success("Daemon is running");
231 print_kv("PID", &pid.to_string());
232 print_kv("PID file", &pid_file);
233 print_kv("Status", "running");
234
235 let ps_output = Command::new("ps")
237 .arg("-p")
238 .arg(pid.to_string())
239 .arg("-o")
240 .arg("etime=,rss=")
241 .output();
242
243 if let Ok(output) = ps_output {
244 if output.status.success() {
245 let info = String::from_utf8_lossy(&output.stdout);
246 let parts: Vec<&str> = info.split_whitespace().collect();
247 if parts.len() >= 2 {
248 print_kv("Uptime", parts[0]);
249 let memory_kb = parts[1].parse::<u64>().unwrap_or(0);
250 print_kv("Memory", &format_bytes(memory_kb * 1024));
251 }
252 }
253 }
254 } else {
255 output::warning("Daemon is not running (stale PID file)");
256 print_kv("PID file", &pid_file);
257 print_kv("Stale PID", &pid.to_string());
258 print_kv("Status", "stopped");
259 output::info("You may want to remove the stale PID file");
260 }
261 }
262
263 #[cfg(not(unix))]
264 {
265 error("Daemon status check is only supported on Unix-like systems");
266 print_kv("PID", &pid.to_string());
267 }
268
269 Ok(())
270}
271
272pub async fn daemon_restart(data_dir: String, pid_file: String, log_file: String) -> Result<()> {
274 output::info("Restarting daemon...");
275
276 let pid_path = std::path::Path::new(&pid_file);
278 if pid_path.exists() {
279 daemon_stop(pid_file.clone()).await?;
280 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
282 } else {
283 output::info("Daemon was not running");
284 }
285
286 daemon_start(data_dir, pid_file, log_file).await?;
288
289 success("Daemon restarted successfully");
290
291 Ok(())
292}
293
294pub async fn daemon_health(pid_file: String, data_dir: String, format: String) -> Result<()> {
304 use std::fs;
305
306 let mut health_status = Vec::new();
307 let mut overall_healthy = true;
308
309 if format == "text" {
311 println!("IPFRS Health Check");
312 println!("==================");
313 println!();
314 }
315
316 let pid_path = std::path::Path::new(&pid_file);
318 let daemon_running = if pid_path.exists() {
319 if let Ok(pid_content) = fs::read_to_string(pid_path) {
320 if let Ok(pid) = pid_content.trim().parse::<i32>() {
321 #[cfg(unix)]
322 {
323 use std::process::Command;
324 let check = Command::new("kill").arg("-0").arg(pid.to_string()).output();
325 check.is_ok() && check.unwrap().status.success()
326 }
327 #[cfg(not(unix))]
328 {
329 true
330 }
331 } else {
332 false
333 }
334 } else {
335 false
336 }
337 } else {
338 false
339 };
340
341 health_status.push(("daemon_running", daemon_running));
342 if !daemon_running {
343 overall_healthy = false;
344 }
345
346 if format == "text" {
347 println!("Daemon Status:");
348 println!("-------------");
349 if daemon_running {
350 success("✓ Daemon is running");
351 } else {
352 error("✗ Daemon is not running");
353 }
354 println!();
355 }
356
357 if format == "text" {
359 println!("Repository Health:");
360 println!("-----------------");
361 }
362
363 let data_path = std::path::Path::new(&data_dir);
364 let repo_exists = data_path.exists() && data_path.is_dir();
365 health_status.push(("repository_exists", repo_exists));
366
367 if format == "text" {
368 if repo_exists {
369 success(&format!("✓ Repository exists at {}", data_dir));
370
371 if let Ok(blocks_path) = data_path.join("blocks").canonicalize() {
373 if blocks_path.exists() {
374 if let Ok(entries) = fs::read_dir(&blocks_path) {
376 let block_count = entries.count();
377 print_kv(" Blocks", &block_count.to_string());
378 }
379 }
380 }
381 } else {
382 error(&format!("✗ Repository not found at {}", data_dir));
383 overall_healthy = false;
384 }
385 println!();
386 }
387
388 if format == "text" {
390 println!("Disk Space:");
391 println!("-----------");
392 }
393
394 #[cfg(unix)]
395 {
396 use std::process::Command;
397 if let Ok(output) = Command::new("df").arg("-h").arg(&data_dir).output() {
398 if output.status.success() {
399 let df_output = String::from_utf8_lossy(&output.stdout);
400 let lines: Vec<&str> = df_output.lines().collect();
401 if lines.len() >= 2 {
402 let parts: Vec<&str> = lines[1].split_whitespace().collect();
403 if parts.len() >= 5 {
404 let available = parts[3];
405 let use_percent = parts[4];
406
407 let usage: u32 = use_percent.trim_end_matches('%').parse().unwrap_or(0);
408 let disk_healthy = usage < 90;
409 health_status.push(("disk_space_ok", disk_healthy));
410
411 if format == "text" {
412 if disk_healthy {
413 success(&format!(
414 "✓ Disk usage: {} (available: {})",
415 use_percent, available
416 ));
417 } else {
418 output::warning(&format!(
419 "⚠ Disk usage high: {} (available: {})",
420 use_percent, available
421 ));
422 overall_healthy = false;
423 }
424 }
425 }
426 }
427 }
428 }
429 }
430
431 if format == "text" {
432 println!();
433 }
434
435 if daemon_running && format == "text" {
437 println!("Memory Usage:");
438 println!("-------------");
439
440 if let Ok(pid_content) = fs::read_to_string(pid_path) {
441 if let Ok(pid) = pid_content.trim().parse::<i32>() {
442 #[cfg(unix)]
443 {
444 use std::process::Command;
445 if let Ok(output) = Command::new("ps")
446 .arg("-p")
447 .arg(pid.to_string())
448 .arg("-o")
449 .arg("rss=,vsz=,%mem=")
450 .output()
451 {
452 if output.status.success() {
453 let mem_info = String::from_utf8_lossy(&output.stdout);
454 let parts: Vec<&str> = mem_info.split_whitespace().collect();
455 if parts.len() >= 3 {
456 let rss_kb = parts[0].parse::<u64>().unwrap_or(0);
457 let vsz_kb = parts[1].parse::<u64>().unwrap_or(0);
458 let mem_percent = parts[2];
459
460 print_kv(" RSS", &format_bytes(rss_kb * 1024));
461 print_kv(" VSZ", &format_bytes(vsz_kb * 1024));
462 print_kv(" Memory %", mem_percent);
463 }
464 }
465 }
466 }
467 }
468 }
469 println!();
470 }
471
472 if format == "text" {
474 println!("Overall Status:");
475 println!("---------------");
476 if overall_healthy {
477 success("✓ All health checks passed");
478 } else {
479 error("✗ Some health checks failed");
480 println!();
481 println!("Recommendations:");
482 if !daemon_running {
483 println!(" • Start the daemon: ipfrs daemon start");
484 }
485 if !repo_exists {
486 println!(" • Initialize repository: ipfrs init");
487 }
488 }
489 } else if format == "json" {
490 use serde_json::json;
492 let health_obj = json!({
493 "healthy": overall_healthy,
494 "daemon_running": daemon_running,
495 "repository_exists": repo_exists,
496 "checks": health_status.iter().map(|(k, v)| json!({
497 "name": k,
498 "passed": v
499 })).collect::<Vec<_>>()
500 });
501 println!("{}", serde_json::to_string_pretty(&health_obj)?);
502 }
503
504 Ok(())
505}