use std::path::{Path, PathBuf};
use crate::error::OlError;
use super::{
Supervisor, SupervisorKind, SupervisorStatus, ERR_SUPERVISION_BOOTSTRAP_FAILED,
ERR_SUPERVISION_CONTROL_FAILED, ERR_SUPERVISION_INSTALL_FAILED,
};
pub const LABEL: &str = "ai.openlatch.client";
pub const WATCHDOG_LABEL: &str = "ai.openlatch.client.watchdog";
#[cfg(unix)]
fn get_uid() -> u32 {
unsafe { libc::getuid() }
}
#[cfg(not(unix))]
fn get_uid() -> u32 {
0
}
enum BootstrapError {
AlreadyBootstrapped,
NoGuiSession,
Other(String),
}
fn bootstrap(plist: &Path, domain: &str) -> Result<(), BootstrapError> {
let out = std::process::Command::new("launchctl")
.args(["bootstrap", domain, &plist.display().to_string()])
.output();
match out {
Ok(o) if o.status.success() => Ok(()),
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr).to_string();
if stderr.contains("already bootstrapped") {
Err(BootstrapError::AlreadyBootstrapped)
} else if stderr.contains("Input/output error")
|| stderr.contains("Could not find domain")
{
Err(BootstrapError::NoGuiSession)
} else {
Err(BootstrapError::Other(stderr))
}
}
Err(e) => Err(BootstrapError::Other(format!("Cannot run launchctl: {e}"))),
}
}
fn bootstrap_with_fallback(plist: &Path) -> Result<(), String> {
let uid = get_uid();
match bootstrap(plist, &format!("gui/{uid}")) {
Ok(()) | Err(BootstrapError::AlreadyBootstrapped) => Ok(()),
Err(BootstrapError::NoGuiSession) => match bootstrap(plist, &format!("user/{uid}")) {
Ok(()) | Err(BootstrapError::AlreadyBootstrapped) => Ok(()),
Err(BootstrapError::NoGuiSession) => {
Err("launchctl bootstrap failed in both gui/ and user/ domains (no session)".into())
}
Err(BootstrapError::Other(msg)) if msg.is_empty() => {
Err("launchctl bootstrap failed in both gui/ and user/ domains".into())
}
Err(BootstrapError::Other(msg)) => Err(format!("launchctl bootstrap failed: {msg}")),
},
Err(BootstrapError::Other(msg)) => Err(format!("launchctl bootstrap failed: {msg}")),
}
}
fn bootout_everywhere(label: &str) {
let uid = get_uid();
for domain in [format!("gui/{uid}"), format!("user/{uid}")] {
let _ = std::process::Command::new("launchctl")
.args(["bootout", &format!("{domain}/{label}")])
.output();
}
}
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
pub struct LaunchdSupervisor {
plist_path: PathBuf,
watchdog_path: PathBuf,
}
impl Default for LaunchdSupervisor {
fn default() -> Self {
Self::new()
}
}
impl LaunchdSupervisor {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
let agents = home.join("Library/LaunchAgents");
Self {
plist_path: agents.join(format!("{LABEL}.plist")),
watchdog_path: agents.join(format!("{WATCHDOG_LABEL}.plist")),
}
}
fn generate_watchdog_plist(&self) -> String {
let marker = super::unit_version_marker();
let interval = super::WATCHDOG_INTERVAL_SECS;
let uid = get_uid();
let plist = shell_quote(&self.plist_path.display().to_string());
let quiet = ">/dev/null 2>&1";
let script = format!(
"/bin/launchctl print gui/{uid}/{LABEL} {quiet} \
|| /bin/launchctl print user/{uid}/{LABEL} {quiet} \
|| /bin/launchctl bootstrap gui/{uid} {plist} {quiet} \
|| /bin/launchctl bootstrap user/{uid} {plist} {quiet}"
)
.replace('&', "&")
.replace('<', "<")
.replace('>', ">");
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">
<!-- {marker} -->
<dict>
<key>Label</key>
<string>{WATCHDOG_LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>{script}</string>
</array>
<key>StartInterval</key>
<integer>{interval}</integer>
<key>StandardOutPath</key>
<string>/dev/null</string>
<key>StandardErrorPath</key>
<string>/dev/null</string>
</dict>
</plist>"#
)
}
fn write(path: &Path, contents: &str) -> Result<(), OlError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("Cannot create LaunchAgents directory: {e}"),
)
})?;
}
std::fs::write(path, contents).map_err(|e| {
OlError::new(
ERR_SUPERVISION_INSTALL_FAILED,
format!("Cannot write {}: {e}", path.display()),
)
})
}
fn install_watchdog(&self) -> Result<(), OlError> {
Self::write(&self.watchdog_path, &self.generate_watchdog_plist())?;
bootout_everywhere(WATCHDOG_LABEL);
bootstrap_with_fallback(&self.watchdog_path)
.map_err(|msg| OlError::new(ERR_SUPERVISION_BOOTSTRAP_FAILED, msg))
}
fn generate_plist(&self, binary_path: &Path) -> String {
let bin = binary_path.display();
let marker = super::unit_version_marker();
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">
<!-- {marker} -->
<dict>
<key>Label</key>
<string>{LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>{bin}</string>
<string>daemon</string>
<string>start</string>
<string>--foreground</string>
</array>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>2</integer>
<key>ExitTimeOut</key>
<integer>20</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/openlatch-stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/openlatch-stderr.log</string>
</dict>
</plist>"#
)
}
}
impl Supervisor for LaunchdSupervisor {
fn kind(&self) -> SupervisorKind {
SupervisorKind::Launchd
}
fn install(&self, binary_path: &Path) -> Result<(), OlError> {
Self::write(&self.plist_path, &self.generate_plist(binary_path))?;
bootstrap_with_fallback(&self.plist_path)
.map_err(|msg| OlError::new(ERR_SUPERVISION_BOOTSTRAP_FAILED, msg))?;
if let Err(e) = self.install_watchdog() {
tracing::warn!(
error = %e.message, code = e.code,
"daemon supervised, but the watchdog agent did not load; it loads at the next login"
);
}
Ok(())
}
fn refresh_in_place(&self, binary_path: &Path) -> Result<(), OlError> {
Self::write(&self.plist_path, &self.generate_plist(binary_path))?;
self.install_watchdog()
}
fn uninstall(&self) -> Result<(), OlError> {
bootout_everywhere(WATCHDOG_LABEL);
let _ = std::fs::remove_file(&self.watchdog_path);
bootout_everywhere(LABEL);
let _ = std::fs::remove_file(&self.plist_path);
Ok(())
}
fn status(&self) -> Result<SupervisorStatus, OlError> {
if !self.plist_path.exists() {
return Ok(SupervisorStatus {
installed: false,
running: false,
unit_current: false,
description: "not installed".into(),
});
}
let unit_current = [&self.plist_path, &self.watchdog_path].iter().all(|p| {
std::fs::read_to_string(p)
.map(|c| super::unit_is_current(&c))
.unwrap_or(false)
});
let output = std::process::Command::new("launchctl")
.args(["list", LABEL])
.output();
match output {
Ok(o) if o.status.success() => Ok(SupervisorStatus {
installed: true,
running: true,
unit_current,
description: if unit_current {
"launchd (KeepAlive and watchdog active)".into()
} else {
"launchd (running an outdated plist — reinstall to get the watchdog)".into()
},
}),
_ => Ok(SupervisorStatus {
installed: true,
running: false,
unit_current,
description: "launchd (plist present, not running)".into(),
}),
}
}
fn start(&self) -> Result<(), OlError> {
bootstrap_with_fallback(&self.plist_path)
.map_err(|msg| OlError::new(ERR_SUPERVISION_CONTROL_FAILED, msg))
}
fn stop(&self) -> Result<(), OlError> {
let uid = get_uid();
let mut last_err = String::new();
for domain in [format!("gui/{uid}"), format!("user/{uid}")] {
let out = std::process::Command::new("launchctl")
.args(["bootout", &format!("{domain}/{LABEL}")])
.output();
match out {
Ok(o) if o.status.success() => return Ok(()),
Ok(o) => last_err = String::from_utf8_lossy(&o.stderr).trim().to_string(),
Err(e) => last_err = format!("cannot run launchctl: {e}"),
}
}
Err(OlError::new(
ERR_SUPERVISION_CONTROL_FAILED,
format!("launchctl bootout failed in gui/ and user/ domains: {last_err}"),
))
}
fn restart(&self) -> Result<(), OlError> {
let uid = get_uid();
for domain in [format!("gui/{uid}"), format!("user/{uid}")] {
let out = std::process::Command::new("launchctl")
.args(["kickstart", "-k", &format!("{domain}/{LABEL}")])
.output();
if out.is_ok_and(|o| o.status.success()) {
return Ok(());
}
}
self.start()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plist_keep_alive_is_unconditional() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_plist(Path::new("/opt/openlatch/bin/openlatch"));
assert!(
plist.contains("<key>KeepAlive</key>\n <true/>"),
"{plist}"
);
assert!(
!plist.contains("<key>SuccessfulExit</key>"),
"the conditional KeepAlive dict is what stopped relaunches:\n{plist}"
);
}
#[test]
fn plist_has_throttle_interval() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_plist(Path::new("/opt/openlatch/bin/openlatch"));
assert!(plist.contains("<key>ThrottleInterval</key>"));
assert!(plist.contains("<integer>2</integer>"), "{plist}");
}
#[test]
fn plist_carries_the_version_marker() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_plist(Path::new("/opt/openlatch/bin/openlatch"));
assert!(super::super::unit_is_current(&plist), "{plist}");
}
#[test]
fn plist_contains_label() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_plist(Path::new("/opt/openlatch/bin/openlatch"));
assert!(plist.contains(LABEL));
}
#[test]
fn watchdog_reloads_the_main_job_every_five_minutes() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_watchdog_plist();
assert!(
plist.contains(&format!("<string>{WATCHDOG_LABEL}</string>")),
"{plist}"
);
assert!(
plist.contains("<key>StartInterval</key>\n <integer>300</integer>"),
"{plist}"
);
assert!(!plist.contains("RunAtLoad"), "{plist}");
assert!(!plist.contains("KeepAlive"), "{plist}");
assert!(super::super::unit_is_current(&plist), "{plist}");
let uid = get_uid();
let main = sup.plist_path.display().to_string();
assert!(
plist.contains(&format!("/bin/launchctl bootstrap gui/{uid} '{main}'")),
"{plist}"
);
assert!(
plist.contains(&format!("/bin/launchctl bootstrap user/{uid} '{main}'")),
"{plist}"
);
assert!(plist.contains("2>&1"), "{plist}");
assert!(!plist.contains("2>&1"), "{plist}");
assert!(!plist.contains("--foreground"), "{plist}");
}
#[test]
fn watchdog_checks_both_domains_before_bootstrapping() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_watchdog_plist();
let uid = get_uid();
let first_bootstrap = plist.find("launchctl bootstrap").expect("bootstraps");
for domain in ["gui", "user"] {
let check = plist
.find(&format!("launchctl print {domain}/{uid}/{LABEL} "))
.unwrap_or_else(|| panic!("no {domain}/ check:\n{plist}"));
assert!(check < first_bootstrap, "{plist}");
}
}
#[test]
fn shell_quote_survives_an_apostrophe() {
assert_eq!(shell_quote("/Users/o'neil/x"), r"'/Users/o'\''neil/x'");
}
#[test]
fn plist_declares_utf8() {
let sup = LaunchdSupervisor::new();
let plist = sup.generate_plist(Path::new("/opt/openlatch/bin/openlatch"));
assert!(plist.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
}
}