use std::path::PathBuf;
pub const LABEL: &str = "com.telepi";
pub fn build_plist(
telepi_bin: &str,
config_path: &str,
log_dir: &PathBuf,
) -> String {
let out_log = log_dir.join("telepi.out.log").display().to_string();
let err_log = log_dir.join("telepi.err.log").display().to_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>{bin}</string>
<string>start</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>TELEPI_CONFIG</key>
<string>{config}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>{out_log}</string>
<key>StandardErrorPath</key>
<string>{err_log}</string>
</dict>
</plist>
"#,
label = LABEL,
bin = telepi_bin,
config = config_path,
out_log = out_log,
err_log = err_log,
)
}
pub fn installed_plist_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join("Library")
.join("LaunchAgents")
.join(format!("{LABEL}.plist"))
}
pub fn install(telepi_bin: &str, config_path: &str, log_dir: &PathBuf) -> Result<(), String> {
let plist_content = build_plist(telepi_bin, config_path, log_dir);
let plist_path = installed_plist_path();
if let Some(parent) = plist_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("failed to create LaunchAgents dir: {e}"))?;
}
std::fs::write(&plist_path, plist_content)
.map_err(|e| format!("failed to write plist: {e}"))?;
let status = std::process::Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist_path)
.status()
.map_err(|e| format!("failed to run launchctl: {e}"))?;
if !status.success() {
return Err("launchctl load failed".into());
}
Ok(())
}
pub fn uninstall() -> Result<(), String> {
let plist_path = installed_plist_path();
if plist_path.exists() {
let _ = std::process::Command::new("launchctl")
.args(["unload", "-w"])
.arg(&plist_path)
.status();
std::fs::remove_file(&plist_path)
.map_err(|e| format!("failed to remove plist: {e}"))?;
}
Ok(())
}
pub fn is_running() -> bool {
let output = std::process::Command::new("launchctl")
.args(["list"])
.arg(LABEL)
.output();
match output {
Ok(out) => out.status.success(),
Err(_) => false,
}
}
pub fn stop() -> Result<(), String> {
let plist_path = installed_plist_path();
if !plist_path.exists() {
return Err("service not installed".into());
}
let status = std::process::Command::new("launchctl")
.args(["unload", "-w"])
.arg(&plist_path)
.status()
.map_err(|e| format!("failed to run launchctl: {e}"))?;
if !status.success() {
return Err("launchctl unload failed".into());
}
Ok(())
}
pub fn start() -> Result<(), String> {
let plist_path = installed_plist_path();
if !plist_path.exists() {
return Err("service not installed — run `telepi gateway start` first".into());
}
let status = std::process::Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist_path)
.status()
.map_err(|e| format!("failed to run launchctl: {e}"))?;
if !status.success() {
return Err("launchctl load failed".into());
}
Ok(())
}