1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//! Modern, timeout-protected macOS launchd (`launchctl`) control.
//!
//! Uses the `bootstrap` / `bootout` / `enable` / `kickstart` subcommands in the
//! per-user `gui/<uid>` domain instead of the legacy `load` / `unload`. On
//! macOS 13+ the legacy verbs intermittently fail with `Input/output error`
//! and can leave a job *half-loaded*; combined with `KeepAlive` that produces a
//! launchd crash-loop which wedges every client waiting on the service (and
//! previously forced users to reboot after a binary update).
//!
//! Every invocation is wrapped in a hard timeout via
//! [`crate::ipc::process::run_with_timeout`], so a stuck `launchctl` can never
//! block a `lean-ctx` command.
use std::path::Path;
use std::process::Command;
use std::time::Duration;
const LAUNCHCTL_TIMEOUT: Duration = Duration::from_secs(10);
fn gui_domain() -> String {
// SAFETY: `getuid` takes no arguments, always succeeds, and only reads the
// calling process's real UID — it cannot fail or cause undefined behaviour.
let uid = unsafe { libc::getuid() };
format!("gui/{uid}")
}
fn service_target(label: &str) -> String {
format!("{}/{label}", gui_domain())
}
/// Invoke `launchctl` with the given args under a hard timeout.
/// Returns `Some(success)` when the call completed, `None` when it timed out.
fn launchctl(args: &[&str]) -> Option<bool> {
let mut cmd = Command::new("launchctl");
cmd.args(args);
if let Some(out) = crate::ipc::process::run_with_timeout(cmd, LAUNCHCTL_TIMEOUT) {
Some(out.status.success())
} else {
tracing::warn!("launchctl {args:?} timed out after {LAUNCHCTL_TIMEOUT:?}");
None
}
}
/// Stop and unregister a LaunchAgent (idempotent; a not-loaded job is fine).
/// Never hangs. Tries label-based bootout first, then path-based as a fallback.
pub fn bootout(label: &str, plist: &Path) {
if launchctl(&["bootout", &service_target(label)]).is_some() {
return;
}
let _ = launchctl(&["bootout", &gui_domain(), &plist.to_string_lossy()]);
}
/// Register and enable a LaunchAgent in the gui domain. Idempotent and
/// timeout-protected. Returns `true` if bootstrap reported success.
///
/// The job is always booted out first, so the subsequent bootstrap is a clean
/// load that honours the plist's `RunAtLoad` (proxy/daemon start immediately;
/// interval-only jobs like the auto-updater simply register). No `kickstart` is
/// issued, so this never forces an unwanted immediate run.
pub fn bootstrap(label: &str, plist: &Path) -> bool {
// Clear any stale registration so bootstrap can't fail with
// "service already bootstrapped".
bootout(label, plist);
let _ = launchctl(&["enable", &service_target(label)]);
launchctl(&["bootstrap", &gui_domain(), &plist.to_string_lossy()]).unwrap_or(false)
}
/// Returns `true` if the service is currently registered in the gui domain.
pub fn is_loaded(label: &str) -> bool {
launchctl(&["print", &service_target(label)]).unwrap_or(false)
}