1// Copyright 2015 Daniel Theophanes.
2// Use of this source code is governed by a zlib-style
3// license that can be found in the LICENSE file.
4
5package service
6
7import (
8	"bytes"
9	"errors"
10	"fmt"
11	"os"
12	"os/signal"
13	"path/filepath"
14	"regexp"
15	"strconv"
16	"strings"
17	"syscall"
18	"text/template"
19)
20
21func isSystemd() bool {
22	if _, err := os.Stat("/run/systemd/system"); err == nil {
23		return true
24	}
25	if _, err := os.Stat("/proc/1/comm"); err == nil {
26		filerc, err := os.Open("/proc/1/comm")
27		if err != nil {
28			return false
29		}
30		defer filerc.Close()
31
32		buf := new(bytes.Buffer)
33		buf.ReadFrom(filerc)
34		contents := buf.String()
35
36		if strings.Trim(contents, " \r\n") == "systemd" {
37			return true
38		}
39	}
40	return false
41}
42
43type systemd struct {
44	i        Interface
45	platform string
46	*Config
47}
48
49func newSystemdService(i Interface, platform string, c *Config) (Service, error) {
50	s := &systemd{
51		i:        i,
52		platform: platform,
53		Config:   c,
54	}
55
56	return s, nil
57}
58
59func (s *systemd) String() string {
60	if len(s.DisplayName) > 0 {
61		return s.DisplayName
62	}
63	return s.Name
64}
65
66func (s *systemd) Platform() string {
67	return s.platform
68}
69
70func (s *systemd) configPath() (cp string, err error) {
71	if !s.isUserService() {
72		cp = "/etc/systemd/system/" + s.Config.Name + ".service"
73		return
74	}
75	homeDir, err := os.UserHomeDir()
76	if err != nil {
77		return
78	}
79	systemdUserDir := filepath.Join(homeDir, ".config/systemd/user")
80	err = os.MkdirAll(systemdUserDir, os.ModePerm)
81	if err != nil {
82		return
83	}
84	cp = filepath.Join(systemdUserDir, s.Config.Name+".service")
85	return
86}
87
88func (s *systemd) getSystemdVersion() int64 {
89	_, out, err := runWithOutput("systemctl", "--version")
90	if err != nil {
91		return -1
92	}
93
94	re := regexp.MustCompile(`systemd ([0-9]+)`)
95	matches := re.FindStringSubmatch(out)
96	if len(matches) != 2 {
97		return -1
98	}
99
100	v, err := strconv.ParseInt(matches[1], 10, 64)
101	if err != nil {
102		return -1
103	}
104
105	return v
106}
107
108func (s *systemd) hasOutputFileSupport() bool {
109	defaultValue := true
110	version := s.getSystemdVersion()
111	if version == -1 {
112		return defaultValue
113	}
114
115	if version < 236 {
116		return false
117	}
118
119	return defaultValue
120}
121
122func (s *systemd) template() *template.Template {
123	customScript := s.Option.string(optionSystemdScript, "")
124
125	if customScript != "" {
126		return template.Must(template.New("").Funcs(tf).Parse(customScript))
127	} else {
128		return template.Must(template.New("").Funcs(tf).Parse(systemdScript))
129	}
130}
131
132func (s *systemd) isUserService() bool {
133	return s.Option.bool(optionUserService, optionUserServiceDefault)
134}
135
136func (s *systemd) Install() error {
137	confPath, err := s.configPath()
138	if err != nil {
139		return err
140	}
141	_, err = os.Stat(confPath)
142	if err == nil {
143		return fmt.Errorf("Init already exists: %s", confPath)
144	}
145
146	f, err := os.OpenFile(confPath, os.O_WRONLY|os.O_CREATE, 0644)
147	if err != nil {
148		return err
149	}
150	defer f.Close()
151
152	path, err := s.execPath()
153	if err != nil {
154		return err
155	}
156
157	var to = &struct {
158		*Config
159		Path                 string
160		HasOutputFileSupport bool
161		ReloadSignal         string
162		PIDFile              string
163		LimitNOFILE          int
164		Restart              string
165		SuccessExitStatus    string
166		LogOutput            bool
167	}{
168		s.Config,
169		path,
170		s.hasOutputFileSupport(),
171		s.Option.string(optionReloadSignal, ""),
172		s.Option.string(optionPIDFile, ""),
173		s.Option.int(optionLimitNOFILE, optionLimitNOFILEDefault),
174		s.Option.string(optionRestart, "always"),
175		s.Option.string(optionSuccessExitStatus, ""),
176		s.Option.bool(optionLogOutput, optionLogOutputDefault),
177	}
178
179	err = s.template().Execute(f, to)
180	if err != nil {
181		return err
182	}
183
184	err = s.runAction("enable")
185	if err != nil {
186		return err
187	}
188
189	return s.run("daemon-reload")
190}
191
192func (s *systemd) Uninstall() error {
193	err := s.runAction("disable")
194	if err != nil {
195		return err
196	}
197	cp, err := s.configPath()
198	if err != nil {
199		return err
200	}
201	if err := os.Remove(cp); err != nil {
202		return err
203	}
204	return nil
205}
206
207func (s *systemd) Logger(errs chan<- error) (Logger, error) {
208	if system.Interactive() {
209		return ConsoleLogger, nil
210	}
211	return s.SystemLogger(errs)
212}
213func (s *systemd) SystemLogger(errs chan<- error) (Logger, error) {
214	return newSysLogger(s.Name, errs)
215}
216
217func (s *systemd) Run() (err error) {
218	err = s.i.Start(s)
219	if err != nil {
220		return err
221	}
222
223	s.Option.funcSingle(optionRunWait, func() {
224		var sigChan = make(chan os.Signal, 3)
225		signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
226		<-sigChan
227	})()
228
229	return s.i.Stop(s)
230}
231
232func (s *systemd) Status() (Status, error) {
233	exitCode, out, err := runWithOutput("systemctl", "is-active", s.Name)
234	if exitCode == 0 && err != nil {
235		return StatusUnknown, err
236	}
237
238	switch {
239	case strings.HasPrefix(out, "active"):
240		return StatusRunning, nil
241	case strings.HasPrefix(out, "inactive"):
242		// inactive can also mean its not installed, check unit files
243		exitCode, out, err := runWithOutput("systemctl", "list-unit-files", "-t", "service", s.Name)
244		if exitCode == 0 && err != nil {
245			return StatusUnknown, err
246		}
247		if strings.Contains(out, s.Name) {
248			// unit file exists, installed but not running
249			return StatusStopped, nil
250		}
251		// no unit file
252		return StatusUnknown, ErrNotInstalled
253	case strings.HasPrefix(out, "activating"):
254		return StatusRunning, nil
255	case strings.HasPrefix(out, "failed"):
256		return StatusUnknown, errors.New("service in failed state")
257	default:
258		return StatusUnknown, ErrNotInstalled
259	}
260}
261
262func (s *systemd) Start() error {
263	return s.runAction("start")
264}
265
266func (s *systemd) Stop() error {
267	return s.runAction("stop")
268}
269
270func (s *systemd) Restart() error {
271	return s.runAction("restart")
272}
273
274func (s *systemd) run(action string, args ...string) error {
275	if s.isUserService() {
276		return run("systemctl", append([]string{action, "--user"}, args...)...)
277	}
278	return run("systemctl", append([]string{action}, args...)...)
279}
280
281func (s *systemd) runAction(action string) error {
282	return s.run(action, s.Name+".service")
283}
284
285const systemdScript = `[Unit]
286Description={{.Description}}
287ConditionFileIsExecutable={{.Path|cmdEscape}}
288{{range $i, $dep := .Dependencies}}
289{{$dep}} {{end}}
290
291[Service]
292StartLimitInterval=5
293StartLimitBurst=10
294ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
295{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}}
296{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}
297{{if .UserName}}User={{.UserName}}{{end}}
298{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}}
299{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}
300{{if and .LogOutput .HasOutputFileSupport -}}
301StandardOutput=file:/var/log/{{.Name}}.out
302StandardError=file:/var/log/{{.Name}}.err
303{{- end}}
304{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}}
305{{if .Restart}}Restart={{.Restart}}{{end}}
306{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}}
307RestartSec=120
308EnvironmentFile=-/etc/sysconfig/{{.Name}}
309
310[Install]
311WantedBy=multi-user.target
312`
313