main
go 117 lines 2.77 KB
Raw
1 //go:build linux
2
3 package service
4
5 import (
6 "context"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 )
13
14 func Install(ctx context.Context, def Definition) error {
15 unitPath, userMode, err := linuxUnitPath(def.Name)
16 if err != nil {
17 return err
18 }
19 if err := os.MkdirAll(filepath.Dir(unitPath), 0o755); err != nil {
20 return err
21 }
22 if err := os.WriteFile(unitPath, []byte(systemdUnit(def)), 0o644); err != nil {
23 return err
24 }
25 if err := runSystemctl(ctx, userMode, "daemon-reload"); err != nil {
26 return err
27 }
28 return runSystemctl(ctx, userMode, "enable", def.Name+".service")
29 }
30
31 func Start(ctx context.Context, name string) error {
32 _, userMode, err := linuxUnitPath(name)
33 if err != nil {
34 return err
35 }
36 return runSystemctl(ctx, userMode, "start", name+".service")
37 }
38
39 func Stop(ctx context.Context, name string) error {
40 _, userMode, err := linuxUnitPath(name)
41 if err != nil {
42 return err
43 }
44 return runSystemctl(ctx, userMode, "stop", name+".service")
45 }
46
47 func StopDisable(ctx context.Context, name string) error {
48 _, userMode, err := linuxUnitPath(name)
49 if err != nil {
50 return err
51 }
52 return runSystemctl(ctx, userMode, "disable", "--now", name+".service")
53 }
54
55 func Run(ctx context.Context, name string, run func(context.Context) error) error {
56 return run(ctx)
57 }
58
59 func linuxUnitPath(name string) (string, bool, error) {
60 if os.Geteuid() == 0 {
61 return filepath.Join("/etc/systemd/system", name+".service"), false, nil
62 }
63 home, err := os.UserHomeDir()
64 if err != nil {
65 return "", false, err
66 }
67 return filepath.Join(home, ".config", "systemd", "user", name+".service"), true, nil
68 }
69
70 func systemctlArgs(userMode bool, args ...string) []string {
71 if userMode {
72 return append([]string{"--user"}, args...)
73 }
74 return args
75 }
76
77 func runSystemctl(ctx context.Context, userMode bool, args ...string) error {
78 commandArgs := systemctlArgs(userMode, args...)
79 output, err := exec.CommandContext(ctx, "systemctl", commandArgs...).CombinedOutput()
80 if err == nil {
81 return nil
82 }
83 message := "systemctl " + strings.Join(commandArgs, " ")
84 if detail := strings.TrimSpace(string(output)); detail != "" {
85 message += ": " + detail
86 }
87 if userMode {
88 message += "; user systemd must be available for managed agent mode"
89 }
90 return fmt.Errorf("%s: %w", message, err)
91 }
92
93 func systemdUnit(def Definition) string {
94 parts := append([]string{def.Executable}, def.Args...)
95 for i := range parts {
96 parts[i] = shellQuote(parts[i])
97 }
98 return fmt.Sprintf(`[Unit]
99 Description=%s
100 After=network-online.target
101 Wants=network-online.target
102
103 [Service]
104 Type=simple
105 WorkingDirectory=%s
106 ExecStart=%s
107 Restart=always
108 RestartSec=5
109
110 [Install]
111 WantedBy=default.target
112 `, def.Description, shellQuote(def.WorkingDir), strings.Join(parts, " "))
113 }
114
115 func shellQuote(value string) string {
116 return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
117 }