@@ -56,7 +56,9 @@ fn uninstall_service() -> Result<()> {
5656
5757// ── systemd (Linux) ──────────────────────────────────────────────────────────
5858
59- const SYSTEMD_UNIT_TEMPLATE : & str = "\
59+ /// User service (~/.config/systemd/user/axon.service).
60+ /// Runs as the invoking user; `WantedBy=default.target` is correct here.
61+ const SYSTEMD_USER_UNIT : & str = "\
6062 [Unit]
6163Description=Axon Data Store
6264After=network.target
@@ -66,11 +68,36 @@ Type=simple
6668ExecStart={binary_path} serve
6769Restart=on-failure
6870RestartSec=5
71+ StandardOutput=journal
72+ StandardError=journal
6973
7074[Install]
7175WantedBy=default.target
7276" ;
7377
78+ /// System service (/etc/systemd/system/axon.service).
79+ /// Runs as the `axon` system user; `WantedBy=multi-user.target` is standard for
80+ /// non-graphical daemons. The user and data directory must be created separately
81+ /// (see `create_axon_system_user`).
82+ const SYSTEMD_GLOBAL_UNIT : & str = "\
83+ [Unit]
84+ Description=Axon Data Store
85+ After=network.target
86+
87+ [Service]
88+ Type=simple
89+ User=axon
90+ Group=axon
91+ ExecStart={binary_path} serve
92+ Restart=on-failure
93+ RestartSec=5
94+ StandardOutput=journal
95+ StandardError=journal
96+
97+ [Install]
98+ WantedBy=multi-user.target
99+ " ;
100+
74101fn systemd_unit_path ( global : bool ) -> Result < PathBuf > {
75102 if global {
76103 Ok ( PathBuf :: from ( "/etc/systemd/system/axon.service" ) )
@@ -84,9 +111,46 @@ fn systemd_unit_path(global: bool) -> Result<PathBuf> {
84111 }
85112}
86113
114+ fn create_axon_system_user ( ) -> Result < ( ) > {
115+ // Check whether the `axon` system user already exists.
116+ let exists = Command :: new ( "id" )
117+ . arg ( "axon" )
118+ . status ( )
119+ . context ( "failed to run `id axon`" ) ?
120+ . success ( ) ;
121+
122+ if !exists {
123+ run_cmd (
124+ "useradd" ,
125+ & [
126+ "--system" ,
127+ "--no-create-home" ,
128+ "--home-dir" ,
129+ "/var/lib/axon" ,
130+ "--shell" ,
131+ "/usr/sbin/nologin" ,
132+ "--comment" ,
133+ "Axon Data Store" ,
134+ "axon" ,
135+ ] ,
136+ )
137+ . context ( "failed to create `axon` system user" ) ?;
138+ println ! ( "created system user `axon`" ) ;
139+ }
140+
141+ // Ensure the data directory exists and is owned by axon.
142+ std:: fs:: create_dir_all ( "/var/lib/axon" )
143+ . context ( "failed to create /var/lib/axon" ) ?;
144+ run_cmd ( "chown" , & [ "axon:axon" , "/var/lib/axon" ] )
145+ . context ( "failed to chown /var/lib/axon" ) ?;
146+ println ! ( "data directory: /var/lib/axon" ) ;
147+ Ok ( ( ) )
148+ }
149+
87150fn install_systemd ( bin : & std:: path:: Path , global : bool ) -> Result < ( ) > {
88151 let unit_path = systemd_unit_path ( global) ?;
89- let unit_content = SYSTEMD_UNIT_TEMPLATE . replace ( "{binary_path}" , & bin. display ( ) . to_string ( ) ) ;
152+ let template = if global { SYSTEMD_GLOBAL_UNIT } else { SYSTEMD_USER_UNIT } ;
153+ let unit_content = template. replace ( "{binary_path}" , & bin. display ( ) . to_string ( ) ) ;
90154
91155 if let Some ( parent) = unit_path. parent ( ) {
92156 std:: fs:: create_dir_all ( parent)
@@ -97,6 +161,7 @@ fn install_systemd(bin: &std::path::Path, global: bool) -> Result<()> {
97161 println ! ( "wrote {}" , unit_path. display( ) ) ;
98162
99163 if global {
164+ create_axon_system_user ( ) ?;
100165 run_cmd ( "systemctl" , & [ "daemon-reload" ] ) ?;
101166 run_cmd ( "systemctl" , & [ "enable" , "axon" ] ) ?;
102167 println ! ( "enabled axon.service (system)" ) ;
@@ -156,6 +221,16 @@ const LAUNCHD_PLIST_TEMPLATE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
156221
157222const LAUNCHD_LABEL : & str = "com.axon.server" ;
158223
224+ /// Returns `gui/<uid>` — the launchctl domain for the current user's GUI session.
225+ fn launchd_user_domain ( ) -> Result < String > {
226+ let out = Command :: new ( "id" )
227+ . arg ( "-u" )
228+ . output ( )
229+ . context ( "failed to run `id -u`" ) ?;
230+ let uid = String :: from_utf8_lossy ( & out. stdout ) . trim ( ) . to_string ( ) ;
231+ Ok ( format ! ( "gui/{uid}" ) )
232+ }
233+
159234fn launchd_plist_path ( global : bool ) -> Result < PathBuf > {
160235 if global {
161236 Ok ( PathBuf :: from (
@@ -182,7 +257,17 @@ fn install_launchd(bin: &std::path::Path, global: bool) -> Result<()> {
182257 . with_context ( || format ! ( "failed to write {}" , plist_path. display( ) ) ) ?;
183258 println ! ( "wrote {}" , plist_path. display( ) ) ;
184259
185- run_cmd ( "launchctl" , & [ "load" , & plist_path. display ( ) . to_string ( ) ] ) ?;
260+ // `launchctl load` is deprecated; use `bootstrap` on macOS 10.15+.
261+ // For user agents: bootstrap gui/<uid>; for system daemons: bootstrap system.
262+ if global {
263+ run_cmd ( "launchctl" , & [ "bootstrap" , "system" , & plist_path. display ( ) . to_string ( ) ] ) ?;
264+ } else {
265+ let domain = launchd_user_domain ( ) ?;
266+ run_cmd (
267+ "launchctl" ,
268+ & [ "bootstrap" , & domain, & plist_path. display ( ) . to_string ( ) ] ,
269+ ) ?;
270+ }
186271 println ! ( "loaded {LAUNCHD_LABEL}" ) ;
187272 Ok ( ( ) )
188273}
@@ -191,15 +276,16 @@ fn uninstall_launchd() -> Result<()> {
191276 let user_path = launchd_plist_path ( false ) ?;
192277 let global_path = launchd_plist_path ( true ) ?;
193278
194- let plist_path = if user_path. exists ( ) {
195- user_path
279+ let ( plist_path, domain ) = if user_path. exists ( ) {
280+ ( user_path, launchd_user_domain ( ) ? )
196281 } else if global_path. exists ( ) {
197- global_path
282+ ( global_path, "system" . to_string ( ) )
198283 } else {
199284 anyhow:: bail!( "no axon launchd plist found" ) ;
200285 } ;
201286
202- let _ = run_cmd ( "launchctl" , & [ "unload" , & plist_path. display ( ) . to_string ( ) ] ) ;
287+ // `launchctl unload` is deprecated; use `bootout`.
288+ let _ = run_cmd ( "launchctl" , & [ "bootout" , & domain, & plist_path. display ( ) . to_string ( ) ] ) ;
203289 std:: fs:: remove_file ( & plist_path)
204290 . with_context ( || format ! ( "failed to remove {}" , plist_path. display( ) ) ) ?;
205291 println ! ( "removed {}" , plist_path. display( ) ) ;
0 commit comments