agent_procs/daemon/
spawn.rs1use crate::paths;
2use std::fs;
3use std::path::Path;
4use std::process::{Child, Command, ExitStatus};
5
6pub fn spawn_daemon(session: &str) -> std::io::Result<()> {
10 let socket_dir = paths::socket_base_dir();
12 fs::create_dir_all(&socket_dir)?;
13 #[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 let _ = fs::remove_file(&socket_path);
28
29 let exe = std::env::current_exe()?;
31
32 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 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 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
78pub async fn run_daemon(session: &str) -> i32 {
81 {
85 use tracing_subscriber::{EnvFilter, fmt};
86
87 let log_file = paths::state_dir(session).join("daemon.log");
88 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 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 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}