#![cfg(target_os = "macos")]
use std::time::Duration;
use crate::launchd::LaunchdConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Unload {
Gone {
waited_secs: u64,
},
StillLoaded {
waited_secs: u64,
},
}
impl Unload {
#[must_use]
pub fn waited_secs(&self) -> u64 {
match self {
Unload::Gone { waited_secs } | Unload::StillLoaded { waited_secs } => *waited_secs,
}
}
#[must_use]
pub fn is_gone(&self) -> bool {
matches!(self, Unload::Gone { .. })
}
fn phrase(&self) -> String {
match self {
Unload::Gone { waited_secs } => format!("{waited_secs}s (label gone)"),
Unload::StillLoaded { waited_secs } => {
format!("{waited_secs}s (label STILL registered)")
}
}
}
}
pub fn await_unload(
budget_secs: u64,
mut is_loaded: impl FnMut() -> bool,
mut tick: impl FnMut(),
) -> Unload {
if !is_loaded() {
return Unload::Gone { waited_secs: 0 };
}
for waited_secs in 1..=budget_secs {
tick();
if !is_loaded() {
return Unload::Gone { waited_secs };
}
}
Unload::StillLoaded {
waited_secs: budget_secs,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Restarted {
pub unload: Unload,
pub attempts: u32,
}
pub fn restart_sequence(
label: &str,
quiesce: impl FnOnce(),
bootout: impl FnOnce() -> Result<(), String>,
mut await_unload: impl FnMut() -> Unload,
mut bootstrap: impl FnMut() -> Result<(), String>,
) -> Result<Restarted, String> {
quiesce();
bootout()?;
let first_wait = await_unload();
let Err(first_error) = bootstrap() else {
return Ok(Restarted {
unload: first_wait,
attempts: 1,
});
};
let second_wait = await_unload();
match bootstrap() {
Ok(()) => Ok(Restarted {
unload: second_wait,
attempts: 2,
}),
Err(second_error) => Err(format!(
"restarting {label} failed: booted out successfully, then `launchctl \
bootstrap` failed twice. After waiting {} — {first_error}. After a \
further {} — {second_error}",
first_wait.phrase(),
second_wait.phrase()
)),
}
}
impl LaunchdConfig {
pub fn restart_gracefully(&self) -> anyhow::Result<Restarted> {
let budget_secs = crate::shutdown::termination_grace().as_secs();
restart_sequence(
&self.label,
|| self.guard_short_grace(budget_secs),
|| self.bootout().map_err(|e| format!("{e:#}")),
|| self.await_unload(budget_secs),
|| self.bootstrap().map_err(|e| format!("{e:#}")),
)
.map_err(anyhow::Error::msg)
}
fn await_unload(&self, budget_secs: u64) -> Unload {
await_unload(
budget_secs,
|| self.is_loaded(),
|| std::thread::sleep(Duration::from_secs(1)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
const LABEL: &str = "com.trusty.memory";
const RACE_ERROR: &str = "launchctl bootstrap gui/502 \
…/com.trusty.memory.plist failed: Bootstrap \
failed: 5: Input/output error";
#[derive(Default)]
struct Steps(RefCell<Vec<&'static str>>);
impl Steps {
fn push(&self, step: &'static str) {
self.0.borrow_mut().push(step);
}
fn taken(&self) -> Vec<&'static str> {
self.0.borrow().clone()
}
}
#[test]
fn restart_waits_for_the_unload_before_bootstrapping() {
let steps = Steps::default();
let outcome = restart_sequence(
LABEL,
|| steps.push("quiesce"),
|| {
steps.push("bootout");
Ok(())
},
|| {
steps.push("await_unload");
Unload::Gone { waited_secs: 3 }
},
|| {
steps.push("bootstrap");
Ok(())
},
)
.expect("a clean bounce succeeds");
assert_eq!(
steps.taken(),
vec!["quiesce", "bootout", "await_unload", "bootstrap"],
"the bootstrap must not be issued until launchd has released the label"
);
assert_eq!(
outcome,
Restarted {
unload: Unload::Gone { waited_secs: 3 },
attempts: 1,
}
);
}
#[test]
fn restart_retries_the_bootstrap_once_after_a_second_wait() {
let steps = Steps::default();
let attempts = RefCell::new(0_u32);
let outcome = restart_sequence(
LABEL,
|| steps.push("quiesce"),
|| {
steps.push("bootout");
Ok(())
},
|| {
steps.push("await_unload");
Unload::Gone { waited_secs: 2 }
},
|| {
steps.push("bootstrap");
*attempts.borrow_mut() += 1;
if *attempts.borrow() == 1 {
Err(RACE_ERROR.to_owned())
} else {
Ok(())
}
},
)
.expect("the retry recovers the observed race");
assert_eq!(
steps.taken(),
vec![
"quiesce",
"bootout",
"await_unload",
"bootstrap",
"await_unload",
"bootstrap"
],
"the retry must wait for the unload again, not re-roll the same race"
);
assert_eq!(outcome.attempts, 2);
}
#[test]
fn restart_error_names_the_label_and_the_waits() {
let err = restart_sequence(
LABEL,
|| {},
|| Ok(()),
|| Unload::StillLoaded { waited_secs: 60 },
|| Err(RACE_ERROR.to_owned()),
)
.expect_err("two failed bootstraps are an error");
assert!(err.contains(LABEL), "the label must be named: {err}");
assert!(
err.contains("60s (label STILL registered)"),
"the wait and its outcome must be named: {err}"
);
assert!(
err.contains("Input/output error"),
"launchd's own reason must survive: {err}"
);
}
#[test]
fn restart_does_not_bootstrap_when_the_bootout_fails() {
let steps = Steps::default();
let err = restart_sequence(
LABEL,
|| steps.push("quiesce"),
|| Err("launchctl bootout failed: Operation not permitted".to_owned()),
|| {
steps.push("await_unload");
unreachable!("no wait after a bootout that failed")
},
|| {
steps.push("bootstrap");
unreachable!("never bootstrap on top of a unit still running")
},
)
.expect_err("a failed bootout is an error");
assert!(err.contains("Operation not permitted"));
assert_eq!(steps.taken(), vec!["quiesce"]);
}
#[test]
fn await_unload_is_immediate_when_the_label_is_already_gone() {
let outcome = await_unload(60, || false, || unreachable!("nothing to wait for"));
assert_eq!(outcome, Unload::Gone { waited_secs: 0 });
assert!(outcome.is_gone());
}
#[test]
fn await_unload_waits_for_the_label_to_disappear() {
let probes = RefCell::new(0_u64);
let ticks = RefCell::new(0_u64);
let outcome = await_unload(
60,
|| {
*probes.borrow_mut() += 1;
*probes.borrow() <= 3
},
|| *ticks.borrow_mut() += 1,
);
assert_eq!(outcome, Unload::Gone { waited_secs: 3 });
assert_eq!(*ticks.borrow(), 3, "one probe per tick, no busy loop");
}
#[test]
fn await_unload_gives_up_at_the_budget() {
let ticks = RefCell::new(0_u64);
let outcome = await_unload(4, || true, || *ticks.borrow_mut() += 1);
assert_eq!(outcome, Unload::StillLoaded { waited_secs: 4 });
assert!(!outcome.is_gone());
assert_eq!(*ticks.borrow(), 4);
}
}