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	"encoding/xml"
10	"fmt"
11	"os"
12	"os/signal"
13	"regexp"
14	"syscall"
15	"text/template"
16	"time"
17)
18
19const maxPathSize = 32 * 1024
20
21const version = "solaris-smf"
22
23type solarisSystem struct{}
24
25func (solarisSystem) String() string {
26	return version
27}
28func (solarisSystem) Detect() bool {
29	return true
30}
31func (solarisSystem) Interactive() bool {
32	return interactive
33}
34func (solarisSystem) New(i Interface, c *Config) (Service, error) {
35	s := &solarisService{
36		i:      i,
37		Config: c,
38
39		Prefix: c.Option.string(optionPrefix, optionPrefixDefault),
40	}
41
42	return s, nil
43}
44
45func init() {
46	ChooseSystem(solarisSystem{})
47}
48
49var interactive = false
50
51func init() {
52	var err error
53	interactive, err = isInteractive()
54	if err != nil {
55		panic(err)
56	}
57}
58
59func isInteractive() (bool, error) {
60	// The PPid of a service process be 1 / init.
61	return os.Getppid() != 1, nil
62}
63
64type solarisService struct {
65	i Interface
66	*Config
67
68	Prefix string
69}
70
71func (s *solarisService) String() string {
72	if len(s.DisplayName) > 0 {
73		return s.DisplayName
74	}
75	return s.Name
76}
77
78func (s *solarisService) Platform() string {
79	return version
80}
81
82func (s *solarisService) template() *template.Template {
83	functions := template.FuncMap{
84		"bool": func(v bool) string {
85			if v {
86				return "true"
87			}
88			return "false"
89		},
90	}
91
92	customConfig := s.Option.string(optionSysvScript, "")
93
94	if customConfig != "" {
95		return template.Must(template.New("").Funcs(functions).Parse(customConfig))
96	} else {
97		return template.Must(template.New("").Funcs(functions).Parse(manifest))
98	}
99}
100
101func (s *solarisService) configPath() (string, error) {
102	return "/lib/svc/manifest/" + s.Prefix + "/" + s.Config.Name + ".xml", nil
103}
104
105func (s *solarisService) getFMRI() string {
106	return "svc:/" + s.Prefix + "/" + s.Config.Name + ":default"
107}
108
109func (s *solarisService) Install() error {
110	// write start script
111	confPath, err := s.configPath()
112	if err != nil {
113		return err
114	}
115	_, err = os.Stat(confPath)
116	if err == nil {
117		return fmt.Errorf("Manifest already exists: %s", confPath)
118	}
119
120	f, err := os.Create(confPath)
121	if err != nil {
122		return err
123	}
124	defer f.Close()
125
126	path, err := s.execPath()
127	if err != nil {
128		return err
129	}
130	Display := ""
131	escaped := &bytes.Buffer{}
132	if err := xml.EscapeText(escaped, []byte(s.DisplayName)); err == nil {
133		Display = escaped.String()
134	}
135	var to = &struct {
136		*Config
137		Prefix  string
138		Display string
139		Path    string
140	}{
141		s.Config,
142		s.Prefix,
143		Display,
144		path,
145	}
146
147	err = s.template().Execute(f, to)
148	if err != nil {
149		return err
150	}
151
152	// import service
153	err = run("svcadm", "restart", "manifest-import")
154	if err != nil {
155		return err
156	}
157
158	return nil
159}
160
161func (s *solarisService) Uninstall() error {
162	s.Stop()
163
164	confPath, err := s.configPath()
165	if err != nil {
166		return err
167	}
168	err = os.Remove(confPath)
169	if err != nil {
170		return err
171	}
172
173	// unregister service
174	err = run("svcadm", "restart", "manifest-import")
175	if err != nil {
176		return err
177	}
178
179	return nil
180}
181
182func (s *solarisService) Status() (Status, error) {
183	fmri := s.getFMRI()
184	exitCode, out, err := runWithOutput("svcs", fmri)
185	if exitCode != 0 {
186		return StatusUnknown, ErrNotInstalled
187	}
188
189	re := regexp.MustCompile(`(degraded|disabled|legacy_run|maintenance|offline|online)\s+\w+` + fmri)
190	matches := re.FindStringSubmatch(out)
191	if len(matches) == 2 {
192		status := string(matches[1])
193		if status == "online" {
194			return StatusRunning, nil
195		} else {
196			return StatusStopped, nil
197		}
198	}
199	return StatusUnknown, err
200}
201
202func (s *solarisService) Start() error {
203	return run("/usr/sbin/svcadm", "enable", s.getFMRI())
204}
205func (s *solarisService) Stop() error {
206	return run("/usr/sbin/svcadm", "disable", s.getFMRI())
207}
208func (s *solarisService) Restart() error {
209	err := s.Stop()
210	if err != nil {
211		return err
212	}
213	time.Sleep(50 * time.Millisecond)
214	return s.Start()
215}
216
217func (s *solarisService) Run() error {
218	var err error
219
220	err = s.i.Start(s)
221	if err != nil {
222		return err
223	}
224
225	s.Option.funcSingle(optionRunWait, func() {
226		var sigChan = make(chan os.Signal, 3)
227		signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
228		<-sigChan
229	})()
230
231	return s.i.Stop(s)
232}
233
234func (s *solarisService) Logger(errs chan<- error) (Logger, error) {
235	if interactive {
236		return ConsoleLogger, nil
237	}
238	return s.SystemLogger(errs)
239}
240func (s *solarisService) SystemLogger(errs chan<- error) (Logger, error) {
241	return newSysLogger(s.Name, errs)
242}
243
244var manifest = `<?xml version="1.0"?>
245<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
246
247<service_bundle type='manifest' name='golang-{{.Name}}'>
248<service
249	name='{{.Prefix}}/{{.Name}}'
250	type='service'
251	version='1'>
252
253	<create_default_instance enabled='false' />
254
255	<single_instance />
256
257	<!--
258	  Wait for network interfaces to be initialized.
259	-->
260	<dependency name='network'
261	    grouping='require_all'
262	    restart_on='restart'
263	    type='service'>
264	    <service_fmri value='svc:/milestone/network:default'/>
265	</dependency>
266
267	<!--
268	  Wait for all local filesystems to be mounted.
269	-->
270	<dependency name='filesystem-local'
271	    grouping='require_all'
272	    restart_on='none'
273	    type='service'>
274	    <service_fmri
275		value='svc:/system/filesystem/local:default'/>
276	</dependency>
277
278	<exec_method
279		type='method'
280		name='start'
281		exec='bash -c {{.Path}} &amp;'
282		timeout_seconds='10' />
283
284	<exec_method
285		type='method'
286		name='stop'
287		exec='pkill -TERM -f {{.Path}}'
288		timeout_seconds='60' />
289
290	<!--
291	<property_group name='startd' type='framework'>
292                <propval name='duration' type='astring' value='transient' />
293        </property_group>
294	-->
295
296	<stability value='Unstable' />
297
298	<template>
299                <common_name>
300                        <loctext xml:lang='C'>
301                                {{.Display}}
302                        </loctext>
303                </common_name>
304        </template>
305</service>
306
307</service_bundle>
308`
309