Repository navigation
Expand file tree
/
Copy pathrunner_install.go
More file actions
291 lines (269 loc) · 7.41 KB
/
Copy pathrunner_install.go
File metadata and controls
291 lines (269 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
package main
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
)
const (
launchAgentLabel = "com.gandermd.gander.runner"
launchAgentName = "com.gandermd.gander.runner.plist"
systemdService = "gander.service"
launchAgentLogDir = ".gander"
)
// isInstalled reports whether the OS-level auto-start unit exists.
func isInstalled() (bool, error) {
path, err := unitPath()
if err != nil {
return false, err
}
_, err = os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// runRunnerInstall writes the OS-level auto-start unit pointing at the
// running binary and asks the supervisor to load+enable it. Idempotent:
// re-running has no effect if the file is unchanged.
func runRunnerInstall(quiet bool) error {
bin, err := os.Executable()
if err != nil {
return fmt.Errorf("locate binary: %w", err)
}
if resolved, err := filepath.EvalSymlinks(bin); err == nil {
bin = resolved
}
if isTestExecutable(bin) {
return fmt.Errorf("refusing to install auto-start for test binary %s", bin)
}
if fi, err := os.Stat(bin); err != nil || fi.Mode()&0111 == 0 {
return fmt.Errorf("%s is not an executable", bin)
}
path, err := unitPath()
if err != nil {
return err
}
content, err := renderUnit(bin)
if err != nil {
return err
}
existing, _ := os.ReadFile(path)
if bytes.Equal(existing, []byte(content)) {
if !quiet {
fmt.Printf("runner: auto-start unit already installed at %s\n", path)
}
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("mkdir for unit: %w", err)
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
return fmt.Errorf("write %s: %w", tmp, err)
}
if err := os.Rename(tmp, path); err != nil {
return fmt.Errorf("install %s: %w", path, err)
}
// Pre-create log files with mode 0600 so launchd/systemd don't
// create them with default umask (022). Best-effort: if they
// already exist we leave them alone.
ensureLogFiles0600(filepath.Dir(path))
return loadAndEnable(path, quiet)
}
func runRunnerUninstall(quiet bool) error {
path, err := unitPath()
if err != nil {
return err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
if !quiet {
fmt.Printf("runner: no unit at %s; nothing to do\n", path)
}
return nil
}
if err := unloadAndDisable(path, quiet); err != nil {
// Continue with file removal even if unload errors.
if !quiet {
fmt.Printf("runner: unload: %v (continuing to remove file)\n", err)
}
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
if !quiet {
fmt.Printf("runner: auto-start unit removed: %s\n", path)
}
return nil
}
// autoInstallIfNeeded writes the unit on the first-spawn path so the
// daemon is up at every login; called from ensureRunner's spawn path.
// Errors are logged but never block the user's command.
func autoInstallIfNeeded() {
if exe, err := os.Executable(); err != nil || isTestExecutable(exe) {
return
}
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
return
}
installed, err := isInstalled()
if err != nil || installed {
return
}
if err := runRunnerInstall(true); err != nil {
log.Printf("runner: auto-install: %v", err)
} else {
log.Printf("runner: auto-installed %s unit (re-runs on login)", unitLabel())
}
}
func unitLabel() string {
if runtime.GOOS == "darwin" {
return "LaunchAgent " + launchAgentLabel
}
if runtime.GOOS == "linux" {
return "systemd user " + systemdService
}
return "auto-start"
}
func unitPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
switch runtime.GOOS {
case "darwin":
return filepath.Join(home, "Library", "LaunchAgents", launchAgentName), nil
case "linux":
return filepath.Join(home, ".config", "systemd", "user", systemdService), nil
default:
return "", fmt.Errorf("auto-start not supported on %s (only macOS and Linux)", runtime.GOOS)
}
}
func renderUnit(bin string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
ganderLog := filepath.Join(home, ".gander", "runner.log")
ganderErr := filepath.Join(home, ".gander", "runner.err")
switch runtime.GOOS {
case "darwin":
return fmt.Sprintf(launchAgentPlist, launchAgentLabel, bin, ganderLog, ganderErr), nil
case "linux":
return fmt.Sprintf(systemdUnit, bin), nil
default:
return "", fmt.Errorf("auto-start not supported on %s", runtime.GOOS)
}
}
const launchAgentPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>%s</string>
<key>ProgramArguments</key>
<array>
<string>%s</string>
<string>_serve</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>10</integer>
<key>StandardOutPath</key><string>%s</string>
<key>StandardErrorPath</key><string>%s</string>
</dict></plist>
`
const systemdUnit = `[Unit]
Description=gander watcher runner
After=network.target
[Service]
Type=simple
ExecStart=%s _serve
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
`
func ensureLogFiles0600(dir string) {
for _, name := range []string{"runner.log", "runner.err"} {
p := filepath.Join(dir, name)
if _, err := os.Stat(p); err == nil {
_ = os.Chmod(p, 0600)
continue
}
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
continue
}
_ = f.Close()
}
}
func loadAndEnable(unitPath string, quiet bool) error {
switch runtime.GOOS {
case "darwin":
if _, err := exec.LookPath("launchctl"); err != nil {
return errors.New("launchctl not found; cannot auto-start the daemon")
}
cmd := exec.Command("launchctl", "load", "-w", unitPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("launchctl load: %w", err)
}
if !quiet {
fmt.Printf("runner: %s installed and loaded; daemon auto-starts on login\n", unitPath)
}
return nil
case "linux":
if _, err := exec.LookPath("systemctl"); err != nil {
return errors.New("systemctl not found; cannot auto-start the daemon")
}
reload := exec.Command("systemctl", "--user", "daemon-reload")
reload.Stdout = os.Stdout
reload.Stderr = os.Stderr
if err := reload.Run(); err != nil {
return fmt.Errorf("systemctl daemon-reload: %w", err)
}
enable := exec.Command("systemctl", "--user", "enable", "--now", systemdService)
enable.Stdout = os.Stdout
enable.Stderr = os.Stderr
if err := enable.Run(); err != nil {
return fmt.Errorf("systemctl enable --now: %w", err)
}
if !quiet {
fmt.Printf("runner: %s installed and enabled; daemon auto-starts on login\n", unitPath)
}
return nil
default:
return fmt.Errorf("auto-start not supported on %s", runtime.GOOS)
}
}
func unloadAndDisable(unitPath string, quiet bool) error {
switch runtime.GOOS {
case "darwin":
if _, err := exec.LookPath("launchctl"); err != nil {
return nil
}
cmd := exec.Command("launchctl", "unload", unitPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
return nil
case "linux":
if _, err := exec.LookPath("systemctl"); err != nil {
return nil
}
cmd := exec.Command("systemctl", "--user", "disable", "--now", systemdService)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
return nil
default:
return nil
}
}