use super::install::{
elevated_command, ensure_config_preserved, is_privileged, manager_error_is_permission,
write_atomic_text, InstallError,
};
use super::method::{
launchd_label, standard_launchd_binary, standard_launchd_config, standard_launchd_plist_path,
StartupMethodArg,
};
use super::process::{run_bounded_command, MANAGER_COMMAND_TIMEOUT};
use std::fs;
use std::io::{self};
use std::path::{Path, PathBuf};
pub fn launchd_plist_content() -> String {
const TEMPLATE: &str = 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>com.eggstack.greggd</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/greggd</string>
<string>run</string>
<string>--config</string>
<string>/Library/Application Support/gregg/greggd.toml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>/var/log/greggd.log</string>
<key>StandardErrorPath</key>
<string>/var/log/greggd.log</string>
<key>HardResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>1024</integer>
</dict>
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>1024</integer>
</dict>
</dict>
</plist>
"#;
TEMPLATE.to_string()
}
#[allow(dead_code)]
pub(crate) fn launchd_plist_exists() -> bool {
standard_launchd_plist_path().exists()
}
#[allow(dead_code)]
pub(crate) fn launchd_is_loaded() -> bool {
if matches!(
run_bounded_command(
"launchctl",
&["print", &format!("system/{}", launchd_label())],
MANAGER_COMMAND_TIMEOUT,
),
Ok(output) if output.status.success()
) {
return true;
}
if let Ok(output) = run_bounded_command("launchctl", &["list"], MANAGER_COMMAND_TIMEOUT) {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains(launchd_label()) {
return true;
}
}
}
false
}
#[allow(clippy::too_many_lines)]
pub fn install_launchd(exe: &Path, _config_path: &Path) -> Result<(), InstallError> {
if std::env::consts::OS != "macos" && std::env::consts::OS != "darwin" {
return Err(InstallError::LaunchdNotDetected {
message: "launchd is only available on macOS".into(),
});
}
let bin_path = standard_launchd_binary();
if !bin_path.exists() {
return Err(InstallError::BinaryMissing { path: bin_path });
}
if !is_privileged() {
let cmd = elevated_command(exe, StartupMethodArg::Launchd);
return Err(InstallError::Permission {
message: format!("permission denied: rerun as root: {cmd}"),
});
}
let cfg_path = standard_launchd_config();
if let Some(parent) = cfg_path.parent() {
fs::create_dir_all(parent).map_err(|e| InstallError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
ensure_config_preserved(&cfg_path).map_err(|e| InstallError::Io {
path: cfg_path.clone(),
source: e,
})?;
let plist_content = launchd_plist_content();
let plist_path = standard_launchd_plist_path();
if let Some(parent) = plist_path.parent() {
fs::create_dir_all(parent).map_err(|e| InstallError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
write_atomic_text(&plist_path, &plist_content).map_err(|e| {
if e.kind() == io::ErrorKind::PermissionDenied {
InstallError::Permission {
message: format!(
"permission denied writing {}: rerun as root: {}",
plist_path.display(),
elevated_command(exe, StartupMethodArg::Launchd)
),
}
} else {
InstallError::Io {
path: plist_path.clone(),
source: e,
}
}
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&plist_path, fs::Permissions::from_mode(0o644));
}
let log_path = Path::new("/var/log/greggd.log");
let _ = fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(log_path, fs::Permissions::from_mode(0o644));
}
let label = launchd_label();
let loaded = launchd_is_loaded();
if loaded {
let _ = run_bounded_command(
"launchctl",
&["bootout", &format!("system/{label}")],
MANAGER_COMMAND_TIMEOUT,
);
let _ = run_bounded_command(
"launchctl",
&["bootout", "system", &plist_path.to_string_lossy()],
MANAGER_COMMAND_TIMEOUT,
);
}
let bootstrap_args = ["bootstrap", "system", &plist_path.to_string_lossy()];
let bootstrap = run_bounded_command("launchctl", &bootstrap_args, MANAGER_COMMAND_TIMEOUT)
.map_err(|source| InstallError::Io {
path: PathBuf::from("launchctl bootstrap"),
source,
})?;
if !bootstrap.status.success() && !loaded {
let detail = String::from_utf8_lossy(&bootstrap.stderr)
.trim()
.to_string();
if manager_error_is_permission(&detail) {
return Err(InstallError::Permission {
message: format!(
"launchctl bootstrap failed: {detail}; rerun as root: {}",
elevated_command(exe, StartupMethodArg::Launchd)
),
});
}
return Err(InstallError::Other(format!(
"launchctl bootstrap failed with status {:?}: {detail}",
bootstrap.status.code()
)));
}
if loaded {
let kick = run_bounded_command(
"launchctl",
&["kickstart", "-k", &format!("system/{label}")],
MANAGER_COMMAND_TIMEOUT,
)
.map_err(|source| InstallError::Io {
path: PathBuf::from("launchctl kickstart"),
source,
})?;
if !kick.status.success() {
let detail = String::from_utf8_lossy(&kick.stderr).trim().to_string();
if manager_error_is_permission(&detail) {
return Err(InstallError::Permission {
message: format!(
"launchctl kickstart failed: {detail}; rerun as root: {}",
elevated_command(exe, StartupMethodArg::Launchd)
),
});
}
return Err(InstallError::Other(format!(
"launchctl kickstart failed with status {:?}: {detail}",
kick.status.code()
)));
}
}
println!("greggd launchd service installed: {}", plist_path.display());
println!("config: {}", cfg_path.display());
println!("logs: log show --predicate 'process == \"greggd\"' --last 5m");
Ok(())
}
pub(crate) fn restart_launchd(exe: &Path) -> Result<(), InstallError> {
let label = launchd_label();
let args = ["kickstart", "-k", &format!("system/{label}")];
match run_bounded_command("launchctl", &args, MANAGER_COMMAND_TIMEOUT) {
Ok(output) if output.status.success() => {
println!("greggd restarted via launchd (kickstart -k system/{label})");
Ok(())
}
Ok(output) if manager_error_is_permission(&String::from_utf8_lossy(&output.stderr)) => {
Err(InstallError::Permission {
message: format!(
"permission denied: rerun as root: sudo launchctl kickstart -k system/{label} (exe: {})",
exe.display()
),
})
}
Ok(output) => Err(InstallError::Io {
path: PathBuf::from(format!("launchctl kickstart -k system/{label}")),
source: io::Error::other(format!(
"launchctl kickstart failed with status {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim()
)),
}),
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => Err(InstallError::Permission {
message: format!(
"permission denied: rerun as root: sudo launchctl kickstart -k system/{label} (exe: {})",
exe.display()
),
}),
Err(e) => Err(InstallError::Io {
path: PathBuf::from(format!("launchctl kickstart -k system/{label}")),
source: e,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn launchd_plist_content_contains_label() {
let content = launchd_plist_content();
assert!(content.contains("com.eggstack.greggd"));
assert!(content.contains("/usr/local/bin/greggd"));
assert!(content.contains("KeepAlive"));
}
#[test]
fn embedded_launchd_plist_matches_packaging_file_when_present() {
let packaging = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../packaging/launchd/com.eggstack.greggd.plist");
if let Ok(file) = std::fs::read_to_string(&packaging) {
let mut file_norm = file.replace("\r\n", "\n");
if !file_norm.ends_with('\n') {
file_norm.push('\n');
}
assert_eq!(
launchd_plist_content(),
file_norm,
"embedded launchd plist must stay synchronized with packaging/launchd/com.eggstack.greggd.plist"
);
}
}
}