use crate::paths;
use std::fs;
use std::path::Path;
use std::process::{Child, Command, ExitStatus};
pub fn spawn_daemon(session: &str) -> std::io::Result<()> {
let socket_dir = paths::socket_base_dir();
fs::create_dir_all(&socket_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&socket_dir, fs::Permissions::from_mode(0o700))?;
}
let state = paths::state_dir(session);
fs::create_dir_all(state.join("logs"))?;
let socket_path = paths::socket_path(session);
let pid_path = paths::pid_path(session);
let _ = fs::remove_file(&socket_path);
let exe = std::env::current_exe()?;
let mut child = Command::new(&exe)
.args(["run-daemon", session])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
let daemon_log_path = state.join("daemon.log");
let result = wait_for_daemon_ready(&mut child, &pid_path, &socket_path, &daemon_log_path);
drop(child);
result
}
fn wait_for_daemon_ready(
child: &mut Child,
pid_path: &Path,
socket_path: &Path,
daemon_log_path: &Path,
) -> std::io::Result<()> {
for _ in 0..100 {
if let Some(status) = child.try_wait()? {
return Err(std::io::Error::other(format!(
"daemon exited early ({}){}",
format_exit_status(status),
daemon_log_hint(daemon_log_path)
)));
}
if pid_path.exists() && socket_path.exists() {
if std::os::unix::net::UnixStream::connect(socket_path).is_ok() {
return Ok(());
}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"daemon did not start within 5s{}",
daemon_log_hint(daemon_log_path)
),
))
}
pub async fn run_daemon(session: &str) -> i32 {
{
use tracing_subscriber::{EnvFilter, fmt};
let log_file = paths::state_dir(session).join("daemon.log");
let _ = std::fs::create_dir_all(paths::state_dir(session));
if let Ok(file) = std::fs::File::create(&log_file) {
let subscriber = fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("agent_procs=info".parse().unwrap()),
)
.with_writer(file)
.with_ansi(false)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
}
}
let socket_path = paths::socket_path(session);
let pid_path = paths::pid_path(session);
let socket_dir = paths::socket_base_dir();
let _ = std::fs::create_dir_all(&socket_dir);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o700));
}
let state = paths::state_dir(session);
let _ = std::fs::create_dir_all(state.join("logs"));
if let Ok(mut f) = std::fs::File::create(&pid_path) {
use std::io::Write;
let _ = writeln!(f, "{}", std::process::id());
}
let exit_code = match super::server::run(session, &socket_path).await {
Ok(()) => 0,
Err(e) => {
tracing::error!(error = %e, "daemon server exited with error");
1
}
};
let _ = std::fs::remove_file(&socket_path);
let _ = std::fs::remove_file(&pid_path);
exit_code
}
fn format_exit_status(status: ExitStatus) -> String {
match status.code() {
Some(code) => format!("exit code {}", code),
None => "terminated by signal".to_string(),
}
}
fn daemon_log_hint(daemon_log_path: &Path) -> String {
let Ok(contents) = fs::read_to_string(daemon_log_path) else {
return String::new();
};
let Some(line) = contents.lines().rev().find(|line| !line.trim().is_empty()) else {
return String::new();
};
format!("; {}", line.trim())
}