#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LaunchdSupervision {
Supervised(String),
NotSupervised,
Unknown(String),
}
impl LaunchdSupervision {
pub fn is_supervised(&self) -> bool {
matches!(self, LaunchdSupervision::Supervised(_))
}
pub fn describe(&self) -> String {
match self {
LaunchdSupervision::Supervised(label) => {
format!("launchd runs this process under `{label}`")
}
LaunchdSupervision::NotSupervised => "launchd does not run this process".to_string(),
LaunchdSupervision::Unknown(why) => {
format!("launchd supervision UNKNOWN — {why}")
}
}
}
}
pub fn launchd_supervision() -> LaunchdSupervision {
#[cfg(target_os = "macos")]
{
match run_bounded("launchctl", &["list"], LAUNCHCTL_TIMEOUT) {
Ok(stdout) => supervision_from_launchctl_list(&stdout, std::process::id()),
Err(why) => LaunchdSupervision::Unknown(why),
}
}
#[cfg(not(target_os = "macos"))]
{
LaunchdSupervision::NotSupervised
}
}
#[cfg(target_os = "macos")]
const LAUNCHCTL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
#[cfg(target_os = "macos")]
fn run_bounded(
program: &str,
args: &[&str],
timeout: std::time::Duration,
) -> Result<String, String> {
use std::io::Read as _;
let label = format!("`{program} {}`", args.join(" "));
let mut child = std::process::Command::new(program)
.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| format!("{label} could not run: {e}"))?;
let deadline = std::time::Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => {
let mut stdout = String::new();
if let Some(mut pipe) = child.stdout.take() {
let _ = pipe.read_to_string(&mut stdout);
}
return if status.success() {
Ok(stdout)
} else {
Err(format!("{label} exited with status {status}"))
};
}
Ok(None) => {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Err(format!("{label} did not answer within {timeout:?}"));
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
Err(e) => return Err(format!("{label} could not be waited on: {e}")),
}
}
}
pub fn supervision_from_launchctl_list(stdout: &str, pid: u32) -> LaunchdSupervision {
let mut parsed_any_row = false;
for line in stdout.lines() {
let mut fields = line.split('\t');
let (Some(pid_field), Some(_status), Some(label)) =
(fields.next(), fields.next(), fields.next())
else {
continue;
};
let label = label.trim();
if label.is_empty() {
continue;
}
let pid_field = pid_field.trim();
if pid_field == "PID" {
continue;
}
parsed_any_row = true;
if pid_field.parse::<u32>() == Ok(pid) {
return LaunchdSupervision::Supervised(label.to_string());
}
}
if parsed_any_row {
LaunchdSupervision::NotSupervised
} else {
LaunchdSupervision::Unknown("`launchctl list` returned no parseable job rows".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table() -> String {
[
"PID\tStatus\tLabel",
"505\t0\tcom.apple.trustd.agent",
"-\t0\tcom.apple.mdworker.mail",
"98606\t0\tcom.trusty.mpm",
]
.join("\n")
}
#[test]
fn supervision_reads_the_pid_column() {
assert_eq!(
supervision_from_launchctl_list(&table(), 98606),
LaunchdSupervision::Supervised("com.trusty.mpm".to_string())
);
}
#[test]
fn supervision_not_supervised_when_pid_absent() {
assert_eq!(
supervision_from_launchctl_list(&table(), 12345),
LaunchdSupervision::NotSupervised
);
}
#[test]
fn supervision_ignores_the_header_row() {
let out = supervision_from_launchctl_list("PID\tStatus\tLabel\n", 1);
assert!(
matches!(out, LaunchdSupervision::Unknown(_)),
"header-only output is not an answer, got {out:?}"
);
}
#[test]
fn supervision_ignores_dash_pids() {
let out = supervision_from_launchctl_list("PID\tStatus\tLabel\n-\t0\tcom.x\n", 0);
assert_eq!(out, LaunchdSupervision::NotSupervised);
}
#[test]
fn supervision_unknown_when_table_has_no_rows() {
assert!(matches!(
supervision_from_launchctl_list("", 1),
LaunchdSupervision::Unknown(_)
));
}
#[test]
fn supervision_unknown_for_unrecognised_format() {
assert!(matches!(
supervision_from_launchctl_list("{\"jobs\": []}\n", 1),
LaunchdSupervision::Unknown(_)
));
}
#[test]
fn supervision_rejects_a_child_that_inherited_xpc_service_name() {
let verdict = supervision_from_launchctl_list(&table(), 424_242);
assert_eq!(
verdict,
LaunchdSupervision::NotSupervised,
"a child that merely INHERITED XPC_SERVICE_NAME must not self-report supervised"
);
assert!(!verdict.is_supervised());
}
#[cfg(target_os = "macos")]
#[test]
fn bounded_run_kills_a_command_that_outlives_its_deadline() {
let started = std::time::Instant::now();
let result = run_bounded("sleep", &["30"], std::time::Duration::from_millis(300));
let elapsed = started.elapsed();
let err = result.expect_err("a 30s command must not satisfy a 300ms deadline");
assert!(err.contains("did not answer within"), "was: {err}");
assert!(
elapsed < std::time::Duration::from_secs(5),
"the deadline was not enforced; waited {elapsed:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn bounded_run_returns_stdout_on_success() {
let out = run_bounded("echo", &["hello"], std::time::Duration::from_secs(5)).unwrap();
assert_eq!(out.trim(), "hello");
}
#[cfg(target_os = "macos")]
#[test]
fn bounded_run_reports_a_nonzero_exit() {
let err = run_bounded("false", &[], std::time::Duration::from_secs(5))
.expect_err("a non-zero exit is not an answer");
assert!(err.contains("exited with status"), "was: {err}");
}
#[test]
fn is_supervised_is_false_for_unknown() {
assert!(!LaunchdSupervision::Unknown("no launchctl".into()).is_supervised());
assert!(!LaunchdSupervision::NotSupervised.is_supervised());
assert!(LaunchdSupervision::Supervised("com.trusty.mpm".into()).is_supervised());
}
#[test]
fn describe_names_the_label() {
let d = LaunchdSupervision::Supervised("com.trusty.mpm".into()).describe();
assert!(d.contains("com.trusty.mpm"), "was: {d}");
}
#[test]
fn describe_repeats_the_unknown_reason() {
let d = LaunchdSupervision::Unknown("launchctl missing".into()).describe();
assert!(d.contains("UNKNOWN"), "was: {d}");
assert!(d.contains("launchctl missing"), "was: {d}");
}
}