Skip to content

Commit 7b9bedc

Browse files
committed
Fix launchd install from SSH sessions
1 parent 3d26447 commit 7b9bedc

2 files changed

Lines changed: 57 additions & 10 deletions

File tree

garyx/src/service_manager/launchd.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,32 @@ impl LaunchdManager {
5353
.arg("managername")
5454
.output()
5555
.map(|out| {
56-
out.status.success()
57-
&& String::from_utf8_lossy(&out.stdout).trim() == "Aqua"
56+
out.status.success() && String::from_utf8_lossy(&out.stdout).trim() == "Aqua"
5857
})
5958
.unwrap_or(false)
6059
}
6160

61+
fn domain_exists(&self, domain: &str) -> bool {
62+
ProcessCommand::new(LAUNCHCTL_BIN)
63+
.args(["print", domain])
64+
.output()
65+
.map(|out| out.status.success())
66+
.unwrap_or(false)
67+
}
68+
6269
/// Domains to attempt when bootstrapping a not-yet-loaded agent, in
63-
/// priority order. In an Aqua session we prefer `gui/<uid>` to keep parity
64-
/// with historical desktop installs, falling back to the per-user domain;
65-
/// over SSH the per-user domain is the only one available.
70+
/// priority order. Prefer `gui/<uid>` when an Aqua login domain exists,
71+
/// even if this command is invoked from an SSH / Background session: macOS
72+
/// accepts bootstrapping into that existing GUI domain, while bootstrapping
73+
/// the same LaunchAgent into `user/<uid>` can fail with launchctl error 5.
74+
/// On truly headless sessions, fall back to the per-user domain.
6675
fn candidate_install_domains(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
6776
let uid = self.uid()?;
68-
if self.is_aqua_session() {
69-
Ok(vec![format!("gui/{uid}"), format!("user/{uid}")])
70-
} else {
71-
Ok(vec![format!("user/{uid}")])
72-
}
77+
Ok(candidate_install_domains_for(
78+
&uid,
79+
self.is_aqua_session(),
80+
self.domain_exists(&format!("gui/{uid}")),
81+
))
7382
}
7483

7584
/// The domain the agent is currently loaded in, if any. Probing both
@@ -260,6 +269,20 @@ impl ServiceManager for LaunchdManager {
260269
}
261270
}
262271

272+
fn candidate_install_domains_for(
273+
uid: &str,
274+
is_aqua_session: bool,
275+
gui_domain_exists: bool,
276+
) -> Vec<String> {
277+
let gui_domain = format!("gui/{uid}");
278+
let user_domain = format!("user/{uid}");
279+
if is_aqua_session || gui_domain_exists {
280+
vec![gui_domain, user_domain]
281+
} else {
282+
vec![user_domain]
283+
}
284+
}
285+
263286
fn xml_escape(value: &str) -> String {
264287
value
265288
.replace('&', "&amp;")

garyx/src/service_manager/launchd/tests.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
use super::*;
22

3+
#[test]
4+
fn candidate_install_domains_prefers_gui_for_aqua_session() {
5+
assert_eq!(
6+
candidate_install_domains_for("501", true, true),
7+
vec!["gui/501".to_owned(), "user/501".to_owned()]
8+
);
9+
}
10+
11+
#[test]
12+
fn candidate_install_domains_prefers_existing_gui_domain_from_background_session() {
13+
assert_eq!(
14+
candidate_install_domains_for("501", false, true),
15+
vec!["gui/501".to_owned(), "user/501".to_owned()]
16+
);
17+
}
18+
19+
#[test]
20+
fn candidate_install_domains_uses_user_domain_when_no_gui_domain_exists() {
21+
assert_eq!(
22+
candidate_install_domains_for("501", false, false),
23+
vec!["user/501".to_owned()]
24+
);
25+
}
26+
327
#[test]
428
fn render_launch_agent_plist_uses_expected_label_and_program() {
529
let plist = render_launch_agent_plist(

0 commit comments

Comments
 (0)