ipfrs_cli/commands/
daemon.rs

1//! Daemon management commands
2//!
3//! This module provides functions for managing the IPFRS daemon:
4//! - `run_daemon` - Run daemon in foreground
5//! - `daemon_start` - Start daemon in background
6//! - `daemon_stop` - Stop daemon
7//! - `daemon_status` - Check daemon status
8//! - `daemon_restart` - Restart daemon
9//! - `daemon_health` - Comprehensive health check
10
11use anyhow::Result;
12use tracing::info;
13
14use crate::output::{self, error, format_bytes, print_kv, success};
15use crate::progress;
16
17/// Run daemon in foreground
18pub 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    // Wait for Ctrl+C
34    tokio::signal::ctrl_c().await?;
35
36    info!("Shutting down...");
37    node.stop().await?;
38
39    Ok(())
40}
41
42/// Start daemon in background
43pub 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    // Check if daemon is already running
50    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            // Check if process is still running
54            #[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        // If we get here, the PID file is stale, remove it
70        fs::remove_file(pid_path)?;
71    }
72
73    // Create data directory if it doesn't exist
74    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    // Get the current executable path
82    let exe_path = std::env::current_exe()?;
83
84    // Spawn daemon process in background
85    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    // Write PID file
103    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
116/// Stop daemon
117pub 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        // Send SIGTERM
141        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        // Wait for process to terminate (with timeout)
154        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                // Process has terminated
162                break;
163            }
164            attempts += 1;
165        }
166
167        // Check if process is still running
168        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    // Remove PID file
192    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
200/// Check daemon status
201pub 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        // Check if process is running
224        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            // Try to get process info
236            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
272/// Restart daemon
273pub async fn daemon_restart(data_dir: String, pid_file: String, log_file: String) -> Result<()> {
274    output::info("Restarting daemon...");
275
276    // Stop the daemon if running
277    let pid_path = std::path::Path::new(&pid_file);
278    if pid_path.exists() {
279        daemon_stop(pid_file.clone()).await?;
280        // Wait a bit for cleanup
281        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
282    } else {
283        output::info("Daemon was not running");
284    }
285
286    // Start the daemon
287    daemon_start(data_dir, pid_file, log_file).await?;
288
289    success("Daemon restarted successfully");
290
291    Ok(())
292}
293
294/// Comprehensive health check for the daemon and system
295///
296/// Checks:
297/// - Daemon status
298/// - Repository health
299/// - Network connectivity
300/// - Disk space
301/// - Memory usage
302/// - API responsiveness
303pub 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    // Header
310    if format == "text" {
311        println!("IPFRS Health Check");
312        println!("==================");
313        println!();
314    }
315
316    // 1. Daemon Status
317    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    // 2. Repository Health
358    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            // Check repository size
372            if let Ok(blocks_path) = data_path.join("blocks").canonicalize() {
373                if blocks_path.exists() {
374                    // Count blocks
375                    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    // 3. Disk Space
389    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    // 4. Memory Usage (if daemon is running)
436    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    // 5. Overall Status
473    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        // JSON output
491        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}