use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use super::plist::LABEL;
use super::{Scope, ServiceError};
use crate::utils::command::new_command;
pub const PLIST_FILE_NAME: &str = "com.lablup.all-smi.plist";
const SYSTEM_PLIST_DIR: &str = "/Library/LaunchDaemons";
const SYSTEM_LOG_PATH: &str = "/var/log/all-smi/all-smi.log";
const USER_AGENT_SUBDIR: &str = "Library/LaunchAgents";
const USER_LOG_SUBPATH: &str = "Library/Logs/all-smi/all-smi.log";
const BOOTSTRAP_ATTEMPTS: u32 = 5;
const BOOTSTRAP_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(200);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
pub plist: PathBuf,
pub log: PathBuf,
pub domain: String,
pub target: String,
}
pub fn layout_from(scope: Scope, home: &Path, uid: u32) -> Layout {
match scope {
Scope::System => Layout {
plist: Path::new(SYSTEM_PLIST_DIR).join(PLIST_FILE_NAME),
log: PathBuf::from(SYSTEM_LOG_PATH),
domain: "system".to_string(),
target: format!("system/{LABEL}"),
},
Scope::User => Layout {
plist: home.join(USER_AGENT_SUBDIR).join(PLIST_FILE_NAME),
log: home.join(USER_LOG_SUBPATH),
domain: format!("gui/{uid}"),
target: format!("gui/{uid}/{LABEL}"),
},
}
}
pub fn layout(scope: Scope) -> Result<Layout, ServiceError> {
if scope == Scope::System {
return Ok(layout_from(scope, Path::new("/"), 0));
}
let uid = gui_uid()?;
let home = dirs::home_dir().ok_or_else(|| {
ServiceError::NotSupported(
"cannot locate a home directory for the LaunchAgent; set $HOME, or drop --user to \
install the system LaunchDaemon with sudo"
.to_string(),
)
})?;
Ok(layout_from(scope, &home, uid))
}
#[cfg(unix)]
fn gui_uid() -> Result<u32, ServiceError> {
let uid = unsafe { libc::getuid() };
if uid == 0 {
return Err(ServiceError::NotSupported(
"--user targets the gui/0 launchd domain, which does not exist because root has no \
login session. Drop sudo to manage your own LaunchAgent, or drop --user to install \
the system LaunchDaemon."
.to_string(),
));
}
Ok(uid)
}
#[cfg(not(unix))]
fn gui_uid() -> Result<u32, ServiceError> {
Err(ServiceError::NotSupported(
"launchd is macOS-only".to_string(),
))
}
pub(super) fn describe(args: &[&str]) -> String {
format!("launchctl {}", args.join(" "))
}
fn command(args: &[&str]) -> Command {
let mut cmd = new_command("launchctl");
cmd.args(args);
cmd
}
pub(super) fn output(args: &[&str]) -> Result<Output, ServiceError> {
Ok(command(args).output()?)
}
pub(super) fn run(args: &[&str]) -> Result<String, ServiceError> {
let output = output(args)?;
if !output.status.success() {
return Err(ServiceError::CommandFailed {
cmd: describe(args),
stderr: failure_text(&output),
});
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub(super) fn run_best_effort(args: &[&str]) {
let _ = output(args);
}
pub(super) fn failure_text(output: &Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if !stderr.is_empty() {
return stderr;
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !stdout.is_empty() {
return stdout;
}
format!("exited with {}", output.status)
}
pub(super) fn print_job(target: &str) -> Result<Option<String>, ServiceError> {
let output = output(&["print", target])?;
if output.status.success() {
Ok(Some(String::from_utf8_lossy(&output.stdout).into_owned()))
} else {
Ok(None)
}
}
pub(super) fn bootstrap(layout: &Layout) -> Result<(), ServiceError> {
let plist_arg = path_arg(&layout.plist)?;
let args = ["bootstrap", layout.domain.as_str(), plist_arg];
let mut last = None;
for attempt in 0..BOOTSTRAP_ATTEMPTS {
match run(&args) {
Ok(_) => return Ok(()),
Err(e) => {
last = Some(e);
if attempt + 1 < BOOTSTRAP_ATTEMPTS {
std::thread::sleep(BOOTSTRAP_RETRY_DELAY);
}
}
}
}
Err(last.unwrap_or_else(|| ServiceError::CommandFailed {
cmd: describe(&args),
stderr: "bootstrap did not run".to_string(),
}))
}
pub(super) fn path_arg(path: &Path) -> Result<&str, ServiceError> {
path.to_str().ok_or_else(|| {
ServiceError::Conflict(format!(
"path `{}` is not valid UTF-8 and cannot be passed to launchctl",
path.display()
))
})
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PrintInfo {
pub state: String,
pub pid: Option<u32>,
}
impl PrintInfo {
pub fn running(&self) -> bool {
self.state == "running"
}
}
pub fn parse_print_output(raw: &str) -> PrintInfo {
let mut state: Option<String> = None;
let mut pid: Option<u32> = None;
for line in raw.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
let value = value.trim();
match key.trim() {
"state" if state.is_none() => state = Some(value.to_string()),
"pid" if pid.is_none() => pid = value.parse::<u32>().ok().filter(|p| *p != 0),
_ => {}
}
}
PrintInfo {
state: state.unwrap_or_default(),
pid,
}
}
pub fn parse_disabled(raw: &str, label: &str) -> Option<bool> {
let needle = format!("\"{label}\"");
for line in raw.lines() {
let Some(rest) = line.trim().strip_prefix(needle.as_str()) else {
continue;
};
let Some(value) = rest.split("=>").nth(1) else {
continue;
};
return match value.trim() {
"true" | "disabled" => Some(true),
"false" | "enabled" => Some(false),
_ => None,
};
}
None
}
pub(super) fn query_disabled(domain: &str) -> Option<bool> {
let output = output(&["print-disabled", domain]).ok()?;
if !output.status.success() {
return None;
}
let raw = String::from_utf8_lossy(&output.stdout);
Some(parse_disabled(&raw, LABEL).unwrap_or(false))
}
pub(super) fn not_loaded_detail(installed: bool, stderr: &str) -> String {
if !installed {
return "not installed".to_string();
}
if stderr.contains("Could not find service") {
return "not loaded".to_string();
}
let first = stderr.lines().next().unwrap_or("").trim();
if first.is_empty() {
"not loaded".to_string()
} else {
format!("loaded state unknown: {first}")
}
}
#[cfg(test)]
#[path = "launchctl_tests.rs"]
mod tests;