use std::path::{Path, PathBuf};
use super::common::{daemon_log, moadim_exe, run};
const LAUNCHD_LABEL: &str = "io.moadim.daemon";
pub(super) fn launchctl_bin() -> String {
if let Ok(bin) = std::env::var("MOADIM_LAUNCHCTL_BIN") {
return bin;
}
#[cfg(test)]
let fallback = "/nonexistent/moadim-test-launchctl-guard".to_string();
#[cfg(not(test))]
let fallback = "launchctl".to_string();
fallback
}
pub(super) fn xml_escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub(super) fn plist_path() -> anyhow::Result<PathBuf> {
plist_path_from_home(crate::paths::home())
}
pub(super) fn plist_path_from_home(home: Option<PathBuf>) -> anyhow::Result<PathBuf> {
let home = home.ok_or_else(|| anyhow::anyhow!("could not determine the home directory"))?;
Ok(home
.join("Library/LaunchAgents")
.join(format!("{LAUNCHD_LABEL}.plist")))
}
fn agent_path(home: &Path) -> String {
format!(
"/opt/homebrew/bin:/usr/local/bin:{home}/.cargo/bin:{home}/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
home = home.display(),
)
}
pub(super) fn render_plist(exe: &Path, log: &Path, home: &Path) -> 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>
<string>--interactive</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>{path}</string>
</dict>
</dict>
</plist>
"#,
label = LAUNCHD_LABEL,
exe = xml_escape(&exe.display().to_string()),
log = xml_escape(&log.display().to_string()),
path = xml_escape(&agent_path(home)),
)
}
pub(super) fn write_plist(plist: &Path, exe: &Path, log: &Path, home: &Path) -> anyhow::Result<()> {
if let Some(dir) = plist.parent() {
std::fs::create_dir_all(dir)?;
}
if let Some(dir) = log.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(plist, render_plist(exe, log, home))?;
Ok(())
}
fn reload_agent(plist: &Path) -> anyhow::Result<()> {
let plist_arg = plist.display().to_string();
let launchctl = launchctl_bin();
let _ = run(&launchctl, &["unload", &plist_arg]);
run(&launchctl, &["load", "-w", &plist_arg])
}
fn report_installed(plist: &Path, log: &Path) {
println!("moadim installed as a launchd agent ({LAUNCHD_LABEL})");
println!(" plist {}", plist.display());
println!(" logs {}", log.display());
println!(" status launchctl list | grep {LAUNCHD_LABEL}");
}
pub fn install() -> anyhow::Result<()> {
let exe = moadim_exe().expect("current executable path is always available");
let log = daemon_log();
let home =
crate::paths::home().expect("home directory must be known to install the launchd agent");
let plist = plist_path().expect("home directory must be known to install the launchd agent");
write_plist(&plist, &exe, &log, &home)?;
reload_agent(&plist)?;
report_installed(&plist, &log);
request_automation_permission();
Ok(())
}
fn request_automation_permission() {
println!(
" hint if macOS asks \"moadim would like to administer your computer\", click OK — \
granting it here prevents background interruptions"
);
let _ = std::process::Command::new("osascript")
.args([
"-e",
"tell application \"System Events\" to get name of every process",
])
.output();
}
pub fn uninstall() -> anyhow::Result<()> {
let plist = plist_path().expect("home directory must be known to uninstall the launchd agent");
if plist.exists() {
let plist_arg = plist.display().to_string();
let _ = run(&launchctl_bin(), &["unload", "-w", &plist_arg]);
std::fs::remove_file(&plist)?;
println!("moadim launchd agent removed ({})", plist.display());
} else {
println!(
"moadim launchd agent is not installed (no plist at {})",
plist.display()
);
}
Ok(())
}