pub fn unit_to_label(unit: &str) -> String {
let base = unit.strip_suffix(".service").unwrap_or(unit);
match base.strip_prefix("ados-") {
Some(rest) => format!("co.ados.{rest}"),
None => base.to_string(),
}
}
pub fn current_uid() -> u32 {
nix::unistd::geteuid().as_raw()
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PlistLogPaths<'a> {
pub stdout: Option<&'a str>,
pub stderr: Option<&'a str>,
}
pub fn render_plist(
label: &str,
program: &str,
args: &[String],
env: &[(String, String)],
keep_alive: bool,
logs: PlistLogPaths<'_>,
) -> String {
let mut out = String::new();
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",
);
out.push_str("<plist version=\"1.0\">\n");
out.push_str("<dict>\n");
out.push_str("\t<key>Label</key>\n");
out.push_str(&format!("\t<string>{}</string>\n", xml_escape(label)));
out.push_str("\t<key>ProgramArguments</key>\n");
out.push_str("\t<array>\n");
out.push_str(&format!("\t\t<string>{}</string>\n", xml_escape(program)));
for a in args {
out.push_str(&format!("\t\t<string>{}</string>\n", xml_escape(a)));
}
out.push_str("\t</array>\n");
if !env.is_empty() {
out.push_str("\t<key>EnvironmentVariables</key>\n");
out.push_str("\t<dict>\n");
for (k, v) in env {
out.push_str(&format!("\t\t<key>{}</key>\n", xml_escape(k)));
out.push_str(&format!("\t\t<string>{}</string>\n", xml_escape(v)));
}
out.push_str("\t</dict>\n");
}
if let Some(path) = logs.stdout {
out.push_str("\t<key>StandardOutPath</key>\n");
out.push_str(&format!("\t<string>{}</string>\n", xml_escape(path)));
}
if let Some(path) = logs.stderr {
out.push_str("\t<key>StandardErrorPath</key>\n");
out.push_str(&format!("\t<string>{}</string>\n", xml_escape(path)));
}
out.push_str("\t<key>RunAtLoad</key>\n");
out.push_str("\t<true/>\n");
if keep_alive {
out.push_str("\t<key>KeepAlive</key>\n");
out.push_str("\t<dict>\n");
out.push_str("\t\t<key>Crashed</key>\n");
out.push_str("\t\t<true/>\n");
out.push_str("\t</dict>\n");
}
out.push_str("</dict>\n");
out.push_str("</plist>\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_to_label_strips_suffix_and_maps_prefix() {
assert_eq!(unit_to_label("ados-control.service"), "co.ados.control");
assert_eq!(unit_to_label("ados-wfb"), "co.ados.wfb");
assert_eq!(unit_to_label("ados-wfb-rx.service"), "co.ados.wfb-rx");
assert_eq!(unit_to_label("mediamtx.service"), "mediamtx");
assert_eq!(unit_to_label("custom"), "custom");
}
#[test]
fn render_plist_emits_program_args_env_and_keepalive() {
let plist = render_plist(
"co.ados.control",
"/opt/ados/bin/ados-control",
&["--profile".to_string(), "compute".to_string()],
&[("ADOS_HOME".to_string(), "/var/ados".to_string())],
true,
PlistLogPaths::default(),
);
assert!(plist.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
assert!(plist.contains("<plist version=\"1.0\">"));
assert!(plist.trim_end().ends_with("</plist>"));
assert!(plist.contains("<key>Label</key>"));
assert!(plist.contains("<string>co.ados.control</string>"));
assert!(plist.contains("<key>ProgramArguments</key>"));
assert!(plist.contains("<string>/opt/ados/bin/ados-control</string>"));
assert!(plist.contains("<string>--profile</string>"));
assert!(plist.contains("<string>compute</string>"));
assert!(plist.contains("<key>EnvironmentVariables</key>"));
assert!(plist.contains("<key>ADOS_HOME</key>"));
assert!(plist.contains("<string>/var/ados</string>"));
assert!(plist.contains("<key>RunAtLoad</key>"));
assert!(plist.contains("<key>KeepAlive</key>"));
assert!(plist.contains("<key>Crashed</key>"));
}
#[test]
fn render_plist_omits_keepalive_and_env_when_unset() {
let plist = render_plist(
"co.ados.logd",
"/opt/ados/bin/ados-logd",
&[],
&[],
false,
PlistLogPaths::default(),
);
assert!(!plist.contains("KeepAlive"));
assert!(!plist.contains("EnvironmentVariables"));
assert!(!plist.contains("StandardOutPath"));
assert!(!plist.contains("StandardErrorPath"));
assert!(plist.contains("<string>/opt/ados/bin/ados-logd</string>"));
assert!(plist.contains("<key>RunAtLoad</key>"));
}
#[test]
fn render_plist_emits_log_redirection_when_given() {
let plist = render_plist(
"co.ados.control",
"/Users/op/.ados/bin/ados-control",
&[],
&[],
true,
PlistLogPaths {
stdout: Some("/Users/op/.ados/log/control.out.log"),
stderr: Some("/Users/op/.ados/log/control.err.log"),
},
);
assert!(plist.contains("<key>StandardOutPath</key>"));
assert!(plist.contains("<string>/Users/op/.ados/log/control.out.log</string>"));
assert!(plist.contains("<key>StandardErrorPath</key>"));
assert!(plist.contains("<string>/Users/op/.ados/log/control.err.log</string>"));
}
#[test]
fn render_plist_escapes_xml_special_characters() {
let plist = render_plist(
"co.ados.x",
"/bin/sh",
&["a < b & c > d".to_string()],
&[("K".to_string(), "v&<>".to_string())],
false,
PlistLogPaths::default(),
);
assert!(plist.contains("a < b & c > d"));
assert!(plist.contains("v&<>"));
assert!(!plist.contains("a < b & c > d"));
}
}