@@ -16,10 +16,9 @@ import { resolveExecutorDataDir } from "./local-server-manifest";
1616// `<executor> daemon run --foreground --port <p>`, bind loopback, write
1717// `server.json`, and get restarted on crash but not on a clean stop.
1818//
19- // macOS is the fully-built, e2e-proven path. Linux is implemented best-effort
20- // (systemctl is usually present but unverified here). Windows is a guided
21- // scaffold that prints the exact commands rather than claiming registration we
22- // cannot yet verify.
19+ // macOS (launchd) and Linux (systemd --user + lingering) are both reboot-
20+ // survival verified in real VMs. Windows is a guided scaffold that prints the
21+ // exact commands rather than claiming registration we cannot yet verify.
2322// ---------------------------------------------------------------------------
2423
2524export const SERVICE_LABEL = "sh.executor.daemon" ;
@@ -93,9 +92,11 @@ interface CommandResult {
9392const runCommand = (
9493 cmd : string ,
9594 args : ReadonlyArray < string > ,
95+ env ?: Record < string , string | undefined > ,
9696) : Effect . Effect < CommandResult , Error > =>
9797 Effect . callback < CommandResult , Error > ( ( resume ) => {
98- execFile ( cmd , [ ...args ] , { encoding : "utf8" } , ( error , stdout , stderr ) => {
98+ const options = env ? { encoding : "utf8" as const , env : { ...process . env , ...env } } : { encoding : "utf8" as const } ;
99+ execFile ( cmd , [ ...args ] , options , ( error , stdout , stderr ) => {
99100 // A string `code` (ENOENT etc.) means the command could not be spawned.
100101 if ( error && typeof ( error as { code ?: unknown } ) . code === "string" ) {
101102 resume (
@@ -321,7 +322,7 @@ const makeLaunchdBackend = (): ServiceBackend => {
321322} ;
322323
323324// ---------------------------------------------------------------------------
324- // Linux — systemd --user (best-effort, unverified on this machine )
325+ // Linux — systemd --user + lingering (reboot-survival verified in an Ubuntu VM )
325326// ---------------------------------------------------------------------------
326327
327328const systemdUnitDir = ( path : Path . Path ) : string =>
@@ -384,44 +385,75 @@ const makeSystemdBackend = (): ServiceBackend => {
384385 stderrPath : path . join ( logs , "daemon.error.log" ) ,
385386 } ) ;
386387 yield * fs . writeFileString ( systemdUnitPath ( path ) , unit , { mode : 0o600 } ) ;
387- yield * runCommand ( "systemctl" , [ "--user" , "daemon-reload" ] ) . pipe ( Effect . ignore ) ;
388- const enable = yield * runCommand ( "systemctl" , [ "--user" , "enable" , "--now" , unitName ] ) ;
388+ // `systemctl --user` needs XDG_RUNTIME_DIR to reach the user bus. Supply
389+ // it if the caller's environment lacks it (e.g. a non-login shell) so
390+ // install is robust regardless of how it was invoked.
391+ const username = userInfo ( ) . username ;
392+ const sdEnv = { XDG_RUNTIME_DIR : process . env . XDG_RUNTIME_DIR ?? `/run/user/${ currentUid ( ) } ` } ;
393+ yield * runCommand ( "systemctl" , [ "--user" , "daemon-reload" ] , sdEnv ) . pipe ( Effect . ignore ) ;
394+ const enable = yield * runCommand (
395+ "systemctl" ,
396+ [ "--user" , "enable" , "--now" , unitName ] ,
397+ sdEnv ,
398+ ) ;
389399 if ( enable . code !== 0 ) {
390400 return yield * Effect . fail (
391- new Error (
392- `systemctl --user enable failed (exit ${ enable . code } ): ${ enable . stderr . trim ( ) } \nNote: 'loginctl enable-linger ${ userInfo ( ) . username } ' may be required for the service to survive logout/reboot.` ,
393- ) ,
401+ new Error ( `systemctl --user enable failed (exit ${ enable . code } ): ${ enable . stderr . trim ( ) } ` ) ,
394402 ) ;
395403 }
404+ // Enable lingering so the user manager — and this enabled service —
405+ // starts at BOOT, not just on login, so the daemon survives a reboot
406+ // unattended (verified in a real Ubuntu VM via loginctl). Best-effort:
407+ // if the platform needs privilege, the service still works for the
408+ // logged-in case and `service status` flags the missing linger.
409+ yield * runCommand ( "loginctl" , [ "enable-linger" , username ] , sdEnv ) . pipe ( Effect . ignore ) ;
396410 } ) ,
397411 uninstall : ( ) =>
398412 Effect . gen ( function * ( ) {
399413 const fs = yield * FileSystem . FileSystem ;
400414 const path = yield * Path . Path ;
401- yield * runCommand ( "systemctl" , [ "--user" , "disable" , "--now" , unitName ] ) . pipe (
415+ const sdEnv = { XDG_RUNTIME_DIR : process . env . XDG_RUNTIME_DIR ?? `/run/user/${ currentUid ( ) } ` } ;
416+ yield * runCommand ( "systemctl" , [ "--user" , "disable" , "--now" , unitName ] , sdEnv ) . pipe (
402417 Effect . ignore ,
403418 ) ;
404419 yield * fs . remove ( systemdUnitPath ( path ) , { force : true } ) ;
405- yield * runCommand ( "systemctl" , [ "--user" , "daemon-reload" ] ) . pipe ( Effect . ignore ) ;
420+ yield * runCommand ( "systemctl" , [ "--user" , "daemon-reload" ] , sdEnv ) . pipe ( Effect . ignore ) ;
421+ yield * runCommand ( "loginctl" , [ "disable-linger" , userInfo ( ) . username ] , sdEnv ) . pipe (
422+ Effect . ignore ,
423+ ) ;
406424 } ) ,
407425 status : ( ) =>
408426 Effect . gen ( function * ( ) {
409427 const fs = yield * FileSystem . FileSystem ;
410428 const path = yield * Path . Path ;
429+ const sdEnv = { XDG_RUNTIME_DIR : process . env . XDG_RUNTIME_DIR ?? `/run/user/${ currentUid ( ) } ` } ;
411430 const registered = yield * fs . exists ( systemdUnitPath ( path ) ) ;
412- const active = yield * runCommand ( "systemctl" , [ "--user" , "is-active" , unitName ] ) ;
431+ const active = yield * runCommand ( "systemctl" , [ "--user" , "is-active" , unitName ] , sdEnv ) ;
413432 const running = active . stdout . trim ( ) === "active" ;
433+ const linger = yield * runCommand ( "loginctl" , [
434+ "show-user" ,
435+ userInfo ( ) . username ,
436+ "-p" ,
437+ "Linger" ,
438+ "--value" ,
439+ ] ) ;
440+ const lingerOn = linger . stdout . trim ( ) === "yes" ;
414441 return {
415442 platform : "linux" as const ,
416443 registered,
417444 running,
418445 pid : null ,
419- detail : [ "Linux support is best-effort and unverified; report issues." ] ,
446+ detail : lingerOn
447+ ? [ ]
448+ : [
449+ "Lingering is off — the daemon won't start until you log in. Run `loginctl enable-linger`." ,
450+ ] ,
420451 } ;
421452 } ) ,
422453 restart : ( ) =>
423454 Effect . gen ( function * ( ) {
424- const result = yield * runCommand ( "systemctl" , [ "--user" , "restart" , unitName ] ) ;
455+ const sdEnv = { XDG_RUNTIME_DIR : process . env . XDG_RUNTIME_DIR ?? `/run/user/${ currentUid ( ) } ` } ;
456+ const result = yield * runCommand ( "systemctl" , [ "--user" , "restart" , unitName ] , sdEnv ) ;
425457 if ( result . code !== 0 ) {
426458 return yield * Effect . fail (
427459 new Error (
0 commit comments