use std::path::PathBuf;
use std::process::Command;
use stackless_core::paths::Paths;
pub const LABEL: &str = "dev.stackless.daemon";
pub fn persistence_status_path(paths: &Paths) -> PathBuf {
paths.persistence_marker()
}
fn plist_path() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from).map(|home| {
home.join("Library/LaunchAgents")
.join(format!("{LABEL}.plist"))
})
}
fn plist_xml(exe: &str) -> String {
let exe = xml_escape(exe);
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>daemon</string>
<string>run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
</dict>
</plist>
"#
)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
pub fn ensure_registered(paths: &Paths) {
let outcome = register();
let text = match &outcome {
Ok(()) => "registered".to_owned(),
Err(why) => why.clone(),
};
let marker = persistence_status_path(paths);
if let Some(dir) = marker.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = std::fs::write(marker, text);
}
fn register() -> Result<(), String> {
let exe = std::env::current_exe()
.map(|p| p.display().to_string())
.map_err(|err| format!("cannot resolve the stackless binary path: {err}"))?;
let plist =
plist_path().ok_or_else(|| "HOME is unset; cannot locate LaunchAgents".to_owned())?;
if let Some(dir) = plist.parent() {
std::fs::create_dir_all(dir)
.map_err(|err| format!("cannot create {}: {err}", dir.display()))?;
}
let want = plist_xml(&exe);
let current = std::fs::read_to_string(&plist).ok();
if current.as_deref() != Some(want.as_str()) {
std::fs::write(&plist, &want)
.map_err(|err| format!("cannot write {}: {err}", plist.display()))?;
}
let uid = nix_getuid();
let domain = format!("gui/{uid}");
if service_loaded(&domain) {
return Ok(());
}
let output = bootstrap(&domain, &plist)?;
if output.status.success() || service_loaded(&domain) {
return Ok(());
}
if service_disabled(&domain) {
let _ = Command::new("launchctl")
.args(["enable", &format!("{domain}/{LABEL}")])
.output();
let retry = bootstrap(&domain, &plist)?;
if retry.status.success() || service_loaded(&domain) {
return Ok(());
}
let stderr = String::from_utf8_lossy(&retry.stderr);
return Err(format!(
"launchctl bootstrap {domain} failed after enabling the disabled service {LABEL}: {}",
stderr.trim()
));
}
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!(
"launchctl bootstrap {domain} failed: {}",
stderr.trim()
))
}
fn bootstrap(domain: &str, plist: &PathBuf) -> Result<std::process::Output, String> {
Command::new("launchctl")
.args(["bootstrap", domain])
.arg(plist)
.output()
.map_err(|err| format!("cannot run launchctl: {err}"))
}
fn service_disabled(domain: &str) -> bool {
Command::new("launchctl")
.args(["print-disabled", domain])
.output()
.ok()
.map(|out| parse_disabled(&String::from_utf8_lossy(&out.stdout)))
.unwrap_or(false)
}
fn parse_disabled(print_disabled: &str) -> bool {
print_disabled.lines().any(|line| {
let Some(rest) = line.trim().strip_prefix(&format!("\"{LABEL}\"")) else {
return false;
};
let Some(value) = rest.trim().strip_prefix("=>") else {
return false;
};
matches!(value.trim().trim_end_matches(';'), "disabled" | "true")
})
}
fn service_loaded(domain: &str) -> bool {
Command::new("launchctl")
.arg("print")
.arg(format!("{domain}/{LABEL}"))
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
pub fn kickstart_if_supervised() -> bool {
let Ok(exe) = std::env::current_exe() else {
return false;
};
let exe = exe.display().to_string();
let Some(plist) = plist_path() else {
return false;
};
match plist_program_binary(&plist) {
Some(named) if named == exe => {}
_ => return false,
}
let domain = format!("gui/{}", nix_getuid());
if !service_loaded(&domain) {
return false;
}
Command::new("launchctl")
.args(["kickstart", &format!("{domain}/{LABEL}")])
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
fn plist_program_binary(plist: &PathBuf) -> Option<String> {
let xml = std::fs::read_to_string(plist).ok()?;
let after_key = xml.split("<key>ProgramArguments</key>").nth(1)?;
let array = after_key.split("<array>").nth(1)?;
let array = array.split("</array>").next()?;
let open = array.find("<string>")? + "<string>".len();
let close = array[open..].find("</string>")? + open;
Some(xml_unescape(array[open..close].trim()))
}
fn xml_unescape(s: &str) -> String {
s.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
}
fn nix_getuid() -> u32 {
Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0)
}
pub fn service_registered() -> bool {
service_loaded(&format!("gui/{}", nix_getuid()))
}
pub fn degradation_warning(paths: &Paths) -> Option<String> {
let status = std::fs::read_to_string(persistence_status_path(paths)).ok()?;
let status = status.trim();
if status.is_empty() || status == "registered" {
return None;
}
Some(format!(
"leases enforced only while the daemon happens to be running: {status}"
))
}
#[cfg(test)]
mod tests {
use super::parse_disabled;
#[test]
fn disable_record_current_macos() {
let out = "disabled services = {\n\t\t\"com.example.other\" => enabled\n\t\t\"dev.stackless.daemon\" => disabled\n\t}";
assert!(parse_disabled(out));
}
#[test]
fn disable_record_legacy_bool() {
let out = "\t\"dev.stackless.daemon\" => true;";
assert!(parse_disabled(out));
}
#[test]
fn enabled_or_absent_is_not_disabled() {
assert!(!parse_disabled("\t\"dev.stackless.daemon\" => enabled"));
assert!(!parse_disabled("\t\"com.example.other\" => disabled"));
assert!(!parse_disabled(""));
}
}