@@ -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+
263286fn xml_escape ( value : & str ) -> String {
264287 value
265288 . replace ( '&' , "&" )
0 commit comments