forked from nezhahq/service
-
Notifications
You must be signed in to change notification settings - Fork 0
/
service.go
454 lines (401 loc) · 15.5 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
// Copyright 2015 Daniel Theophanes.
// Use of this source code is governed by a zlib-style
// license that can be found in the LICENSE file.
// Package service provides a simple way to create a system service.
// Currently supports Windows, Linux/(systemd | Upstart | SysV | OpenRC), and OSX/Launchd.
//
// Windows controls services by setting up callbacks that is non-trivial. This
// is very different then other systems. This package provides the same API
// despite the substantial differences.
// It also can be used to detect how a program is called, from an interactive
// terminal or from a service manager.
//
// Examples in the example/ folder.
//
// package main
//
// import (
// "log"
//
// "github.com/nezhahq/service"
// )
//
// var logger service.Logger
//
// type program struct{}
//
// func (p *program) Start(s service.Service) error {
// // Start should not block. Do the actual work async.
// go p.run()
// return nil
// }
// func (p *program) run() {
// // Do work here
// }
// func (p *program) Stop(s service.Service) error {
// // Stop should not block. Return with a few seconds.
// return nil
// }
//
// func main() {
// svcConfig := &service.Config{
// Name: "GoServiceTest",
// DisplayName: "Go Service Test",
// Description: "This is a test Go service.",
// }
//
// prg := &program{}
// s, err := service.New(prg, svcConfig)
// if err != nil {
// log.Fatal(err)
// }
// logger, err = s.Logger(nil)
// if err != nil {
// log.Fatal(err)
// }
// err = s.Run()
// if err != nil {
// logger.Error(err)
// }
// }
package service // import "github.com/nezhahq/service"
import (
"errors"
"fmt"
"log"
)
const (
optionKeepAlive = "KeepAlive"
optionKeepAliveDefault = true
optionRunAtLoad = "RunAtLoad"
optionRunAtLoadDefault = false
optionUserService = "UserService"
optionUserServiceDefault = false
optionSessionCreate = "SessionCreate"
optionSessionCreateDefault = false
optionLogOutput = "LogOutput"
optionLogOutputDefault = false
optionPrefix = "Prefix"
optionPrefixDefault = "application"
optionRunWait = "RunWait"
optionReloadSignal = "ReloadSignal"
optionPIDFile = "PIDFile"
optionLimitNOFILE = "LimitNOFILE"
optionLimitNOFILEDefault = -1 // -1 = don't set in configuration
optionRestart = "Restart"
optionSuccessExitStatus = "SuccessExitStatus"
optionSystemdScript = "SystemdScript"
optionSysvScript = "SysvScript"
optionRCSScript = "RCSScript"
optionUpstartScript = "UpstartScript"
optionLaunchdConfig = "LaunchdConfig"
optionOpenRCScript = "OpenRCScript"
optionLogDirectory = "LogDirectory"
)
// Status represents service status as an byte value
type Status byte
// Status of service represented as an byte
const (
StatusUnknown Status = iota // Status is unable to be determined due to an error or it was not installed.
StatusRunning
StatusStopped
)
// Config provides the setup for a Service. The Name field is required.
type Config struct {
Name string // Required name of the service. No spaces suggested.
DisplayName string // Display name, spaces allowed.
Description string // Long description of service.
UserName string // Run as username.
Arguments []string // Run with arguments.
// Optional field to specify the executable for service.
// If empty the current executable is used.
Executable string
// Array of service dependencies.
// Not yet fully implemented on Linux or OS X:
// 1. Support linux-systemd dependencies, just put each full line as the
// element of the string array, such as
// "After=network.target syslog.target"
// "Requires=syslog.target"
// Note, such lines will be directly appended into the [Unit] of
// the generated service config file, will not check their correctness.
Dependencies []string
// The following fields are not supported on Windows.
WorkingDirectory string // Initial working directory.
ChRoot string
// System specific options.
Option KeyValue
EnvVars map[string]string
}
var (
system System
systemRegistry []System
)
var (
// ErrNameFieldRequired is returned when Config.Name is empty.
ErrNameFieldRequired = errors.New("Config.Name field is required.")
// ErrNoServiceSystemDetected is returned when no system was detected.
ErrNoServiceSystemDetected = errors.New("No service system detected.")
// ErrNotInstalled is returned when the service is not installed.
ErrNotInstalled = errors.New("the service is not installed")
)
// New creates a new service based on a service interface and configuration.
func New(i Interface, c *Config) (Service, error) {
if len(c.Name) == 0 {
return nil, ErrNameFieldRequired
}
if system == nil {
return nil, ErrNoServiceSystemDetected
}
return system.New(i, c)
}
// KeyValue provides a list of system specific options.
// * OS X
// - LaunchdConfig string () - Use custom launchd config.
// - KeepAlive bool (true) - Prevent the system from stopping the service automatically.
// - RunAtLoad bool (false) - Run the service after its job has been loaded.
// - SessionCreate bool (false) - Create a full user session.
//
// * Solaris
// - Prefix string ("application") - Service FMRI prefix.
//
// * POSIX
// - UserService bool (false) - Install as a current user service.
// - SystemdScript string () - Use custom systemd script.
// - UpstartScript string () - Use custom upstart script.
// - SysvScript string () - Use custom sysv script.
// - OpenRCScript string () - Use custom OpenRC script.
// - RunWait func() (wait for SIGNAL) - Do not install signal but wait for this function to return.
// - ReloadSignal string () [USR1, ...] - Signal to send on reload.
// - PIDFile string () [/run/prog.pid] - Location of the PID file.
// - LogOutput bool (false) - Redirect StdErr & StandardOutPath to files.
// - Restart string (always) - How shall service be restarted.
// - SuccessExitStatus string () - The list of exit status that shall be considered as successful,
// in addition to the default ones.
// - LogDirectory string(/var/log) - The path to the log files directory
//
// * Linux (systemd)
// - LimitNOFILE int (-1) - Maximum open files (ulimit -n)
// (https://serverfault.com/questions/628610/increasing-nproc-for-processes-launched-by-systemd-on-centos-7)
// * Windows
// - DelayedAutoStart bool (false) - After booting, start this service after some delay.
// - Password string () - Password to use when interfacing with the system service manager.
// - Interactive bool (false) - The service can interact with the desktop. (more information https://docs.microsoft.com/en-us/windows/win32/services/interactive-services)
// - DelayedAutoStart bool (false) - after booting start this service after some delay.
// - StartType string ("automatic") - Start service type. (automatic | manual | disabled)
// - OnFailure string ("restart" ) - Action to perform on service failure. (restart | reboot | noaction)
// - OnFailureDelayDuration string ( "1s" ) - Delay before restarting the service, time.Duration string.
// - OnFailureResetPeriod int ( 10 ) - Reset period for errors, seconds.
type KeyValue map[string]interface{}
// bool returns the value of the given name, assuming the value is a boolean.
// If the value isn't found or is not of the type, the defaultValue is returned.
func (kv KeyValue) bool(name string, defaultValue bool) bool {
if v, found := kv[name]; found {
if castValue, is := v.(bool); is {
return castValue
}
}
return defaultValue
}
// int returns the value of the given name, assuming the value is an int.
// If the value isn't found or is not of the type, the defaultValue is returned.
func (kv KeyValue) int(name string, defaultValue int) int {
if v, found := kv[name]; found {
if castValue, is := v.(int); is {
return castValue
}
}
return defaultValue
}
// string returns the value of the given name, assuming the value is a string.
// If the value isn't found or is not of the type, the defaultValue is returned.
func (kv KeyValue) string(name string, defaultValue string) string {
if v, found := kv[name]; found {
if castValue, is := v.(string); is {
return castValue
}
}
return defaultValue
}
// float64 returns the value of the given name, assuming the value is a float64.
// If the value isn't found or is not of the type, the defaultValue is returned.
func (kv KeyValue) float64(name string, defaultValue float64) float64 {
if v, found := kv[name]; found {
if castValue, is := v.(float64); is {
return castValue
}
}
return defaultValue
}
// funcSingle returns the value of the given name, assuming the value is a func().
// If the value isn't found or is not of the type, the defaultValue is returned.
func (kv KeyValue) funcSingle(name string, defaultValue func()) func() {
if v, found := kv[name]; found {
if castValue, is := v.(func()); is {
return castValue
}
}
return defaultValue
}
// Platform returns a description of the system service.
func Platform() string {
if system == nil {
return ""
}
return system.String()
}
// Interactive returns false if running under the OS service manager
// and true otherwise.
func Interactive() bool {
if system == nil {
return true
}
return system.Interactive()
}
func newSystem() System {
for _, choice := range systemRegistry {
if choice.Detect() == false {
continue
}
return choice
}
return nil
}
// ChooseSystem chooses a system from the given system services.
// SystemServices are considered in the order they are suggested.
// Calling this may change what Interactive and Platform return.
func ChooseSystem(a ...System) {
systemRegistry = a
system = newSystem()
}
// ChosenSystem returns the system that service will use.
func ChosenSystem() System {
return system
}
// AvailableSystems returns the list of system services considered
// when choosing the system service.
func AvailableSystems() []System {
return systemRegistry
}
// System represents the service manager that is available.
type System interface {
// String returns a description of the system.
String() string
// Detect returns true if the system is available to use.
Detect() bool
// Interactive returns false if running under the system service manager
// and true otherwise.
Interactive() bool
// New creates a new service for this system.
New(i Interface, c *Config) (Service, error)
}
// Interface represents the service interface for a program. Start runs before
// the hosting process is granted control and Stop runs when control is returned.
//
// 1. OS service manager executes user program.
// 2. User program sees it is executed from a service manager (IsInteractive is false).
// 3. User program calls Service.Run() which blocks.
// 4. Interface.Start() is called and quickly returns.
// 5. User program runs.
// 6. OS service manager signals the user program to stop.
// 7. Interface.Stop() is called and quickly returns.
// - For a successful exit, os.Exit should not be called in Interface.Stop().
// 8. Service.Run returns.
// 9. User program should quickly exit.
type Interface interface {
// Start provides a place to initiate the service. The service doesn't
// signal a completed start until after this function returns, so the
// Start function must not take more then a few seconds at most.
Start(s Service) error
// Stop provides a place to clean up program execution before it is terminated.
// It should not take more then a few seconds to execute.
// Stop should not call os.Exit directly in the function.
Stop(s Service) error
}
// Shutdowner represents a service interface for a program that differentiates between "stop" and
// "shutdown". A shutdown is triggered when the whole box (not just the service) is stopped.
type Shutdowner interface {
Interface
// Shutdown provides a place to clean up program execution when the system is being shutdown.
// It is essentially the same as Stop but for the case where machine is being shutdown/restarted
// instead of just normally stopping the service. Stop won't be called when Shutdown is.
Shutdown(s Service) error
}
// TODO: Add Configure to Service interface.
// Service represents a service that can be run or controlled.
type Service interface {
// Run should be called shortly after the program entry point.
// After Interface.Stop has finished running, Run will stop blocking.
// After Run stops blocking, the program must exit shortly after.
Run() error
// Start signals to the OS service manager the given service should start.
Start() error
// Stop signals to the OS service manager the given service should stop.
Stop() error
// Restart signals to the OS service manager the given service should stop then start.
Restart() error
// Install setups up the given service in the OS service manager. This may require
// greater rights. Will return an error if it is already installed.
Install() error
// Uninstall removes the given service from the OS service manager. This may require
// greater rights. Will return an error if the service is not present.
Uninstall() error
// Opens and returns a system logger. If the user program is running
// interactively rather then as a service, the returned logger will write to
// os.Stderr. If errs is non-nil errors will be sent on errs as well as
// returned from Logger's functions.
Logger(errs chan<- error) (Logger, error)
// SystemLogger opens and returns a system logger. If errs is non-nil errors
// will be sent on errs as well as returned from Logger's functions.
SystemLogger(errs chan<- error) (Logger, error)
// String displays the name of the service. The display name if present,
// otherwise the name.
String() string
// Platform displays the name of the system that manages the service.
// In most cases this will be the same as service.Platform().
Platform() string
// Status returns the current service status.
Status() (Status, error)
}
// ControlAction list valid string texts to use in Control.
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}
// Control issues control functions to the service from a given action string.
func Control(s Service, action string) error {
var err error
switch action {
case ControlAction[0]:
err = s.Start()
case ControlAction[1]:
err = s.Stop()
case ControlAction[2]:
err = s.Restart()
case ControlAction[3]:
err = s.Install()
if err != nil {
return fmt.Errorf("Failed to install %v: %v", s, err)
} else {
err = s.Start()
if err != nil {
return fmt.Errorf("Failed to start %v: %v", s, err)
}
}
case ControlAction[4]:
s.Stop()
err = s.Uninstall()
default:
err = fmt.Errorf("Unknown action %s", action)
}
if err != nil {
return fmt.Errorf("Failed to %s %v: %v", action, s, err)
}
log.Printf("Successfully executed action %s!", action)
return nil
}
// Logger writes to the system log.
type Logger interface {
Error(v ...interface{}) error
Warning(v ...interface{}) error
Info(v ...interface{}) error
Errorf(format string, a ...interface{}) error
Warningf(format string, a ...interface{}) error
Infof(format string, a ...interface{}) error
}