diff --git a/README.md b/README.md index be41978..a5188b1 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Notes: - Status is read via `systemctl --user show `. - Logs are read via `journalctl --user -u `. - Both commands are executed as the workspace Unix user via `sudo -n -u ...`. +- The app also auto-runs `loginctl enable-linger ` (via sudo) to keep user systemd available without active login. ## Systemd diff --git a/internal/app/deploy_service.go b/internal/app/deploy_service.go index 1dba0cf..11e9fa0 100644 --- a/internal/app/deploy_service.go +++ b/internal/app/deploy_service.go @@ -89,5 +89,5 @@ func (a *App) deployProjectFromMain(_ context.Context, p Project) error { return err } } - return runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "restart", p.ServiceName) + return runSystemctlUser(p.UnixUser, "restart", p.ServiceName) } diff --git a/internal/app/systemd_service.go b/internal/app/systemd_service.go index f0fab29..4c676ed 100644 --- a/internal/app/systemd_service.go +++ b/internal/app/systemd_service.go @@ -81,8 +81,8 @@ WantedBy=default.target if err := os.WriteFile(servicePath, []byte(unit), 0o644); err != nil { return err } - _ = runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "daemon-reload") - _ = runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "enable", "--now", p.ServiceName) + _ = runSystemctlUser(p.UnixUser, "daemon-reload") + _ = runSystemctlUser(p.UnixUser, "enable", "--now", p.ServiceName) return nil } @@ -97,6 +97,59 @@ func homeForUnixUser(username string) (string, error) { return u.HomeDir, nil } +func userRuntimeEnv(username string) (string, string, error) { + u, err := user.Lookup(username) + if err != nil { + return "", "", fmt.Errorf("lookup unix user %q failed: %w", username, err) + } + runtimeDir := filepath.Join("/run/user", u.Uid) + busAddr := "unix:path=" + filepath.Join(runtimeDir, "bus") + return runtimeDir, busAddr, nil +} + +func runSystemctlUser(username string, args ...string) error { + if err := ensureLingerEnabled(username); err != nil { + return err + } + runtimeDir, busAddr, err := userRuntimeEnv(username) + if err != nil { + return err + } + cmdArgs := []string{ + "-n", "-u", username, + "env", + "XDG_RUNTIME_DIR=" + runtimeDir, + "DBUS_SESSION_BUS_ADDRESS=" + busAddr, + "systemctl", "--user", + } + cmdArgs = append(cmdArgs, args...) + return runCmd("sudo", cmdArgs...) +} + +func runJournalctlUser(username string, args ...string) (string, error) { + if err := ensureLingerEnabled(username); err != nil { + return "", err + } + runtimeDir, busAddr, err := userRuntimeEnv(username) + if err != nil { + return "", err + } + cmdArgs := []string{ + "-n", "-u", username, + "env", + "XDG_RUNTIME_DIR=" + runtimeDir, + "DBUS_SESSION_BUS_ADDRESS=" + busAddr, + "journalctl", "--user", + } + cmdArgs = append(cmdArgs, args...) + return runCmdOut("sudo", cmdArgs...) +} + +func ensureLingerEnabled(username string) error { + // Idempotent; safe to call repeatedly. + return runCmd("sudo", "loginctl", "enable-linger", username) +} + type ServiceStatus struct { Unit string `json:"unit"` ActiveState string `json:"active_state"` @@ -108,11 +161,20 @@ type ServiceStatus struct { } func (a *App) getServiceStatus(p Project) (ServiceStatus, error) { - out, err := runCmdOut( - "sudo", "-n", "-u", p.UnixUser, - "systemctl", "--user", "show", p.ServiceName, "--no-pager", - "--property=ActiveState,SubState,MainPID,ExecMainStatus,Result,FragmentPath", - ) + out, err := func() (string, error) { + runtimeDir, busAddr, err := userRuntimeEnv(p.UnixUser) + if err != nil { + return "", err + } + return runCmdOut( + "sudo", "-n", "-u", p.UnixUser, + "env", + "XDG_RUNTIME_DIR="+runtimeDir, + "DBUS_SESSION_BUS_ADDRESS="+busAddr, + "systemctl", "--user", "show", p.ServiceName, "--no-pager", + "--property=ActiveState,SubState,MainPID,ExecMainStatus,Result,FragmentPath", + ) + }() if err != nil { return ServiceStatus{}, err } @@ -142,8 +204,5 @@ func (a *App) getServiceStatus(p Project) (ServiceStatus, error) { } func (a *App) getServiceLogs(p Project, lines int) (string, error) { - return runCmdOut( - "sudo", "-n", "-u", p.UnixUser, - "journalctl", "--user", "-u", p.ServiceName, "--no-pager", "-n", strconv.Itoa(lines), "-o", "short-iso", - ) + return runJournalctlUser(p.UnixUser, "-u", p.ServiceName, "--no-pager", "-n", strconv.Itoa(lines), "-o", "short-iso") }