use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum DaemonStatus {
Running {
pid: u32,
},
Stale {
pid: u32,
},
Stopped,
}
impl std::fmt::Display for DaemonStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DaemonStatus::Running { pid } => write!(f, "running (PID {pid})"),
DaemonStatus::Stale { pid } => write!(f, "stale (PID {pid} dead)"),
DaemonStatus::Stopped => write!(f, "stopped"),
}
}
}
pub struct DaemonManager {
pid_file: PathBuf,
log_dir: PathBuf,
}
impl DaemonManager {
pub fn new(pid_file: &str, log_dir: &str) -> Self {
Self {
pid_file: crate::config::expand_home(pid_file),
log_dir: crate::config::expand_home(log_dir),
}
}
pub fn status(&self) -> DaemonStatus {
match self.read_pid() {
Some(pid) => {
if self.is_alive(pid) {
DaemonStatus::Running { pid }
} else {
DaemonStatus::Stale { pid }
}
}
None => DaemonStatus::Stopped,
}
}
pub fn start(&self, config_path: &Path) -> Result<()> {
match self.status() {
DaemonStatus::Running { pid } => {
anyhow::bail!("oxios is already running (PID {pid})");
}
DaemonStatus::Stale { .. } => {
self.cleanup()?;
}
DaemonStatus::Stopped => {}
}
std::fs::create_dir_all(&self.log_dir).context("failed to create log directory")?;
let log_file = self.log_dir.join("oxios.log");
let exe = std::env::current_exe().context("failed to locate oxios binary")?;
let child = std::process::Command::new(&exe)
.arg("--foreground")
.arg("--config")
.arg(config_path)
.stdout(std::fs::File::create(&log_file)?)
.stderr(std::fs::File::create(&log_file)?)
.spawn()
.context("failed to spawn oxios daemon")?;
let pid = child.id();
self.write_pid(pid)?;
println!("⬡ oxios started (PID {pid})");
println!(" Logs: {}", log_file.display());
println!(" Dashboard: http://127.0.0.1:4200");
Ok(())
}
pub fn stop(&self) -> Result<()> {
match self.status() {
DaemonStatus::Running { pid } => {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
if ret != 0 {
anyhow::bail!("failed to send SIGTERM to PID {pid}");
}
}
#[cfg(not(unix))]
{
let _ = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output();
}
for _ in 0..10 {
std::thread::sleep(std::time::Duration::from_millis(200));
if !self.is_alive(pid) {
break;
}
}
self.cleanup()?;
println!("⬡ oxios stopped");
Ok(())
}
DaemonStatus::Stale { .. } => {
self.cleanup()?;
println!("⬡ cleaned up stale PID file");
Ok(())
}
DaemonStatus::Stopped => {
println!("⬡ oxios is not running");
Ok(())
}
}
}
pub fn restart(&self, config_path: &Path) -> Result<()> {
if matches!(self.status(), DaemonStatus::Running { .. }) {
self.stop()?;
std::thread::sleep(std::time::Duration::from_millis(500));
}
self.start(config_path)
}
pub fn install_service(&self) -> Result<()> {
let exe = std::env::current_exe().context("failed to locate oxios binary")?;
#[cfg(target_os = "macos")]
{
let plist_dir = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents"))
.context("failed to locate LaunchAgents directory")?;
std::fs::create_dir_all(&plist_dir)?;
let plist_path = plist_dir.join("com.a7garden.oxios.plist");
let home = dirs::home_dir().context("failed to get HOME")?;
let log_path = self.log_dir.join("oxiosd.log");
let plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.a7garden.oxios</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>--foreground</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
<key>WorkingDirectory</key>
<string>{home}</string>
</dict>
</plist>
"#,
exe = exe.display(),
log = log_path.display(),
home = home.display(),
);
std::fs::write(&plist_path, &plist)?;
println!("✓ Installed launchd service");
println!(" {}", plist_path.display());
println!();
println!(" Start with: launchctl load {}", plist_path.display());
println!(" Stop with: launchctl unload {}", plist_path.display());
println!(" Or simply: oxios start / oxios stop");
}
#[cfg(target_os = "linux")]
{
let unit_dir = PathBuf::from("/etc/systemd/system");
let unit_path = unit_dir.join("oxiosd.service");
let unit = format!(
r#"[Unit]
Description=Oxios Agent Operating System
After=network.target
[Service]
Type=simple
ExecStart={exe} --foreground
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
"#,
exe = exe.display(),
);
if let Err(e) = std::fs::write(&unit_path, &unit) {
anyhow::bail!(
"Failed to write {} — run with sudo: {}",
unit_path.display(),
e
);
}
println!("✓ Installed systemd service");
println!(" {}", unit_path.display());
println!();
println!(" Reload: sudo systemctl daemon-reload");
println!(" Start: sudo systemctl start oxiosd");
println!(" Enable: sudo systemctl enable oxiosd");
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
anyhow::bail!("daemon install only supported on macOS and Linux");
}
Ok(())
}
pub fn uninstall_service(&self) -> Result<()> {
#[cfg(target_os = "macos")]
{
let plist_path = dirs::home_dir()
.map(|h| h.join("Library/LaunchAgents/com.a7garden.oxios.plist"))
.context("failed to locate plist")?;
if plist_path.exists() {
std::fs::remove_file(&plist_path)?;
println!("✓ Removed launchd service");
} else {
println!(" Service not installed");
}
}
#[cfg(target_os = "linux")]
{
let unit_path = PathBuf::from("/etc/systemd/system/oxiosd.service");
if unit_path.exists() {
if let Err(e) = std::fs::remove_file(&unit_path) {
anyhow::bail!(
"Failed to remove {} — run with sudo: {}",
unit_path.display(),
e
);
}
println!("✓ Removed systemd service");
} else {
println!(" Service not installed");
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
anyhow::bail!("daemon uninstall only supported on macOS and Linux");
}
Ok(())
}
fn read_pid(&self) -> Option<u32> {
let content = std::fs::read_to_string(&self.pid_file).ok()?;
content.trim().parse().ok()
}
fn write_pid(&self, pid: u32) -> Result<()> {
if let Some(parent) = self.pid_file.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&self.pid_file, pid.to_string())?;
Ok(())
}
fn cleanup(&self) -> Result<()> {
if self.pid_file.exists() {
std::fs::remove_file(&self.pid_file)?;
}
Ok(())
}
fn is_alive(&self, pid: u32) -> bool {
#[cfg(unix)]
{
unsafe { libc::kill(pid as i32, 0) == 0 }
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}
}