use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use uni_error::{ResultContext as _, UniError, UniKind as _, UniResult};
use crate::manager::{ServiceManager, ServiceStatus};
use crate::unix_util::{SERVICE_PERMS, write_file};
use crate::{ServiceCapabilities, ServiceErrKind, ServiceSpec, util};
const GLOBAL_PATH: &str = "/Library/LaunchDaemons";
const LAUNCH_CTL: &str = "launchctl";
pub(crate) fn make_service_manager(
name: OsString,
prefix: OsString,
user: bool,
) -> UniResult<Box<dyn ServiceManager>, ServiceErrKind> {
let prefix_str = util::os_string_to_string(prefix.clone()).kind_context(
ServiceErrKind::InvalidNameOrPrefix,
"The prefix must be valid UTF-8",
)?;
if prefix_str.is_empty() || prefix_str.len() < 2 || !prefix_str.ends_with('.') {
return Err(UniError::from_kind_context(
ServiceErrKind::InvalidNameOrPrefix,
"The service prefix must be a valid reverse domain name and end with a dot",
));
}
LaunchDServiceManager::new(name, prefix, user)
.map(|mgr| Box::new(mgr) as Box<dyn ServiceManager>)
}
pub fn capabilities() -> ServiceCapabilities {
ServiceCapabilities::SUPPORTS_CUSTOM_GROUP
| ServiceCapabilities::RESTART_ON_FAILURE_REQUIRES_AUTOSTART
| ServiceCapabilities::USES_NAME_PREFIX
| ServiceCapabilities::STARTS_IMMEDIATELY_WITH_AUTOSTART
}
struct LaunchDServiceManager {
name: OsString,
prefix: OsString,
user: bool,
}
impl LaunchDServiceManager {
fn new(name: OsString, prefix: OsString, user: bool) -> UniResult<Self, ServiceErrKind> {
if Self::launch_ctl("list", vec![]).is_ok() {
Ok(Self { name, prefix, user })
} else {
Err(UniError::from_kind_context(
ServiceErrKind::ServiceManagementNotAvailable,
"launchd is not available on this system",
))
}
}
pub fn launch_ctl(sub_cmd: &str, args: Vec<&OsStr>) -> UniResult<String, ServiceErrKind> {
let mut command = Command::new(LAUNCH_CTL);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if !sub_cmd.is_empty() {
command.arg(sub_cmd);
}
for arg in args {
command.arg(arg);
}
tracing::debug!("Executing command: {:?}", command);
let output = command.output().kind(ServiceErrKind::IoError)?;
if output.status.success() {
Ok(String::from_utf8(output.stdout).kind(ServiceErrKind::BadUtf8)?)
} else {
let msg = String::from_utf8(output.stderr).kind(ServiceErrKind::BadUtf8)?;
Err(ServiceErrKind::BadExitStatus(output.status.code(), msg).into_error())
}
}
fn path(&self) -> UniResult<PathBuf, ServiceErrKind> {
if self.user {
Ok(dirs::home_dir()
.ok_or_else(|| ServiceErrKind::DirectoryNotFound.into_error())?
.join("Library")
.join("LaunchAgents"))
} else {
Ok(PathBuf::from(GLOBAL_PATH))
}
}
fn make_file_name(&self) -> UniResult<PathBuf, ServiceErrKind> {
let mut filename = OsString::new();
filename.push(&self.prefix);
filename.push(&self.name);
filename.push(".plist");
Ok(self.path()?.join(filename))
}
fn domain_target(&self) -> OsString {
if self.user {
let uid = unsafe { libc::getuid() }.to_string();
let mut s = OsString::new();
s.push("gui/");
s.push(uid);
s
} else {
"system".into()
}
}
fn make_service_target(&self, fully_qualified: bool) -> OsString {
let mut s = if fully_qualified {
let domain = self.domain_target();
let mut s = OsString::with_capacity(domain.len() + self.prefix.len() + self.name.len());
s.push(domain);
s.push("/");
s
} else {
OsString::with_capacity(self.prefix.len() + self.name.len())
};
s.push(&self.prefix);
s.push(&self.name);
s
}
}
impl ServiceManager for LaunchDServiceManager {
fn fully_qualified_name(&self) -> Cow<'_, OsStr> {
self.make_service_target(true).into()
}
fn is_user_service(&self) -> bool {
self.user
}
fn install(&self, spec: &ServiceSpec) -> UniResult<(), ServiceErrKind> {
let args = spec
.path_and_args_string()?
.into_iter()
.map(|arg| format!(r#" <string>{arg}</string>"#))
.collect::<Vec<_>>()
.join("\n");
let label = util::os_string_to_string(self.make_service_target(false))?;
let restart = if spec.restart_on_failure {
format!(
r#" <key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
"#
)
} else {
format!(
r#" <key>KeepAlive</key>
<false/>
"#,
)
};
let run_at_load = if spec.autostart { "true" } else { "false" };
let user = match spec.user_string()? {
Some(user) => format!(" <key>UserName</key>\n <string>{user}</string>\n"),
None => String::new(),
};
let group = match spec.group_string()? {
Some(group) => {
format!(" <key>GroupName</key>\n <string>{group}</string>\n")
}
None => String::new(),
};
let service = 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>
{args}
</array>
{user}{group}{restart}
<key>RunAtLoad</key>
<{run_at_load}/>
</dict>
</plist>
"#,
);
let path = self.path()?;
fs::create_dir_all(&path).kind(ServiceErrKind::IoError)?;
let file = self.make_file_name()?;
write_file(&file, &service, SERVICE_PERMS)?;
Self::launch_ctl(
"bootstrap",
vec![self.domain_target().as_ref(), file.as_ref()],
)?;
Ok(())
}
fn uninstall(&self) -> UniResult<(), ServiceErrKind> {
let file = self.make_file_name()?;
Self::launch_ctl("bootout", vec![self.make_service_target(true).as_ref()])?;
fs::remove_file(file).kind(ServiceErrKind::IoError)?;
Ok(())
}
fn start(&self) -> UniResult<(), ServiceErrKind> {
Self::launch_ctl(
"kickstart",
vec![OsStr::new("-kp"), self.make_service_target(true).as_ref()],
)?;
Ok(())
}
fn stop(&self) -> UniResult<(), ServiceErrKind> {
Self::launch_ctl(
"kill",
vec![
OsStr::new("SIGTERM"),
self.make_service_target(true).as_ref(),
],
)?;
Ok(())
}
fn status(&self) -> UniResult<ServiceStatus, ServiceErrKind> {
match Self::launch_ctl("print", vec![self.make_service_target(true).as_ref()]) {
Ok(status) => {
if status.contains("state = running") {
Ok(ServiceStatus::Running)
} else {
Ok(ServiceStatus::Stopped)
}
}
Err(e) => match e.kind_ref() {
ServiceErrKind::BadExitStatus(code, _) if *code == Some(113) => {
Ok(ServiceStatus::NotInstalled)
}
_ => Err(e),
},
}
}
}