use std::path::Path;
use std::time::Duration;
const PROBE_TIMEOUT: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StartupAction {
Proceed,
ExitAlreadyRunning,
Fail(String),
}
pub fn startup_action_from_probe_result(probe_ok: bool) -> StartupAction {
if probe_ok {
StartupAction::ExitAlreadyRunning
} else {
StartupAction::Proceed
}
}
#[must_use]
pub fn is_production_socket(socket: &Path) -> bool {
if std::env::var_os(trusty_common::DATA_DIR_OVERRIDE_ENV).is_some() {
return false;
}
crate::socket_path().is_ok_and(|p| p == socket)
}
#[must_use]
pub fn production_bind_refusal(
label: &str,
is_production_socket: bool,
unit_registered: bool,
supervision: &trusty_common::supervision::LaunchdSupervision,
) -> Option<String> {
use trusty_common::supervision::LaunchdSupervision;
let positively_unsupervised = matches!(supervision, LaunchdSupervision::NotSupervised);
if !(is_production_socket && unit_registered && positively_unsupervised) {
return None;
}
Some(format!(
"refusing to bind the trusty-memory production socket: launchd unit \
{label} is registered for it and launchd does not run this process. An \
unsupervised daemon here starts without the plist's EnvironmentVariables \
and launchd's own instance then exits 0 reporting success (#6619). Start \
it with `launchctl kickstart -k gui/$(id -u)/{label}`, or point this \
process at a different socket"
))
}
pub async fn single_instance_check(socket: &Path) -> StartupAction {
let probe_ok = trusty_common::uds::socket_is_serving(socket, PROBE_TIMEOUT).await;
startup_action_from_probe_result(probe_ok)
}
pub async fn single_instance_check_retried(
socket: &Path,
max_retries: u8,
delay_ms: u64,
) -> StartupAction {
let mut action = single_instance_check(socket).await;
let mut retries = max_retries;
while action == StartupAction::Proceed && retries > 0 {
retries -= 1;
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
action = single_instance_check(socket).await;
}
action
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn startup_action_from_probe_result_when_alive() {
assert_eq!(
startup_action_from_probe_result(true),
StartupAction::ExitAlreadyRunning,
"alive probe → ExitAlreadyRunning"
);
}
#[test]
fn startup_action_from_probe_result_when_dead() {
assert_eq!(
startup_action_from_probe_result(false),
StartupAction::Proceed,
"dead/absent probe → Proceed"
);
}
#[tokio::test]
async fn single_instance_check_proceeds_when_nothing_is_serving() {
let tmp = tempfile::tempdir().expect("tempdir");
let action = single_instance_check(&tmp.path().join("absent.sock")).await;
assert_eq!(
action,
StartupAction::Proceed,
"an absent socket must never block a cold start"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn single_instance_check_exits_when_something_is_serving() {
let tmp = tempfile::tempdir().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-memory.sock");
let listener = trusty_common::uds::bind_hardened(&socket).expect("bind");
tokio::spawn(async move { while listener.accept().await.is_ok() {} });
assert_eq!(
single_instance_check(&socket).await,
StartupAction::ExitAlreadyRunning,
"a live socket must stop a second instance from binding"
);
}
use trusty_common::supervision::LaunchdSupervision;
const LABEL: &str = "com.trusty.memory";
#[test]
fn bind_refused_for_an_unsupervised_process_on_a_registered_socket() {
let refusal =
production_bind_refusal(LABEL, true, true, &LaunchdSupervision::NotSupervised)
.expect("an unsupervised bind of a registered socket must be refused");
assert!(refusal.contains(LABEL), "the unit must be named: {refusal}");
}
#[test]
fn bind_permitted_for_the_launchd_unit_itself() {
assert_eq!(
production_bind_refusal(
LABEL,
true,
true,
&LaunchdSupervision::Supervised(LABEL.to_owned())
),
None
);
}
#[test]
fn bind_permitted_when_no_unit_is_registered() {
assert_eq!(
production_bind_refusal(LABEL, true, false, &LaunchdSupervision::NotSupervised),
None
);
}
#[test]
fn bind_permitted_on_a_socket_launchd_does_not_own() {
assert_eq!(
production_bind_refusal(LABEL, false, true, &LaunchdSupervision::NotSupervised),
None
);
}
#[test]
fn bind_permitted_when_launchd_cannot_be_asked() {
assert_eq!(
production_bind_refusal(
LABEL,
true,
true,
&LaunchdSupervision::Unknown("launchctl timed out".to_owned())
),
None,
"an unanswerable launchd is not evidence of an orphan"
);
}
#[test]
fn production_socket_is_the_resolved_path() {
let _env = crate::commands::env_test_lock().blocking_lock();
let Ok(resolved) = crate::socket_path() else {
return; };
if std::env::var_os(trusty_common::DATA_DIR_OVERRIDE_ENV).is_some() {
return; }
assert!(is_production_socket(&resolved));
assert!(!is_production_socket(Path::new("/tmp/not-the-daemon.sock")));
}
#[test]
#[serial_test::serial]
fn production_socket_is_false_under_a_data_dir_override() {
let _env = crate::commands::env_test_lock().blocking_lock();
let tmp = tempfile::tempdir().expect("tempdir");
let previous = std::env::var_os(trusty_common::DATA_DIR_OVERRIDE_ENV);
unsafe { std::env::set_var(trusty_common::DATA_DIR_OVERRIDE_ENV, tmp.path()) };
let resolved = crate::socket_path();
let verdict = resolved.as_ref().map(|p| is_production_socket(p));
unsafe {
match previous {
Some(v) => std::env::set_var(trusty_common::DATA_DIR_OVERRIDE_ENV, v),
None => std::env::remove_var(trusty_common::DATA_DIR_OVERRIDE_ENV),
}
}
assert_eq!(
verdict.ok(),
Some(false),
"a sandboxed socket is never launchd's production path"
);
}
}