use anyhow::{anyhow, Context, Result};
use std::fs;
use std::process::Command;
use std::time::{Duration, Instant, SystemTime};
use crate::paths;
pub const LABEL: &str = "dev.webchemist.devist.worker";
pub fn binary_path() -> Result<String> {
let exe = std::env::current_exe().context("locate current devist binary")?;
Ok(exe.to_string_lossy().to_string())
}
pub fn is_enabled() -> Result<bool> {
Ok(paths::worker_plist_file()?.exists())
}
fn render_plist() -> Result<String> {
let exe = binary_path()?;
let stdout_log = paths::worker_launchd_stdout_log()?;
let stderr_log = paths::worker_launchd_stderr_log()?;
let path_env = std::env::var("PATH").unwrap_or_else(|_| {
"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin".into()
});
Ok(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>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>worker</string>
<string>__run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>{stdout}</string>
<key>StandardErrorPath</key>
<string>{stderr}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>{path_env}</string>
</dict>
</dict>
</plist>
"#,
label = LABEL,
exe = exe,
stdout = stdout_log.display(),
stderr = stderr_log.display(),
path_env = path_env,
))
}
pub fn enable() -> Result<()> {
let plist_path = paths::worker_plist_file()?;
let agents_dir = paths::launch_agents_dir()?;
let worker_dir = paths::worker_dir()?;
fs::create_dir_all(&agents_dir).context("create LaunchAgents directory")?;
fs::create_dir_all(&worker_dir).context("create worker directory for logs")?;
let plist_body = render_plist()?;
fs::write(&plist_path, plist_body)
.with_context(|| format!("write {}", plist_path.display()))?;
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{}", uid);
let _ = Command::new("launchctl")
.args(["bootout", &domain, &plist_path.to_string_lossy()])
.output();
let out = Command::new("launchctl")
.args(["bootstrap", &domain, &plist_path.to_string_lossy()])
.output()
.context("run launchctl bootstrap")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(anyhow!(
"launchctl bootstrap failed: {}\nstderr: {}",
out.status,
stderr
));
}
Ok(())
}
pub fn disable() -> Result<()> {
let plist_path = paths::worker_plist_file()?;
let uid = unsafe { libc::getuid() };
let domain = format!("gui/{}", uid);
if plist_path.exists() {
let _ = Command::new("launchctl")
.args(["bootout", &domain, &plist_path.to_string_lossy()])
.output();
fs::remove_file(&plist_path).with_context(|| format!("remove {}", plist_path.display()))?;
}
Ok(())
}
pub fn restart_via_launchd(reason: &str) -> ! {
eprintln!("[self-restart] {reason} — exiting; launchd will respawn");
std::process::exit(0);
}
pub struct BinaryWatch {
started: Instant,
initial_mtime: Option<SystemTime>,
grace: Duration,
}
impl BinaryWatch {
pub fn capture() -> Self {
let initial_mtime = std::env::current_exe()
.ok()
.and_then(|p| fs::metadata(p).ok())
.and_then(|m| m.modified().ok());
Self {
started: Instant::now(),
initial_mtime,
grace: Duration::from_secs(60),
}
}
pub fn was_updated(&self) -> bool {
if self.started.elapsed() < self.grace {
return false;
}
let Some(initial) = self.initial_mtime else {
return false;
};
let current = std::env::current_exe()
.ok()
.and_then(|p| fs::metadata(p).ok())
.and_then(|m| m.modified().ok());
let Some(current) = current else {
return false;
};
current > initial
}
}