Skip to content

Commit c1aaa9f

Browse files
feat(daemon): support user-scope systemd service installation on Linux
Changed Linux daemon installation from system-wide (root) to user-scope to match the Darwin implementation pattern and eliminate root permission requirements. Changes: - Updated all systemctl commands to use --user flag - Changed service path from /etc/systemd/system/ to ~/.config/systemd/user/ - Removed User=root and Group=root from service template - Changed WantedBy target from multi-user.target to default.target - Updated service management methods to use user-scope paths Closes #116 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Le He <AnnatarHe@users.noreply.github.com>
1 parent 2a6002a commit c1aaa9f

2 files changed

Lines changed: 45 additions & 19 deletions

File tree

model/daemon-installer.linux.go

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"os/exec"
9+
"os/user"
910
"path/filepath"
1011
"text/template"
1112

@@ -26,7 +27,7 @@ func NewLinuxDaemonInstaller(baseFolder, user string) *LinuxDaemonInstaller {
2627
}
2728

2829
func (l *LinuxDaemonInstaller) Check() error {
29-
cmd := exec.Command("systemctl", "is-active", "shelltime")
30+
cmd := exec.Command("systemctl", "--user", "is-active", "shelltime")
3031
if err := cmd.Run(); err == nil {
3132
return nil
3233
}
@@ -36,13 +37,20 @@ func (l *LinuxDaemonInstaller) Check() error {
3637
func (l *LinuxDaemonInstaller) CheckAndStopExistingService() error {
3738
color.Yellow.Println("🔍 Checking if service is running...")
3839

39-
if err := l.Check(); err != nil {
40-
return err
41-
}
42-
43-
color.Yellow.Println("🛑 Stopping existing service...")
44-
if err := exec.Command("systemctl", "stop", "shelltime").Run(); err != nil {
45-
return fmt.Errorf("failed to stop existing service: %w", err)
40+
if err := l.Check(); err == nil {
41+
color.Yellow.Println("🛑 Stopping existing service...")
42+
currentUser, err := user.Current()
43+
if err != nil {
44+
return fmt.Errorf("failed to get current user: %w", err)
45+
}
46+
servicePath := filepath.Join(currentUser.HomeDir, ".config/systemd/user/shelltime.service")
47+
if err := exec.Command("systemctl", "--user", "stop", "shelltime").Run(); err != nil {
48+
return fmt.Errorf("failed to stop existing service: %w", err)
49+
}
50+
// Also disable to clean up
51+
_ = exec.Command("systemctl", "--user", "disable", "shelltime").Run()
52+
// Remove old symlink if exists
53+
_ = os.Remove(servicePath)
4654
}
4755
return nil
4856
}
@@ -80,7 +88,19 @@ func (l *LinuxDaemonInstaller) RegisterService() error {
8088
if l.baseFolder == "" {
8189
return fmt.Errorf("base folder is not set")
8290
}
83-
servicePath := "/etc/systemd/system/shelltime.service"
91+
92+
currentUser, err := user.Current()
93+
if err != nil {
94+
return fmt.Errorf("failed to get current user: %w", err)
95+
}
96+
97+
// Create systemd user directory if it doesn't exist
98+
systemdUserDir := filepath.Join(currentUser.HomeDir, ".config/systemd/user")
99+
if err := os.MkdirAll(systemdUserDir, 0755); err != nil {
100+
return fmt.Errorf("failed to create systemd user directory: %w", err)
101+
}
102+
103+
servicePath := filepath.Join(systemdUserDir, "shelltime.service")
84104
if _, err := os.Stat(servicePath); err != nil {
85105
sourceFile := filepath.Join(l.baseFolder, "daemon/shelltime.service")
86106
if err := os.Symlink(sourceFile, servicePath); err != nil {
@@ -92,17 +112,17 @@ func (l *LinuxDaemonInstaller) RegisterService() error {
92112

93113
func (l *LinuxDaemonInstaller) StartService() error {
94114
color.Yellow.Println("🔄 Reloading systemd...")
95-
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
115+
if err := exec.Command("systemctl", "--user", "daemon-reload").Run(); err != nil {
96116
return fmt.Errorf("failed to reload systemd: %w", err)
97117
}
98118

99119
color.Yellow.Println("✨ Enabling service...")
100-
if err := exec.Command("systemctl", "enable", "shelltime").Run(); err != nil {
120+
if err := exec.Command("systemctl", "--user", "enable", "shelltime").Run(); err != nil {
101121
return fmt.Errorf("failed to enable service: %w", err)
102122
}
103123

104124
color.Yellow.Println("🚀 Starting service...")
105-
if err := exec.Command("systemctl", "start", "shelltime").Run(); err != nil {
125+
if err := exec.Command("systemctl", "--user", "start", "shelltime").Run(); err != nil {
106126
return fmt.Errorf("failed to start service: %w", err)
107127
}
108128
return nil
@@ -112,19 +132,27 @@ func (l *LinuxDaemonInstaller) UnregisterService() error {
112132
if l.baseFolder == "" {
113133
return fmt.Errorf("base folder is not set")
114134
}
135+
136+
currentUser, err := user.Current()
137+
if err != nil {
138+
return fmt.Errorf("failed to get current user: %w", err)
139+
}
140+
141+
servicePath := filepath.Join(currentUser.HomeDir, ".config/systemd/user/shelltime.service")
142+
115143
color.Yellow.Println("🛑 Stopping and disabling service if running...")
116144
// Try to stop and disable the service
117-
_ = exec.Command("systemctl", "stop", "shelltime").Run()
118-
_ = exec.Command("systemctl", "disable", "shelltime").Run()
145+
_ = exec.Command("systemctl", "--user", "stop", "shelltime").Run()
146+
_ = exec.Command("systemctl", "--user", "disable", "shelltime").Run()
119147

120148
color.Yellow.Println("🗑 Removing service files...")
121149
// Remove symlink from systemd
122-
if err := os.Remove("/etc/systemd/system/shelltime.service"); err != nil && !os.IsNotExist(err) {
150+
if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) {
123151
return fmt.Errorf("failed to remove systemd service symlink: %w", err)
124152
}
125153

126154
color.Yellow.Println("🔄 Reloading systemd...")
127-
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
155+
if err := exec.Command("systemctl", "--user", "daemon-reload").Run(); err != nil {
128156
return fmt.Errorf("failed to reload systemd: %w", err)
129157
}
130158

model/sys-desc/shelltime.service

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ After=network.target
66
Type=simple
77
ExecStart=/usr/local/bin/shelltime-daemon
88
Restart=always
9-
User=root
10-
Group=root
119

1210
[Install]
13-
WantedBy=multi-user.target
11+
WantedBy=default.target

0 commit comments

Comments
 (0)