use std::path::PathBuf;
use std::process::Command;
use anyhow::Result;
use framework_system::CommandExt;
use crate::config::StartupConfig;
use crate::directory::{self, SYSTEM_PLIST_DIRECTORY};
use crate::file;
use crate::platform::Platform;
use crate::scope::Scope;
const PLIST_SUFFIX: &str = ".plist";
pub(crate) struct MacosStartup;
impl Platform for MacosStartup {
fn exists(&self, config: &StartupConfig, scope: Scope) -> Result<bool> {
Ok(plist_path(config, scope)?.exists())
}
fn is_run(&self, config: &StartupConfig, _scope: Scope) -> Result<bool> {
let output = Command::new("launchctl")
.args(["list", &config.name])
.command_stdout();
Ok(output.is_some_and(|output| output.contains("\"PID\"")))
}
fn install(&self, config: &StartupConfig, scope: Scope) -> Result<()> {
let plist = plist_path(config, scope)?;
let plist_text = plist.to_string_lossy().to_string();
file::write_file(&plist, &plist_content(config))?;
Command::new("launchctl")
.args(["load", "-w", plist_text.as_str()])
.command_run()
}
fn uninstall(&self, config: &StartupConfig, scope: Scope) -> Result<()> {
let _ = Command::new("launchctl")
.args(["remove", &config.name])
.command_run();
file::remove_file(&plist_path(config, scope)?)
}
fn start(&self, config: &StartupConfig, _scope: Scope) -> Result<()> {
Command::new("launchctl")
.args(["start", &config.name])
.command_run()
}
fn stop(&self, config: &StartupConfig, _scope: Scope) -> Result<()> {
Command::new("launchctl")
.args(["stop", &config.name])
.command_run()
}
}
fn plist_path(config: &StartupConfig, scope: Scope) -> Result<PathBuf> {
let root = directory::directory(
SYSTEM_PLIST_DIRECTORY,
directory::user_plist_directory,
scope,
)?;
Ok(root.join(format!("{}{PLIST_SUFFIX}", config.name)))
}
fn plist_content(config: &StartupConfig) -> String {
let working_directory = config
.working_directory
.as_ref()
.map(|path| {
format!(
"\t<key>WorkingDirectory</key>\n\t<string>{}</string>\n",
escape(&path.to_string_lossy())
)
})
.unwrap_or_default();
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
\t<key>Label</key>\n\
\t<string>{label}</string>\n\
\t<key>ProgramArguments</key>\n\
\t<array>\n\
{arguments}\
\t</array>\n\
{working_directory}\
\t<key>RunAtLoad</key>\n\
\t<true/>\n\
</dict>\n\
</plist>\n",
label = escape(&config.name),
arguments = arguments(config),
)
}
fn arguments(config: &StartupConfig) -> String {
let mut arguments = format!(
"\t\t<string>{}</string>\n",
escape(&config.program.to_string_lossy())
);
for arg in &config.args {
arguments.push_str(&format!("\t\t<string>{}</string>\n", escape(arg)));
}
arguments
}
fn escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}