use super::*;
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_plist_path(config_path: &Path) -> PathBuf {
PathBuf::from(format!(
"/Library/LaunchDaemons/{}.plist",
macos_service_label(config_path)
))
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_binary_path(config_path: &Path) -> PathBuf {
PathBuf::from(format!(
"/Library/PrivilegedHelperTools/{}",
macos_service_label(config_path)
))
}
#[cfg(target_os = "macos")]
pub(super) fn macos_service_executable_path(plist_path: &Path) -> Option<PathBuf> {
let plist = fs::read_to_string(plist_path).ok()?;
macos_service_executable_path_from_plist_contents(&plist).map(PathBuf::from)
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_executable_path_from_plist_contents(plist: &str) -> Option<String> {
let after_program_args = plist.split_once("<key>ProgramArguments</key>")?.1;
let after_first_string = after_program_args.split_once("<string>")?.1;
let executable = after_first_string.split_once("</string>")?.0.trim();
if executable.is_empty() {
None
} else {
Some(xml_unescape(executable))
}
}
#[cfg(target_os = "macos")]
fn macos_service_target(config_path: &Path) -> String {
format!("system/{}", macos_service_label(config_path))
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_label(config_path: &Path) -> String {
if macos_service_uses_default_label(config_path) {
return MACOS_SERVICE_LABEL.to_string();
}
format!(
"{MACOS_SERVICE_LABEL}.{}",
macos_service_config_suffix(config_path)
)
}
#[cfg(any(target_os = "macos", test))]
fn macos_service_uses_default_label(config_path: &Path) -> bool {
canonicalize_lossy(config_path) == canonicalize_lossy(&default_config_path())
}
#[cfg(any(target_os = "macos", test))]
fn canonicalize_lossy(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
#[cfg(any(target_os = "macos", test))]
fn macos_service_config_suffix(config_path: &Path) -> String {
let canonical = canonicalize_lossy(config_path);
let mut hasher = std::collections::hash_map::DefaultHasher::new();
canonical.as_os_str().hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
#[cfg(target_os = "macos")]
pub(super) fn macos_install_service(
executable: &Path,
config_path: &Path,
iface: &str,
mesh_refresh_interval_secs: u64,
log_path: &Path,
force: bool,
) -> Result<()> {
let plist_path = macos_service_plist_path(config_path);
let service_label = macos_service_label(config_path);
if plist_path.exists() && !force {
println!(
"service already installed at {} (pass --force to reinstall)",
plist_path.display()
);
return Ok(());
}
macos_service_bootout(config_path, true)?;
stop_existing_daemons_before_service_install(config_path)?;
let service_executable = macos_service_binary_path(config_path);
crate::service_management::install_service_executable_copy(executable, &service_executable)?;
let plist = macos_service_plist_content(
&service_label,
&service_executable,
config_path,
iface,
mesh_refresh_interval_secs,
log_path,
);
if let Some(parent) = plist_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let temp = plist_path.with_extension(format!("tmp-{}", std::process::id()));
fs::write(&temp, plist).with_context(|| format!("failed to write {}", temp.display()))?;
#[cfg(unix)]
fs::set_permissions(&temp, fs::Permissions::from_mode(0o644))
.with_context(|| format!("failed to chmod {}", temp.display()))?;
fs::rename(&temp, &plist_path).with_context(|| {
format!(
"failed to move {} into {}",
temp.display(),
plist_path.display()
)
})?;
macos_activate_service(config_path, &plist_path)?;
println!("installed system service: {}", plist_path.display());
println!("label: {service_label}");
Ok(())
}
#[cfg(target_os = "macos")]
pub(super) fn macos_uninstall_service(config_path: &Path) -> Result<()> {
macos_service_bootout(config_path, true)?;
macos_service_disable(config_path, true)?;
let plist_path = macos_service_plist_path(config_path);
if plist_path.exists() {
fs::remove_file(&plist_path)
.with_context(|| format!("failed to remove {}", plist_path.display()))?;
println!("removed system service plist: {}", plist_path.display());
} else {
println!("system service plist not found: {}", plist_path.display());
}
Ok(())
}
#[cfg(target_os = "macos")]
pub(super) fn macos_enable_service(config_path: &Path) -> Result<()> {
let plist_path = macos_service_plist_path(config_path);
let service_label = macos_service_label(config_path);
if !plist_path.exists() {
return Err(anyhow!(
"system service plist not found: {}",
plist_path.display()
));
}
macos_activate_service(config_path, &plist_path)?;
println!("enabled system service: {}", plist_path.display());
println!("label: {service_label}");
Ok(())
}
#[cfg(target_os = "macos")]
pub(super) fn macos_disable_service(config_path: &Path) -> Result<()> {
let plist_path = macos_service_plist_path(config_path);
let service_label = macos_service_label(config_path);
if !plist_path.exists() {
return Err(anyhow!(
"system service plist not found: {}",
plist_path.display()
));
}
macos_service_bootout(config_path, true)?;
macos_service_disable(config_path, false)?;
println!("disabled system service: {}", plist_path.display());
println!("label: {service_label}");
Ok(())
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_plist_content(
service_label: &str,
executable: &Path,
config_path: &Path,
iface: &str,
mesh_refresh_interval_secs: u64,
log_path: &Path,
) -> String {
let exec = xml_escape(&executable.display().to_string());
let config = xml_escape(&config_path.display().to_string());
let iface = xml_escape(iface);
let interval = mesh_refresh_interval_secs.to_string();
let log = xml_escape(&log_path.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>{service_label}</string>
<key>ProgramArguments</key>
<array>
<string>{exec}</string>
<string>daemon</string>
<string>--service</string>
<string>--config</string>
<string>{config}</string>
<string>--iface</string>
<string>{iface}</string>
<string>--mesh-refresh-interval-secs</string>
<string>{interval}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ProcessType</key>
<string>Background</string>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
</dict>
</plist>
"#
)
}
#[cfg(target_os = "macos")]
fn macos_service_bootstrap(_config_path: &Path, plist_path: &Path) -> Result<()> {
let plist = plist_path
.to_str()
.ok_or_else(|| anyhow!("plist path is not valid UTF-8"))?;
run_launchctl_checked(&["bootstrap", "system", plist], "bootstrap service")
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_activation_commands(
config_path: &Path,
plist_path: &Path,
) -> Vec<Vec<String>> {
let target = format!("system/{}", macos_service_label(config_path));
let plist = plist_path.display().to_string();
vec![
vec!["enable".to_string(), target.clone()],
vec!["bootout".to_string(), target.clone()],
vec!["bootstrap".to_string(), "system".to_string(), plist],
vec!["kickstart".to_string(), "-k".to_string(), target],
]
}
#[cfg(target_os = "macos")]
fn macos_activate_service(config_path: &Path, plist_path: &Path) -> Result<()> {
for args in macos_service_activation_commands(config_path, plist_path) {
match args.first().map(String::as_str) {
Some("enable") => macos_service_enable(config_path)?,
Some("bootout") => macos_service_bootout(config_path, true)?,
Some("bootstrap") => macos_service_bootstrap(config_path, plist_path)?,
Some("kickstart") => macos_service_kickstart(config_path)?,
_ => return Err(anyhow!("unsupported launchctl activation command")),
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn macos_service_enable(config_path: &Path) -> Result<()> {
let target = macos_service_target(config_path);
run_launchctl_checked(&["enable", target.as_str()], "enable service")
}
#[cfg(target_os = "macos")]
fn macos_service_disable(config_path: &Path, ignore_missing: bool) -> Result<()> {
let target = macos_service_target(config_path);
run_launchctl_allow_missing(
&["disable", target.as_str()],
"disable service",
ignore_missing,
)
}
#[cfg(target_os = "macos")]
fn macos_service_kickstart(config_path: &Path) -> Result<()> {
let target = macos_service_target(config_path);
run_launchctl_checked(&["kickstart", "-k", target.as_str()], "kickstart service")
}
#[cfg(target_os = "macos")]
fn macos_service_bootout(config_path: &Path, ignore_missing: bool) -> Result<()> {
let target = macos_service_target(config_path);
run_launchctl_allow_missing(
&["bootout", target.as_str()],
"bootout service",
ignore_missing,
)
}
#[cfg(target_os = "macos")]
pub(super) fn macos_service_print(config_path: &Path) -> Result<String> {
let target = macos_service_target(config_path);
let output = run_launchctl_raw(&["print", target.as_str()], "print service")?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
#[cfg(target_os = "macos")]
pub(super) fn macos_service_print_is_running(print_output: &str) -> bool {
print_output
.lines()
.map(str::trim)
.any(|line| line == "state = running")
}
#[cfg(target_os = "macos")]
pub(super) fn macos_service_print_pid(print_output: &str) -> Option<u32> {
for line in print_output.lines().map(str::trim) {
if let Some(value) = line.strip_prefix("pid = ")
&& let Ok(pid) = value.trim().parse::<u32>()
{
return Some(pid);
}
}
None
}
#[cfg(target_os = "macos")]
pub(super) fn macos_service_disabled(config_path: &Path) -> Result<bool> {
let output = run_launchctl_raw(&["print-disabled", "system"], "print disabled services")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
return Err(anyhow!(
"launchctl print disabled services failed\nstdout: {}\nstderr: {}",
stdout.trim(),
stderr.trim()
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(macos_service_disabled_from_print_disabled_output(
&stdout,
&macos_service_label(config_path),
))
}
#[cfg(any(target_os = "macos", test))]
pub(super) fn macos_service_disabled_from_print_disabled_output(output: &str, label: &str) -> bool {
for line in output.lines().map(str::trim) {
let Some((entry_label, state)) = line.split_once("=>") else {
continue;
};
if entry_label.trim().trim_matches('"') != label {
continue;
}
return state.trim().trim_end_matches(',') == "disabled";
}
false
}
#[cfg(target_os = "macos")]
fn run_launchctl_checked(args: &[&str], context: &str) -> Result<()> {
let output = run_launchctl_raw(args, context)?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
Err(anyhow!(
"launchctl {context} failed\nstdout: {}\nstderr: {}",
stdout.trim(),
stderr.trim()
))
}
#[cfg(target_os = "macos")]
fn run_launchctl_allow_missing(args: &[&str], context: &str, ignore_missing: bool) -> Result<()> {
let output = run_launchctl_raw(args, context)?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let details = format!("stdout: {}\nstderr: {}", stdout.trim(), stderr.trim());
if ignore_missing && launchctl_missing_service_message(&details) {
return Ok(());
}
Err(anyhow!(
"launchctl {context} failed\nstdout: {}\nstderr: {}",
stdout.trim(),
stderr.trim()
))
}
#[cfg(target_os = "macos")]
fn run_launchctl_raw(args: &[&str], context: &str) -> Result<std::process::Output> {
ProcessCommand::new("launchctl")
.args(args)
.output()
.with_context(|| format!("failed to launchctl {context}"))
}
#[cfg(target_os = "macos")]
fn launchctl_missing_service_message(details: &str) -> bool {
let lower = details.to_ascii_lowercase();
lower.contains("could not find service")
|| lower.contains("service is disabled")
|| lower.contains("no such process")
|| lower.contains("not found")
}