use std::fs;
use std::io::Write;
use std::path::Path;
use super::launchctl::{
self, Layout, PLIST_FILE_NAME, layout, not_loaded_detail, parse_print_output,
};
use super::plist::{self, RenderParams};
use super::{
Scope, ServiceBackend, ServiceError, ServiceStatus, current_exe_canonical, require_elevation,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct LaunchdBackend;
impl LaunchdBackend {
pub fn new() -> Self {
Self
}
}
fn read_existing_plist(path: &Path) -> Result<Option<String>, ServiceError> {
match fs::read_to_string(path) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ServiceError::Io(e)),
}
}
fn guard_managed(path: &Path, force: bool, verb: &str) -> Result<(), ServiceError> {
if force {
return Ok(());
}
let Some(existing) = read_existing_plist(path)? else {
return Ok(());
};
if plist::is_managed(&existing) {
return Ok(());
}
Err(ServiceError::Conflict(format!(
"{} was not written by `all-smi service` (it lacks the `{}` marker); refusing to {verb} \
it. Pass --force to proceed, or remove the file yourself first.",
path.display(),
plist::MANAGED_MARKER
)))
}
fn create_log_dir(log_path: &Path) -> Result<(), ServiceError> {
let Some(dir) = log_path.parent() else {
return Ok(());
};
fs::create_dir_all(dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(dir, fs::Permissions::from_mode(0o755))?;
}
Ok(())
}
fn write_plist(path: &Path, contents: &str) -> Result<(), ServiceError> {
let parent = path.parent().ok_or_else(|| {
ServiceError::Io(std::io::Error::other(format!(
"plist path {} has no parent directory",
path.display()
)))
})?;
fs::create_dir_all(parent)?;
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(PLIST_FILE_NAME);
let tmp = parent.join(format!(".{file_name}.tmp"));
{
let mut opts = fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o644);
}
let mut f = opts.open(&tmp)?;
f.write_all(contents.as_bytes())?;
f.sync_all()?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&tmp, fs::Permissions::from_mode(0o644))?;
}
if let Err(e) = fs::rename(&tmp, path) {
let _ = fs::remove_file(&tmp);
return Err(ServiceError::Io(e));
}
Ok(())
}
impl ServiceBackend for LaunchdBackend {
fn install(&self, spec: &super::InstallSpec) -> Result<(), ServiceError> {
if spec.scope == Scope::System {
require_elevation("install")?;
}
let layout = layout(spec.scope)?;
guard_managed(&layout.plist, spec.force, "overwrite")?;
let exec = current_exe_canonical()?;
let rendered = plist::render_plist(&RenderParams {
scope: spec.scope,
exec_path: &exec,
log_path: &layout.log,
service_user: spec.service_user.as_deref(),
})
.map_err(|e| ServiceError::Conflict(e.to_string()))?;
create_log_dir(&layout.log)?;
write_plist(&layout.plist, &rendered)?;
launchctl::run_best_effort(&["enable", &layout.target]);
if spec.start_now {
launchctl::run_best_effort(&["bootout", &layout.target]);
launchctl::bootstrap(&layout)?;
}
Ok(())
}
fn uninstall(&self, scope: Scope) -> Result<(), ServiceError> {
self.remove(scope, false)
}
fn uninstall_forced(&self, scope: Scope) -> Result<(), ServiceError> {
self.remove(scope, true)
}
fn start(&self, scope: Scope) -> Result<(), ServiceError> {
let layout = self.prepare(scope, "start")?;
if launchctl::print_job(&layout.target)?.is_some() {
launchctl::run(&["kickstart", &layout.target])?;
} else {
launchctl::bootstrap(&layout)?;
}
Ok(())
}
fn stop(&self, scope: Scope) -> Result<(), ServiceError> {
let layout = self.prepare(scope, "stop")?;
if launchctl::print_job(&layout.target)?.is_some() {
launchctl::run(&["bootout", &layout.target])?;
}
Ok(())
}
fn restart(&self, scope: Scope) -> Result<(), ServiceError> {
let layout = self.prepare(scope, "restart")?;
if launchctl::print_job(&layout.target)?.is_some() {
launchctl::run(&["kickstart", "-k", &layout.target])?;
} else {
launchctl::bootstrap(&layout)?;
}
Ok(())
}
fn status(&self, scope: Scope) -> Result<ServiceStatus, ServiceError> {
let layout = layout(scope)?;
let installed = layout.plist.exists();
let enabled =
launchctl::query_disabled(&layout.domain).map(|disabled| installed && !disabled);
let output = launchctl::output(&["print", layout.target.as_str()])?;
if !output.status.success() {
return Ok(ServiceStatus {
installed,
enabled,
running: false,
pid: None,
detail: not_loaded_detail(installed, &launchctl::failure_text(&output)),
});
}
let info = parse_print_output(&String::from_utf8_lossy(&output.stdout));
let running = info.running();
Ok(ServiceStatus {
installed,
enabled,
running,
pid: if running { info.pid } else { None },
detail: if info.state.is_empty() {
"loaded".to_string()
} else {
info.state
},
})
}
}
impl LaunchdBackend {
fn prepare(&self, scope: Scope, verb: &'static str) -> Result<Layout, ServiceError> {
if scope == Scope::System {
require_elevation(verb)?;
}
let layout = layout(scope)?;
if !layout.plist.exists() {
return Err(ServiceError::NotInstalled);
}
Ok(layout)
}
fn remove(&self, scope: Scope, force: bool) -> Result<(), ServiceError> {
if scope == Scope::System {
require_elevation("uninstall")?;
}
let layout = layout(scope)?;
if !layout.plist.exists() {
return Err(ServiceError::NotInstalled);
}
guard_managed(&layout.plist, force, "remove")?;
launchctl::run_best_effort(&["bootout", &layout.target]);
fs::remove_file(&layout.plist)?;
Ok(())
}
}
#[cfg(test)]
#[path = "launchd_tests.rs"]
mod tests;