feat(daemon): launchd user-agent install with KeepAlive

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:14:00 +07:00
parent f49fc41cd1
commit 18ac699951
+67 -3
View File
@@ -1,4 +1,68 @@
use anyhow::Result;
pub fn install_agent() -> Result<()> {
anyhow::bail!("install-agent implemented in Task 16")
use anyhow::{Context, Result};
use std::path::PathBuf;
const LABEL: &str = "xyz.spacesh.daemon";
fn plist_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("no home")?;
Ok(home.join("Library").join("LaunchAgents").join(format!("{LABEL}.plist")))
}
/// Render the launchd plist. `run_at_load` defaults to false in this slice.
pub fn render_plist(exe: &str, run_at_load: bool) -> String {
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>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<{run_at_load}/>
</dict>
</plist>
"#,
run_at_load = if run_at_load { "true" } else { "false" }
)
}
pub fn install_agent() -> Result<()> {
let exe = std::env::current_exe()?;
let plist = render_plist(&exe.to_string_lossy(), false);
let path = plist_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, plist)?;
// Load it (best-effort; ignore "already loaded").
let _ = std::process::Command::new("launchctl")
.arg("load")
.arg(&path)
.status();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plist_has_label_keepalive_and_exe() {
let p = render_plist("/usr/local/bin/spaceshd", false);
assert!(p.contains("xyz.spacesh.daemon"));
assert!(p.contains("/usr/local/bin/spaceshd"));
assert!(p.contains("<key>KeepAlive</key>\n <true/>"));
assert!(p.contains("<key>RunAtLoad</key>\n <false/>"));
}
#[test]
fn run_at_load_toggles() {
assert!(render_plist("x", true).contains("<key>RunAtLoad</key>\n <true/>"));
}
}