debugger/cli/
spawn.rs

1//! Daemon spawning logic
2//!
3//! Automatically spawns the daemon process when needed, using the same binary
4//! with the hidden `daemon` subcommand.
5
6use std::time::Duration;
7
8use crate::common::{paths, Error, Result};
9use crate::ipc::{transport, DaemonClient};
10
11/// Timeout for daemon to start up
12const SPAWN_TIMEOUT_SECS: u64 = 5;
13
14/// Ensure the daemon is running, spawning it if necessary
15pub async fn ensure_daemon_running() -> Result<()> {
16    // Try to connect first
17    match DaemonClient::connect().await {
18        Ok(_) => return Ok(()), // Already running
19        Err(Error::DaemonNotRunning) => {
20            // Need to spawn
21            spawn_daemon().await?;
22        }
23        Err(e) => return Err(e),
24    }
25
26    Ok(())
27}
28
29/// Spawn the daemon process
30async fn spawn_daemon() -> Result<()> {
31    tracing::debug!("Spawning daemon process");
32
33    // Get path to current executable
34    let exe_path = std::env::current_exe().map_err(|e| {
35        Error::Internal(format!("Failed to get current executable path: {}", e))
36    })?;
37
38    // Ensure socket directory exists
39    paths::ensure_socket_dir()?;
40
41    // Remove stale socket if it exists
42    paths::remove_socket()?;
43
44    // Spawn detached process with output redirected to /dev/null
45    // The daemon logs to its own log file, so we don't need terminal output
46    #[cfg(unix)]
47    {
48        use std::os::unix::process::CommandExt;
49        use std::fs::File;
50        
51        // Open /dev/null for stdout/stderr
52        let dev_null = File::open("/dev/null")
53            .map_err(|e| Error::Internal(format!("Failed to open /dev/null: {}", e)))?;
54        let dev_null_out = File::create("/dev/null")
55            .map_err(|e| Error::Internal(format!("Failed to open /dev/null for write: {}", e)))?;
56        
57        std::process::Command::new(&exe_path)
58            .arg("daemon")
59            .stdin(std::process::Stdio::from(dev_null))
60            .stdout(std::process::Stdio::from(dev_null_out.try_clone().unwrap()))
61            .stderr(std::process::Stdio::from(dev_null_out))
62            .process_group(0) // New process group (detach from terminal)
63            .spawn()
64            .map_err(|e| Error::Internal(format!("Failed to spawn daemon: {}", e)))?;
65    }
66
67    #[cfg(windows)]
68    {
69        use std::os::windows::process::CommandExt;
70        const DETACHED_PROCESS: u32 = 0x00000008;
71        const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
72        std::process::Command::new(&exe_path)
73            .arg("daemon")
74            .stdin(std::process::Stdio::null())
75            .stdout(std::process::Stdio::null())
76            .stderr(std::process::Stdio::null())
77            .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
78            .spawn()
79            .map_err(|e| Error::Internal(format!("Failed to spawn daemon: {}", e)))?;
80    }
81
82    // Wait for daemon to start accepting connections
83    let deadline = std::time::Instant::now() + Duration::from_secs(SPAWN_TIMEOUT_SECS);
84
85    loop {
86        if std::time::Instant::now() >= deadline {
87            return Err(Error::DaemonSpawnTimeout(SPAWN_TIMEOUT_SECS));
88        }
89
90        // Try to connect
91        tokio::time::sleep(Duration::from_millis(50)).await;
92
93        // Check if socket exists (Unix only)
94        #[cfg(unix)]
95        if !paths::socket_path().exists() {
96            continue;
97        }
98
99        // Try to connect
100        match transport::connect().await {
101            Ok(_) => {
102                tracing::debug!("Daemon started successfully");
103                return Ok(());
104            }
105            Err(_) => continue,
106        }
107    }
108}