main
1package cmd
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "text/template"
8 "time"
9
10 "github.com/coreos/go-systemd/v22/dbus"
11 "github.com/spf13/cobra"
12)
13
14func InstallCmd() *cobra.Command {
15 return &cobra.Command{
16 Use: "install",
17 Short: "install as a service",
18 RunE: install,
19 }
20}
21
22func install(cmd *cobra.Command, args []string) error {
23
24 const (
25 _binaryName = "mm"
26 _installDir = "/usr/local/sbin/"
27 _installPath = _installDir + _binaryName
28 _installDirMode = 0o755
29 _installExeMode = 0o755
30 _configDir = "/etc/"
31 _configPath = _configDir + _binaryName + ".conf"
32 _configMode = 0o400
33 )
34
35 exePath, err := os.Executable()
36 if err != nil {
37 return fmt.Errorf("failed to find self binary: %w", err)
38 }
39 exeBytes, err := os.ReadFile(exePath)
40 if err != nil {
41 return fmt.Errorf("failed to read self binary: %w", err)
42 }
43
44 err = os.MkdirAll(_installDir, _installDirMode)
45 if err != nil {
46 return fmt.Errorf(
47 "failed to create install dir=%q: %w",
48 _installDir, err)
49 }
50 err = os.WriteFile(_installPath, exeBytes, _installExeMode)
51 if err != nil {
52 return fmt.Errorf(
53 "failed to write binary path=%q: %w",
54 _installPath, err)
55 }
56
57 const (
58 _serviceDir = "/etc/systemd/system/"
59 _serviceName = _binaryName + ".service"
60 _servicePath = _serviceDir + _serviceName
61 )
62
63 unitFile, err := os.Create(_servicePath)
64 if err != nil {
65 return fmt.Errorf(
66 "failed to create service file path=%q: %w",
67 _servicePath, err)
68 }
69 tmpl := template.Must(template.New("unit").Parse(unitTemplate))
70 err = tmpl.Execute(unitFile, struct {
71 Name string
72 ExecPath string
73 EnvPath string
74 }{
75 Name: _binaryName,
76 ExecPath: _installPath,
77 EnvPath: _configPath,
78 })
79 if err != nil {
80 return fmt.Errorf(
81 "failed to template service file path=%q: %w",
82 _servicePath, err)
83 }
84
85 ctx, cancel := context.WithTimeout(
86 context.Background(), 10*time.Second)
87 defer cancel()
88
89 conn, err := dbus.NewSystemdConnectionContext(ctx)
90 if err != nil {
91 return fmt.Errorf("failed to connect to systemd: %w", err)
92 }
93 defer conn.Close()
94
95 err = conn.ReloadContext(ctx)
96 if err != nil {
97 return fmt.Errorf("failed to reload systemd: %w", err)
98 }
99
100 runtimeOnly := false
101 replaceExisting := true
102 _, _, err = conn.EnableUnitFilesContext(
103 ctx, []string{_serviceName},
104 runtimeOnly, replaceExisting)
105 if err != nil {
106 return fmt.Errorf("failed to enable service name=%q: %w",
107 _serviceName, err)
108 }
109
110 startChan := make(chan string, 1)
111 _, err = conn.StartUnitContext(ctx, _serviceName, "replace", startChan)
112 if err != nil {
113 return fmt.Errorf("failed to start service name=%q: %w",
114 _serviceName, err)
115 }
116
117 select {
118 case <-startChan:
119 break
120 case <-ctx.Done():
121 return fmt.Errorf("timed out starting service")
122 }
123
124 err = os.Remove(exePath)
125 if err != nil {
126 return fmt.Errorf("failed to delete self binary: %w", err)
127 }
128 return nil
129}
130
131var unitTemplate = `
132[Unit]
133Description={{.Name}} service
134After=network.target
135
136[Service]
137Type=simple
138ExecStart={{.ExecPath}}
139EnvironmentFile={{.EnvPath}}
140Restart=on-failure
141RestartSec=5
142
143[Install]
144WantedBy=multi-user.target
145`