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	"errors"
9	"fmt"
10	"os"
11	"os/signal"
12	"regexp"
13	"strings"
14	"syscall"
15	"text/template"
16)
17
18func isUpstart() bool {
19	if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil {
20		return true
21	}
22	if _, err := os.Stat("/sbin/initctl"); err == nil {
23		if _, out, err := runWithOutput("/sbin/initctl", "--version"); err == nil {
24			if strings.Contains(out, "initctl (upstart") {
25				return true
26			}
27		}
28	}
29	return false
30}
31
32type upstart struct {
33	i        Interface
34	platform string
35	*Config
36}
37
38func newUpstartService(i Interface, platform string, c *Config) (Service, error) {
39	s := &upstart{
40		i:        i,
41		platform: platform,
42		Config:   c,
43	}
44
45	return s, nil
46}
47
48func (s *upstart) String() string {
49	if len(s.DisplayName) > 0 {
50		return s.DisplayName
51	}
52	return s.Name
53}
54
55func (s *upstart) Platform() string {
56	return s.platform
57}
58
59// Upstart has some support for user services in graphical sessions.
60// Due to the mix of actual support for user services over versions, just don't bother.
61// Upstart will be replaced by systemd in most cases anyway.
62var errNoUserServiceUpstart = errors.New("User services are not supported on Upstart.")
63
64func (s *upstart) configPath() (cp string, err error) {
65	if s.Option.bool(optionUserService, optionUserServiceDefault) {
66		err = errNoUserServiceUpstart
67		return
68	}
69	cp = "/etc/init/" + s.Config.Name + ".conf"
70	return
71}
72
73func (s *upstart) hasKillStanza() bool {
74	defaultValue := true
75	version := s.getUpstartVersion()
76	if version == nil {
77		return defaultValue
78	}
79
80	maxVersion := []int{0, 6, 5}
81	if matches, err := versionAtMost(version, maxVersion); err != nil || matches {
82		return false
83	}
84
85	return defaultValue
86}
87
88func (s *upstart) hasSetUIDStanza() bool {
89	defaultValue := true
90	version := s.getUpstartVersion()
91	if version == nil {
92		return defaultValue
93	}
94
95	maxVersion := []int{1, 4, 0}
96	if matches, err := versionAtMost(version, maxVersion); err != nil || matches {
97		return false
98	}
99
100	return defaultValue
101}
102
103func (s *upstart) getUpstartVersion() []int {
104	_, out, err := runWithOutput("/sbin/initctl", "--version")
105	if err != nil {
106		return nil
107	}
108
109	re := regexp.MustCompile(`initctl \(upstart (\d+.\d+.\d+)\)`)
110	matches := re.FindStringSubmatch(out)
111	if len(matches) != 2 {
112		return nil
113	}
114
115	return parseVersion(matches[1])
116}
117
118func (s *upstart) template() *template.Template {
119	customScript := s.Option.string(optionUpstartScript, "")
120
121	if customScript != "" {
122		return template.Must(template.New("").Funcs(tf).Parse(customScript))
123	} else {
124		return template.Must(template.New("").Funcs(tf).Parse(upstartScript))
125	}
126}
127
128func (s *upstart) Install() error {
129	confPath, err := s.configPath()
130	if err != nil {
131		return err
132	}
133	_, err = os.Stat(confPath)
134	if err == nil {
135		return fmt.Errorf("Init already exists: %s", confPath)
136	}
137
138	f, err := os.Create(confPath)
139	if err != nil {
140		return err
141	}
142	defer f.Close()
143
144	path, err := s.execPath()
145	if err != nil {
146		return err
147	}
148
149	var to = &struct {
150		*Config
151		Path            string
152		HasKillStanza   bool
153		HasSetUIDStanza bool
154		LogOutput       bool
155	}{
156		s.Config,
157		path,
158		s.hasKillStanza(),
159		s.hasSetUIDStanza(),
160		s.Option.bool(optionLogOutput, optionLogOutputDefault),
161	}
162
163	return s.template().Execute(f, to)
164}
165
166func (s *upstart) Uninstall() error {
167	cp, err := s.configPath()
168	if err != nil {
169		return err
170	}
171	if err := os.Remove(cp); err != nil {
172		return err
173	}
174	return nil
175}
176
177func (s *upstart) Logger(errs chan<- error) (Logger, error) {
178	if system.Interactive() {
179		return ConsoleLogger, nil
180	}
181	return s.SystemLogger(errs)
182}
183func (s *upstart) SystemLogger(errs chan<- error) (Logger, error) {
184	return newSysLogger(s.Name, errs)
185}
186
187func (s *upstart) Run() (err error) {
188	err = s.i.Start(s)
189	if err != nil {
190		return err
191	}
192
193	s.Option.funcSingle(optionRunWait, func() {
194		var sigChan = make(chan os.Signal, 3)
195		signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
196		<-sigChan
197	})()
198
199	return s.i.Stop(s)
200}
201
202func (s *upstart) Status() (Status, error) {
203	exitCode, out, err := runWithOutput("initctl", "status", s.Name)
204	if exitCode == 0 && err != nil {
205		return StatusUnknown, err
206	}
207
208	switch {
209	case strings.HasPrefix(out, fmt.Sprintf("%s start/running", s.Name)):
210		return StatusRunning, nil
211	case strings.HasPrefix(out, fmt.Sprintf("%s stop/waiting", s.Name)):
212		return StatusStopped, nil
213	default:
214		return StatusUnknown, ErrNotInstalled
215	}
216}
217
218func (s *upstart) Start() error {
219	return run("initctl", "start", s.Name)
220}
221
222func (s *upstart) Stop() error {
223	return run("initctl", "stop", s.Name)
224}
225
226func (s *upstart) Restart() error {
227	return run("initctl", "restart", s.Name)
228}
229
230// The upstart script should stop with an INT or the Go runtime will terminate
231// the program before the Stop handler can run.
232const upstartScript = `# {{.Description}}
233
234{{if .DisplayName}}description    "{{.DisplayName}}"{{end}}
235
236{{if .HasKillStanza}}kill signal INT{{end}}
237{{if .ChRoot}}chroot {{.ChRoot}}{{end}}
238{{if .WorkingDirectory}}chdir {{.WorkingDirectory}}{{end}}
239start on filesystem or runlevel [2345]
240stop on runlevel [!2345]
241
242{{if and .UserName .HasSetUIDStanza}}setuid {{.UserName}}{{end}}
243
244respawn
245respawn limit 10 5
246umask 022
247
248console none
249
250pre-start script
251    test -x {{.Path}} || { stop; exit 0; }
252end script
253
254# Start
255script
256	{{if .LogOutput}}
257	stdout_log="/var/log/{{.Name}}.out"
258	stderr_log="/var/log/{{.Name}}.err"
259	{{end}}
260
261	if [ -f "/etc/sysconfig/{{.Name}}" ]; then
262		set -a
263		source /etc/sysconfig/{{.Name}}
264		set +a
265	fi
266
267	exec {{if and .UserName (not .HasSetUIDStanza)}}sudo -E -u {{.UserName}} {{end}}{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}{{if .LogOutput}} >> $stdout_log 2>> $stderr_log{{end}}
268end script
269`
270