use super::machine::UnixConnector;
use anyhow::{Context, Result, bail};
use arcbox_constants::paths::HostLayout;
use clap::{Args, ValueEnum};
use humantime::format_duration;
use std::ffi::OsString;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
use tokio::time::{Instant, sleep, timeout};
use tonic::transport::Endpoint;
use tracing::warn;
#[derive(Debug, Args)]
pub struct DaemonArgs {
#[arg(value_name = "ACTION")]
pub action: DaemonAction,
#[arg(long)]
pub socket: Option<PathBuf>,
#[arg(long)]
pub grpc_socket: Option<PathBuf>,
#[arg(long)]
pub data_dir: Option<PathBuf>,
#[arg(long)]
pub kernel: Option<PathBuf>,
#[arg(long, short = 'f')]
pub foreground: bool,
#[arg(long)]
pub docker_integration: bool,
#[arg(long)]
pub guest_docker_vsock_port: Option<u32>,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum DaemonAction {
Start,
Stop,
Status,
}
pub async fn execute(args: DaemonArgs) -> Result<()> {
match args.action {
DaemonAction::Start if args.foreground => exec_foreground(&args),
DaemonAction::Start => spawn_background(&args),
DaemonAction::Stop => execute_stop(&args).await,
DaemonAction::Status => execute_status(&args).await,
}
}
fn exec_foreground(args: &DaemonArgs) -> Result<()> {
let daemon_binary = resolve_daemon_binary()?;
let daemon_args = build_daemon_args(args);
#[cfg(unix)]
{
let err = Command::new(&daemon_binary).args(&daemon_args).exec();
Err(anyhow::Error::from(err)).with_context(|| {
format!(
"Failed to exec daemon binary at {}",
daemon_binary.display()
)
})
}
#[cfg(not(unix))]
{
let status = Command::new(&daemon_binary)
.args(&daemon_args)
.status()
.with_context(|| {
format!("Failed to run daemon binary at {}", daemon_binary.display())
})?;
if status.success() {
Ok(())
} else {
bail!("Daemon exited with status {}", status);
}
}
}
async fn execute_stop(args: &DaemonArgs) -> Result<()> {
let layout = resolve_layout(args);
let lock_file = &layout.lock_file;
let grpc_socket = &layout.grpc_socket;
if !daemon_is_alive(lock_file) {
println!("Daemon is not running");
return Ok(());
}
let Some(pid) = read_lock_file(lock_file)? else {
bail!("Daemon is alive (lock held) but lock file has no valid PID");
};
send_sigterm(pid)?;
println!("Stopping ArcBox daemon (PID {pid})...");
let timeout_window = Duration::from_secs(40);
let deadline = Instant::now() + timeout_window;
loop {
let still_alive = daemon_is_alive(lock_file);
let grpc_socket_removed = !grpc_socket.exists();
if !still_alive && grpc_socket_removed {
println!("ArcBox daemon stopped");
return Ok(());
}
if Instant::now() >= deadline {
bail!(
"ArcBox daemon (PID {pid}) did not fully stop within {}s (lock_held={}, grpc_socket_present={})",
timeout_window.as_secs(),
still_alive,
!grpc_socket_removed,
);
}
sleep(Duration::from_millis(100)).await;
}
}
async fn execute_status(args: &DaemonArgs) -> Result<()> {
let layout = resolve_layout(args);
let lock_file = &layout.lock_file;
let docker_socket = &layout.docker_socket;
let grpc_socket = &layout.grpc_socket;
let running = daemon_is_alive(lock_file);
let pid = if running {
read_lock_file(lock_file)?
} else {
None
};
let status = if running { "running" } else { "stopped" };
let uptime = if running {
lock_file_uptime(lock_file).map_or_else(
|| "unknown".to_string(),
|duration| format_duration(duration).to_string(),
)
} else {
"n/a".to_string()
};
let grpc_responsive = if running {
grpc_daemon_is_responsive(grpc_socket).await
} else {
false
};
println!("ArcBox daemon status: {status}");
println!(
"PID: {}",
pid.map_or_else(|| "n/a".to_string(), |value| value.to_string())
);
println!("Docker socket: {}", docker_socket.display());
println!("gRPC socket: {}", grpc_socket.display());
println!("Uptime: {uptime}");
println!(
"gRPC responsive: {}",
if grpc_responsive { "yes" } else { "no" }
);
Ok(())
}
async fn grpc_daemon_is_responsive(socket_path: &Path) -> bool {
if !socket_path.exists() {
return false;
}
let endpoint =
Endpoint::from_static("http://[::]:50051").connect_timeout(Duration::from_secs(1));
let connect_future =
endpoint.connect_with_connector(UnixConnector::new(socket_path.to_path_buf()));
matches!(
timeout(Duration::from_secs(2), connect_future).await,
Ok(Ok(_))
)
}
fn lock_file_uptime(lock_file: &Path) -> Option<Duration> {
let modified_at = std::fs::metadata(lock_file).ok()?.modified().ok()?;
SystemTime::now().duration_since(modified_at).ok()
}
fn spawn_background(args: &DaemonArgs) -> Result<()> {
let layout = resolve_layout(args);
std::fs::create_dir_all(&layout.run_dir).context("Failed to create daemon run directory")?;
std::fs::create_dir_all(&layout.log_dir).context("Failed to create daemon log directory")?;
if daemon_is_alive(&layout.lock_file) {
let pid = read_lock_file(&layout.lock_file)?;
let pid_str = pid.map_or_else(|| "unknown".to_string(), |p| p.to_string());
bail!("Daemon already running (PID {pid_str})");
}
let daemon_binary = resolve_daemon_binary()?;
let daemon_args = build_daemon_args(args);
let child = Command::new(&daemon_binary)
.args(&daemon_args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.with_context(|| {
format!(
"Failed to launch ArcBox daemon binary at {}",
daemon_binary.display()
)
})?;
println!("ArcBox daemon started (PID {})", child.id());
println!(" Lock file: {}", layout.lock_file.display());
println!(" Logs: {}", layout.daemon_log.display());
Ok(())
}
fn resolve_daemon_binary() -> Result<PathBuf> {
let current_exe = std::env::current_exe().context("Failed to resolve current executable")?;
if let Some(parent) = current_exe.parent() {
let sibling = parent.join("arcbox-daemon");
if sibling.is_file() {
return Ok(sibling);
}
}
if let Some(path) = find_in_path("arcbox-daemon") {
return Ok(path);
}
bail!("Failed to locate `arcbox-daemon` next to `arcbox` or in PATH");
}
fn find_in_path(binary: &str) -> Option<PathBuf> {
let path_var = std::env::var_os("PATH")?;
std::env::split_paths(&path_var)
.map(|entry| entry.join(binary))
.find(|candidate| candidate.is_file())
}
fn build_daemon_args(args: &DaemonArgs) -> Vec<OsString> {
let mut daemon_args = Vec::new();
if let Some(socket) = &args.socket {
daemon_args.push(OsString::from("--socket"));
daemon_args.push(socket.as_os_str().to_os_string());
}
if let Some(grpc_socket) = &args.grpc_socket {
daemon_args.push(OsString::from("--grpc-socket"));
daemon_args.push(grpc_socket.as_os_str().to_os_string());
}
if let Some(data_dir) = &args.data_dir {
daemon_args.push(OsString::from("--data-dir"));
daemon_args.push(data_dir.as_os_str().to_os_string());
}
if let Some(kernel) = &args.kernel {
daemon_args.push(OsString::from("--kernel"));
daemon_args.push(kernel.as_os_str().to_os_string());
}
if args.foreground {
daemon_args.push(OsString::from("--foreground"));
}
if args.docker_integration {
daemon_args.push(OsString::from("--docker-integration"));
}
if let Some(port) = args.guest_docker_vsock_port {
daemon_args.push(OsString::from("--guest-docker-vsock-port"));
daemon_args.push(OsString::from(port.to_string()));
}
daemon_args
}
fn resolve_layout(args: &DaemonArgs) -> HostLayout {
let mut layout = HostLayout::resolve(args.data_dir.as_deref());
if let Some(socket) = &args.socket {
layout.docker_socket.clone_from(socket);
}
if let Some(grpc) = &args.grpc_socket {
layout.grpc_socket.clone_from(grpc);
}
layout
}
fn read_lock_file(lock_file: &Path) -> Result<Option<i32>> {
if !lock_file.exists() {
return Ok(None);
}
let pid_text = std::fs::read_to_string(lock_file).with_context(|| {
format!(
"Failed to read daemon lock file from {}",
lock_file.display()
)
})?;
match pid_text.trim().parse::<i32>() {
Ok(pid) if pid > 0 => Ok(Some(pid)),
_ => {
warn!("Invalid PID in daemon lock file {}", lock_file.display());
Ok(None)
}
}
}
fn daemon_is_alive(lock_file: &Path) -> bool {
use std::os::unix::io::AsRawFd;
let file = match std::fs::OpenOptions::new().read(true).open(lock_file) {
Ok(f) => f,
Err(_) => return false,
};
let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if ret == 0 {
unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
} else {
let err = std::io::Error::last_os_error();
let raw = err.raw_os_error();
if raw == Some(libc::EWOULDBLOCK) || raw == Some(libc::EAGAIN) {
return true;
}
warn!(%err, "Unexpected flock error probing daemon lock, assuming not running");
}
false
}
fn send_sigterm(pid: i32) -> Result<()> {
let result = unsafe { libc::kill(pid, libc::SIGTERM) };
if result == 0 {
return Ok(());
}
Err(std::io::Error::last_os_error())
.with_context(|| format!("Failed to send SIGTERM to daemon process {pid}"))
}
#[cfg(test)]
mod tests {
use super::read_lock_file;
use std::fs;
use tempfile::tempdir;
#[test]
fn read_lock_file_returns_none_when_missing() {
let dir = tempdir().expect("failed to create temp dir");
let lock_file = dir.path().join("daemon.lock");
let pid = read_lock_file(&lock_file).expect("read_lock_file should succeed");
assert_eq!(pid, None);
}
#[test]
fn read_lock_file_parses_valid_pid() {
let dir = tempdir().expect("failed to create temp dir");
let lock_file = dir.path().join("daemon.lock");
fs::write(&lock_file, "12345\n").expect("failed to write lock file");
let pid = read_lock_file(&lock_file).expect("read_lock_file should succeed");
assert_eq!(pid, Some(12345));
assert!(lock_file.exists());
}
#[test]
fn read_lock_file_returns_none_for_invalid_content() {
let dir = tempdir().expect("failed to create temp dir");
let lock_file = dir.path().join("daemon.lock");
fs::write(&lock_file, "not-a-pid").expect("failed to write lock file");
let pid = read_lock_file(&lock_file).expect("read_lock_file should succeed");
assert_eq!(pid, None);
assert!(lock_file.exists());
}
}