Skip to main content

agent_procs/daemon/
spawn.rs

1use crate::paths;
2use std::fs;
3use std::path::Path;
4use std::process::{Child, Command, ExitStatus};
5
6/// Spawns the daemon as a detached background process by re-executing the
7/// current binary with the `--run-daemon SESSION` internal flag.
8/// This avoids the fork-inside-tokio-runtime problem.
9pub fn spawn_daemon(session: &str) -> std::io::Result<()> {
10    // Create socket base dir with restricted permissions
11    let socket_dir = paths::socket_base_dir();
12    fs::create_dir_all(&socket_dir)?;
13    // Set permissions to 0700 (owner only)
14    #[cfg(unix)]
15    {
16        use std::os::unix::fs::PermissionsExt;
17        fs::set_permissions(&socket_dir, fs::Permissions::from_mode(0o700))?;
18    }
19
20    let state = paths::state_dir(session);
21    fs::create_dir_all(state.join("logs"))?;
22
23    let socket_path = paths::socket_path(session);
24    let pid_path = paths::pid_path(session);
25
26    // Remove stale socket if present (ignore ENOENT)
27    let _ = fs::remove_file(&socket_path);
28
29    // Re-exec self with a hidden flag to run as daemon
30    let exe = std::env::current_exe()?;
31
32    // Spawn a background child and wait until it is actually listening.
33    let mut child = Command::new(&exe)
34        .args(["run-daemon", session])
35        .stdin(std::process::Stdio::null())
36        .stdout(std::process::Stdio::null())
37        .stderr(std::process::Stdio::null())
38        .spawn()?;
39
40    let daemon_log_path = state.join("daemon.log");
41    let result = wait_for_daemon_ready(&mut child, &pid_path, &socket_path, &daemon_log_path);
42    drop(child);
43    result
44}
45
46fn wait_for_daemon_ready(
47    child: &mut Child,
48    pid_path: &Path,
49    socket_path: &Path,
50    daemon_log_path: &Path,
51) -> std::io::Result<()> {
52    // Poll for socket existence, then try to connect to verify it's accepting
53    for _ in 0..100 {
54        if let Some(status) = child.try_wait()? {
55            return Err(std::io::Error::other(format!(
56                "daemon exited early ({}){}",
57                format_exit_status(status),
58                daemon_log_hint(daemon_log_path)
59            )));
60        }
61        if pid_path.exists() && socket_path.exists() {
62            // Try connecting to confirm the server is actually listening
63            if std::os::unix::net::UnixStream::connect(socket_path).is_ok() {
64                return Ok(());
65            }
66        }
67        std::thread::sleep(std::time::Duration::from_millis(50));
68    }
69    Err(std::io::Error::new(
70        std::io::ErrorKind::TimedOut,
71        format!(
72            "daemon did not start within 5s{}",
73            daemon_log_hint(daemon_log_path)
74        ),
75    ))
76}
77
78/// Entry point called when running as daemon (via --run-daemon flag in main.rs).
79/// This runs in a fresh process with no tokio runtime yet.
80pub async fn run_daemon(session: &str) -> i32 {
81    // Initialize file-based tracing subscriber before any other operations.
82    // The daemon's stdout/stderr are redirected to /dev/null, so structured
83    // logging to a file is the only way to capture diagnostics.
84    {
85        use tracing_subscriber::{EnvFilter, fmt};
86
87        let log_file = paths::state_dir(session).join("daemon.log");
88        // Ensure the parent directory exists before creating the log file.
89        let _ = std::fs::create_dir_all(paths::state_dir(session));
90        if let Ok(file) = std::fs::File::create(&log_file) {
91            let subscriber = fmt()
92                .with_env_filter(
93                    EnvFilter::from_default_env()
94                        .add_directive("agent_procs=info".parse().unwrap()),
95                )
96                .with_writer(file)
97                .with_ansi(false)
98                .finish();
99            let _ = tracing::subscriber::set_global_default(subscriber);
100        }
101    }
102
103    let socket_path = paths::socket_path(session);
104    let pid_path = paths::pid_path(session);
105
106    // Ensure dirs exist
107    let socket_dir = paths::socket_base_dir();
108    let _ = std::fs::create_dir_all(&socket_dir);
109    #[cfg(unix)]
110    {
111        use std::os::unix::fs::PermissionsExt;
112        let _ = std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o700));
113    }
114    let state = paths::state_dir(session);
115    let _ = std::fs::create_dir_all(state.join("logs"));
116
117    // Write PID file
118    if let Ok(mut f) = std::fs::File::create(&pid_path) {
119        use std::io::Write;
120        let _ = writeln!(f, "{}", std::process::id());
121    }
122
123    let exit_code = match super::server::run(session, &socket_path).await {
124        Ok(()) => 0,
125        Err(e) => {
126            tracing::error!(error = %e, "daemon server exited with error");
127            1
128        }
129    };
130
131    let _ = std::fs::remove_file(&socket_path);
132    let _ = std::fs::remove_file(&pid_path);
133    exit_code
134}
135
136fn format_exit_status(status: ExitStatus) -> String {
137    match status.code() {
138        Some(code) => format!("exit code {}", code),
139        None => "terminated by signal".to_string(),
140    }
141}
142
143fn daemon_log_hint(daemon_log_path: &Path) -> String {
144    let Ok(contents) = fs::read_to_string(daemon_log_path) else {
145        return String::new();
146    };
147    let Some(line) = contents.lines().rev().find(|line| !line.trim().is_empty()) else {
148        return String::new();
149    };
150    format!("; {}", line.trim())
151}