Skip to content

Commit 5cba371

Browse files
authored
Merge pull request #171 from shelltime/fix/linux-daemon-install-systemd-session
fix(daemon): set XDG_RUNTIME_DIR for systemctl --user commands on Linux
2 parents 7d2ba1d + 22fa970 commit 5cba371

1 file changed

Lines changed: 64 additions & 9 deletions

File tree

model/daemon-installer.linux.go

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os/user"
1010
"path/filepath"
1111
"text/template"
12+
"time"
1213

1314
"github.com/gookit/color"
1415
)
@@ -26,8 +27,57 @@ func NewLinuxDaemonInstaller(baseFolder, user string) *LinuxDaemonInstaller {
2627
return &LinuxDaemonInstaller{baseFolder: baseFolder, user: user}
2728
}
2829

30+
// getXDGRuntimeDir returns the XDG_RUNTIME_DIR path for the current user
31+
func (l *LinuxDaemonInstaller) getXDGRuntimeDir() string {
32+
// First check if it's already set in environment
33+
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
34+
return dir
35+
}
36+
// Fall back to standard path
37+
return fmt.Sprintf("/run/user/%d", os.Getuid())
38+
}
39+
40+
// ensureUserSystemdSession ensures the user's systemd session is available
41+
// by checking for XDG_RUNTIME_DIR and enabling linger if necessary
42+
func (l *LinuxDaemonInstaller) ensureUserSystemdSession() error {
43+
runtimeDir := l.getXDGRuntimeDir()
44+
45+
// Check if runtime directory exists
46+
if _, err := os.Stat(runtimeDir); err == nil {
47+
return nil // Directory exists, we're good
48+
}
49+
50+
// Try to enable linger to start user systemd session
51+
color.Yellow.Println("🔧 Enabling user session persistence (loginctl enable-linger)...")
52+
cmd := exec.Command("loginctl", "enable-linger", l.user)
53+
if err := cmd.Run(); err != nil {
54+
return fmt.Errorf("failed to enable linger for user %s: %w. Please run: sudo loginctl enable-linger %s", l.user, err, l.user)
55+
}
56+
57+
// Wait for runtime directory to be created (up to 5 seconds)
58+
for i := 0; i < 10; i++ {
59+
time.Sleep(500 * time.Millisecond)
60+
if _, err := os.Stat(runtimeDir); err == nil {
61+
return nil
62+
}
63+
}
64+
65+
return fmt.Errorf("XDG_RUNTIME_DIR (%s) not available. Please log in interactively or run: sudo loginctl enable-linger %s", runtimeDir, l.user)
66+
}
67+
68+
// systemctlUserCmd creates an exec.Cmd for systemctl --user with proper environment
69+
func (l *LinuxDaemonInstaller) systemctlUserCmd(args ...string) *exec.Cmd {
70+
fullArgs := append([]string{"--user"}, args...)
71+
cmd := exec.Command("systemctl", fullArgs...)
72+
73+
// Set up environment with XDG_RUNTIME_DIR
74+
cmd.Env = append(os.Environ(), fmt.Sprintf("XDG_RUNTIME_DIR=%s", l.getXDGRuntimeDir()))
75+
76+
return cmd
77+
}
78+
2979
func (l *LinuxDaemonInstaller) Check() error {
30-
cmd := exec.Command("systemctl", "--user", "is-active", "shelltime")
80+
cmd := l.systemctlUserCmd("is-active", "shelltime")
3181
if err := cmd.Run(); err == nil {
3282
return nil
3383
}
@@ -44,11 +94,11 @@ func (l *LinuxDaemonInstaller) CheckAndStopExistingService() error {
4494
return fmt.Errorf("failed to get current user: %w", err)
4595
}
4696
servicePath := filepath.Join(currentUser.HomeDir, ".config/systemd/user/shelltime.service")
47-
if err := exec.Command("systemctl", "--user", "stop", "shelltime").Run(); err != nil {
97+
if err := l.systemctlUserCmd("stop", "shelltime").Run(); err != nil {
4898
return fmt.Errorf("failed to stop existing service: %w", err)
4999
}
50100
// Also disable to clean up
51-
_ = exec.Command("systemctl", "--user", "disable", "shelltime").Run()
101+
_ = l.systemctlUserCmd("disable", "shelltime").Run()
52102
// Remove old symlink if exists
53103
_ = os.Remove(servicePath)
54104
}
@@ -111,18 +161,23 @@ func (l *LinuxDaemonInstaller) RegisterService() error {
111161
}
112162

113163
func (l *LinuxDaemonInstaller) StartService() error {
164+
// Ensure user systemd session is available
165+
if err := l.ensureUserSystemdSession(); err != nil {
166+
return err
167+
}
168+
114169
color.Yellow.Println("🔄 Reloading systemd...")
115-
if err := exec.Command("systemctl", "--user", "daemon-reload").Run(); err != nil {
170+
if err := l.systemctlUserCmd("daemon-reload").Run(); err != nil {
116171
return fmt.Errorf("failed to reload systemd: %w", err)
117172
}
118173

119174
color.Yellow.Println("✨ Enabling service...")
120-
if err := exec.Command("systemctl", "--user", "enable", "shelltime").Run(); err != nil {
175+
if err := l.systemctlUserCmd("enable", "shelltime").Run(); err != nil {
121176
return fmt.Errorf("failed to enable service: %w", err)
122177
}
123178

124179
color.Yellow.Println("🚀 Starting service...")
125-
if err := exec.Command("systemctl", "--user", "start", "shelltime").Run(); err != nil {
180+
if err := l.systemctlUserCmd("start", "shelltime").Run(); err != nil {
126181
return fmt.Errorf("failed to start service: %w", err)
127182
}
128183
return nil
@@ -142,8 +197,8 @@ func (l *LinuxDaemonInstaller) UnregisterService() error {
142197

143198
color.Yellow.Println("🛑 Stopping and disabling service if running...")
144199
// Try to stop and disable the service
145-
_ = exec.Command("systemctl", "--user", "stop", "shelltime").Run()
146-
_ = exec.Command("systemctl", "--user", "disable", "shelltime").Run()
200+
_ = l.systemctlUserCmd("stop", "shelltime").Run()
201+
_ = l.systemctlUserCmd("disable", "shelltime").Run()
147202

148203
color.Yellow.Println("🗑 Removing service files...")
149204
// Remove symlink from systemd
@@ -152,7 +207,7 @@ func (l *LinuxDaemonInstaller) UnregisterService() error {
152207
}
153208

154209
color.Yellow.Println("🔄 Reloading systemd...")
155-
if err := exec.Command("systemctl", "--user", "daemon-reload").Run(); err != nil {
210+
if err := l.systemctlUserCmd("daemon-reload").Run(); err != nil {
156211
return fmt.Errorf("failed to reload systemd: %w", err)
157212
}
158213

0 commit comments

Comments
 (0)