service: move to v2 API (beta).

This commit is contained in:
Daniel Theophanes
2015-01-13 09:41:14 -08:00
parent 6d5062006f
commit 8b12f7ccf2
12 changed files with 521 additions and 609 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2012 Daniel Theophanes Copyright (c) 2015 Daniel Theophanes
This software is provided 'as-is', without any express or implied This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages warranty. In no event will the authors be held liable for any damages
-170
View File
@@ -1,170 +0,0 @@
// A simple and consistent method to extract a configuration from a file.
// This doesn't contain any method to actually decode the file. The actual
// decoding is done in an external function.
package config
import (
"encoding/json"
"io"
"os"
"path/filepath"
"time"
"bitbucket.org/kardianos/osext"
"gopkg.in/fsnotify.v1"
)
const DefaultPostfix = "_config.json"
// Simple JSON based configuration decoder.
func DecodeJsonConfig(r io.Reader, v interface{}) error {
d := json.NewDecoder(r)
return d.Decode(v)
}
// Simple JSON based configuration encoder.
func EncodeJsonConfig(w io.Writer, v interface{}) error {
bb, err := json.MarshalIndent(v, "", "\t")
if err != nil {
return err
}
n, tot := 0, 0
for {
n, err = w.Write(bb[tot:])
tot += n
if len(bb) == tot {
break
}
}
return err
}
// Return a configuration file path. If baseName is empty,
// then the current executable name without the extension
// is used. If postfix is empty then DefaultPostfix is used.
func GetConfigFilePath(baseName, postfix string) (string, error) {
if len(postfix) == 0 {
postfix = DefaultPostfix
}
path, err := osext.Executable()
if err != nil {
return "", err
}
path, exeName := filepath.Split(path)
if len(baseName) == 0 {
exeName = exeName[:len(exeName)-len(filepath.Ext(exeName))]
} else {
exeName = baseName
}
configPath := filepath.Join(path, exeName+postfix)
return configPath, nil
}
type DecodeConfig func(r io.Reader, v interface{}) error
type EncodeConfig func(w io.Writer, v interface{}) error
type WatchConfig struct {
// Notified here if the file changes.
C chan *WatchConfig
filepath string
watch *fsnotify.Watcher
decode DecodeConfig
close chan struct{}
}
// Create a new configuration watcher. Adds a notification if the configuration file changes
// so it may be reloaded. If defaultConfig is not nil and encode is not nil, the
// configuration file path is checked if a file exists or not. If it doesn't exist
// the default configuration is written to the file.
func NewWatchConfig(filepath string, decode DecodeConfig, defaultConfig interface{}, encode EncodeConfig) (*WatchConfig, error) {
if defaultConfig != nil && encode != nil {
f, err := os.Open(filepath)
if f != nil {
f.Close()
}
if os.IsNotExist(err) {
f, err = os.Create(filepath)
if err != nil {
return nil, err
}
err = encode(f, defaultConfig)
f.Close()
if err != nil {
return nil, err
}
}
}
watch, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
err = watch.Add(filepath)
if err != nil {
return nil, err
}
wc := &WatchConfig{
C: make(chan *WatchConfig),
filepath: filepath,
watch: watch,
decode: decode,
close: make(chan struct{}, 0),
}
go wc.run()
return wc, nil
}
func (wc *WatchConfig) run() {
// Work around watch events being sent more then once.
ticker := time.NewTicker(time.Second)
trigger := false
waitOnce := false
for {
select {
case <-wc.close:
close(wc.C)
return
case <-wc.watch.Errors:
// Nothing right now.
case <-wc.watch.Events:
trigger = true
case <-ticker.C:
// Think of this as a PLC state machine.
if trigger && waitOnce {
wc.C <- wc
trigger = false
waitOnce = false
}
if trigger && !waitOnce {
waitOnce = true
}
}
}
}
// Send a notification as if the configuration file has changed.
func (wc *WatchConfig) TriggerC() {
wc.C <- wc
}
// Load the configuration from the file into the provided value.
func (wc *WatchConfig) Load(v interface{}) error {
f, err := os.Open(wc.filepath)
if err != nil {
return err
}
defer f.Close()
return wc.decode(f, v)
}
// Stop the watch and any loops that are running.
func (wc *WatchConfig) Close() error {
wc.close <- struct{}{}
return wc.watch.Close()
}
+44
View File
@@ -0,0 +1,44 @@
package service
import (
"log"
"os"
)
// ConsoleLogger logs to the std err.
var ConsoleLogger = consoleLogger{}
type consoleLogger struct {
info, warn, err *log.Logger
}
func init() {
ConsoleLogger.info = log.New(os.Stderr, "I: ", log.Ltime)
ConsoleLogger.warn = log.New(os.Stderr, "W: ", log.Ltime)
ConsoleLogger.err = log.New(os.Stderr, "E: ", log.Ltime)
}
func (c consoleLogger) Error(v ...interface{}) error {
c.err.Print(v...)
return nil
}
func (c consoleLogger) Warning(v ...interface{}) error {
c.warn.Print(v...)
return nil
}
func (c consoleLogger) Info(v ...interface{}) error {
c.info.Print(v...)
return nil
}
func (c consoleLogger) Errorf(format string, a ...interface{}) error {
c.err.Printf(format, a...)
return nil
}
func (c consoleLogger) Warningf(format string, a ...interface{}) error {
c.warn.Printf(format, a...)
return nil
}
func (c consoleLogger) Infof(format string, a ...interface{}) error {
c.info.Printf(format, a...)
return nil
}
BIN
View File
Binary file not shown.
+60 -74
View File
@@ -1,95 +1,81 @@
package main package main
import ( import (
"fmt" "log"
"os" "os"
"time" "time"
"bitbucket.org/kardianos/service" "bitbucket.org/kardianos/service2beta"
) )
var log service.Logger var logger service.Logger
type program struct {
exit chan struct{}
}
func (p *program) Start(s service.Service) error {
if s.Interactive() {
logger.Info("Running in terminal.")
} else {
logger.Info("Running under service manager.")
}
p.exit = make(chan struct{})
go p.run()
return nil
}
func (p *program) run() error {
logger.Infof("I'm running %v.", service.LocalSystem())
ticker := time.NewTicker(2 * time.Second)
for {
select {
case tm := <-ticker.C:
err := logger.Infof("Still running at %v...", tm)
if err != nil {
panic(err)
}
case <-p.exit:
ticker.Stop()
return nil
}
}
return nil
}
func (p *program) Stop(s service.Service) error {
err := logger.Info("I'm Stopping!")
if err != nil {
panic(err)
}
close(p.exit)
return nil
}
func main() { func main() {
var name = "GoServiceTest" svcConfig := &service.Config{
var displayName = "Go Service Test" Name: "GoServiceTest",
var desc = "This is a test Go service. It is designed to run well." DisplayName: "Go Service Test",
Description: "This is a test Go service.",
var s, err = service.NewService(name, displayName, desc) }
log = s
prg := &program{}
s, err := service.New(prg, svcConfig)
if err != nil { if err != nil {
fmt.Printf("%s unable to start: %s", displayName, err) panic(err)
return }
logger, err = s.SystemLogger()
if err != nil {
panic(err)
} }
if len(os.Args) > 1 { if len(os.Args) > 1 {
var err error err := service.Control(s, os.Args[1])
verb := os.Args[1]
switch verb {
case "install":
err = s.Install()
if err != nil { if err != nil {
fmt.Printf("Failed to install: %s\n", err) log.Fatal(err)
}
return return
} }
fmt.Printf("Service \"%s\" installed.\n", displayName) err = s.Run()
case "remove":
err = s.Remove()
if err != nil { if err != nil {
fmt.Printf("Failed to remove: %s\n", err) logger.Error(err)
return
}
fmt.Printf("Service \"%s\" removed.\n", displayName)
case "run":
doWork()
case "start":
err = s.Start()
if err != nil {
fmt.Printf("Failed to start: %s\n", err)
return
}
fmt.Printf("Service \"%s\" started.\n", displayName)
case "stop":
err = s.Stop()
if err != nil {
fmt.Printf("Failed to stop: %s\n", err)
return
}
fmt.Printf("Service \"%s\" stopped.\n", displayName)
}
return
}
err = s.Run(func() error {
// start
go doWork()
return nil
}, func() error {
// stop
stopWork()
return nil
})
if err != nil {
s.Error(err.Error())
} }
} }
var exit = make(chan struct{})
func doWork() {
log.Info("I'm Running!")
ticker := time.NewTicker(time.Minute)
for {
select {
case <-ticker.C:
log.Info("Still running...")
case <-exit:
ticker.Stop()
return
}
}
}
func stopWork() {
log.Info("I'm Stopping!")
exit <- struct{}{}
}
+117 -61
View File
@@ -2,23 +2,16 @@
// Currently supports Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd. // Currently supports Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd.
package service package service
import "bitbucket.org/kardianos/osext" import (
"errors"
"fmt"
)
// Creates a new service. name is the internal name // Config provides the setup for a Service. The Name field is required.
// and should not contain spaces. Display name is the pretty print
// name. The description is an arbitrary string used to describe the
// service.
func NewService(name, displayName, description string) (Service, error) {
return newService(&Config{
Name: name,
DisplayName: displayName,
Description: description,
})
}
// Alpha API. Do not yet use.
type Config struct { type Config struct {
Name, DisplayName, Description string 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. UserName string // Run as username.
Arguments []string // Run with arguments. Arguments []string // Run with arguments.
@@ -32,6 +25,18 @@ type Config struct {
KV KeyValue KV KeyValue
} }
var errNameFieldRequired = errors.New("Config.Name field is required.")
// 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
}
return newService(i, c)
}
// KeyValue provides a list of platform specific options. See platform docs for
// more details.
type KeyValue map[string]interface{} type KeyValue map[string]interface{}
// Bool returns the value of the given name, assuming the value is a boolean. // Bool returns the value of the given name, assuming the value is a boolean.
@@ -78,65 +83,116 @@ func (kv KeyValue) float64(name string, defaultValue float64) float64 {
return defaultValue return defaultValue
} }
// Alpha API. Do not yet use. // System represents the system and system's service being used.
func NewServiceConfig(c *Config) (Service, error) { type System interface {
return newService(c) // String returns a description of the OS and service platform.
}
// Represents a generic way to interact with the system's service.
type Service interface {
Installer
Controller
Runner
Logger
String() string String() string
} }
// A Generic way to stop and start a service. // LocalSystem get's the local system information.
type Runner interface { func LocalSystem() System {
// Call quickly after initial entry point. Does not return until return system
// service is ready to stop. onStart is called when the service is
// starting, returning an error will fail to start the service.
// If an error is returned from onStop, the service will still stop.
// An error passed from onStart or onStop will be returned as
// an error from Run.
// Both callbacks should return quickly and not block.
Run(onStart, onStop func() error) error
} }
// Simple install and remove commands. // Interface represents the service interface for a program. Start runs before
type Installer interface { // the hosting process is granted control and Stop runs when control is returned.
// Installs this service on the system. May return an //
// error if this service is already installed. // 1. OS service manager executes user program.
Install() error // 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 not
// 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
// Removes this service from the system. May return an // Stop provides a place to clean up program execution before it is terminated.
// error if this service is not already installed. // It should not take more then a few seconds to execute.
Remove() error // Stop should not call os.Exit directly in the function.
Stop(s Service) error
} }
// A service that implements ServiceController is able to // Service represents a service that can be run or controlled.
// start and stop itself. type Service interface {
type Controller interface { // Run should be called shortly after the program entry point.
// Starts this service on the system. // 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 Start() error
// Stops this service on the system. // Stop signals to the OS service manager the given service should stop.
Stop() error 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
// Remove removes the given service from the OS service manager. This may require
// greater rights. Will return an error if the service is not present.
Remove() error
// Interactive returns false if running under the OS service manager
// and true otherwise.
Interactive() bool
// 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.
Logger() (Logger, error)
// SystemLogger opens and returns a system logger.
SystemLogger() (Logger, error)
// String displays the name of the service. The display name if present,
// otherwise the name.
String() string
} }
// A service that implements ServiceLogger can perform simple system logging. // ControlAction list valid string texts to use in Control.
var ControlAction = [5]string{"start", "stop", "restart", "install", "remove"}
// 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()
case ControlAction[4]:
err = s.Remove()
default:
err = fmt.Errorf("Unknown action %s", action)
}
if err != nil {
return fmt.Errorf("Failed to %s %v: %v", action, s, err)
}
return nil
}
// Logger writes to the system log.
type Logger interface { type Logger interface {
// Basic log functions in the context of the service. Error(v ...interface{}) error
Error(format string, a ...interface{}) error Warning(v ...interface{}) error
Warning(format string, a ...interface{}) error Info(v ...interface{}) error
Info(format string, a ...interface{}) error
}
// Depreciated. Use osext.Executable instead. Errorf(format string, a ...interface{}) error
// Returns the full path of the running executable Warningf(format string, a ...interface{}) error
// as reported by the system. Includes the executable Infof(format string, a ...interface{}) error
// image name.
func GetExePath() (exePath string, err error) {
return osext.Executable()
} }
+49 -22
View File
@@ -2,40 +2,57 @@ package service
import ( import (
"fmt" "fmt"
"log/syslog"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"os/user" "os/user"
"text/template" "text/template"
"time"
"bitbucket.org/kardianos/osext" "bitbucket.org/kardianos/osext"
) )
const maxPathSize = 32 * 1024 const maxPathSize = 32 * 1024
func newService(c *Config) (s *darwinLaunchdService, err error) { const version = "Darwin Launchd"
s = &darwinLaunchdService{
type darwinSystem struct{}
func (ls darwinSystem) String() string {
return version
}
var system = darwinSystem{}
func isInteractive() (bool, error) {
// TODO: The PPID of Launchd is 1. The PPid of a service process should match launchd's PID.
return os.Getppid() != 1, nil
}
func newService(i Interface, c *Config) (*darwinLaunchdService, error) {
s := &darwinLaunchdService{
i: i,
Config: c, Config: c,
} }
s.logger, err = syslog.New(syslog.LOG_INFO, c.Name) var err error
if err != nil { s.interactive, err = isInteractive()
return nil, err
}
return s, nil return s, err
} }
type darwinLaunchdService struct { type darwinLaunchdService struct {
i Interface
*Config *Config
logger *syslog.Writer
interactive bool
} }
const version = "Darwin Launchd"
func (s *darwinLaunchdService) String() string { func (s *darwinLaunchdService) String() string {
return version if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
} }
func (s *darwinLaunchdService) getServiceFilePath() (string, error) { func (s *darwinLaunchdService) getServiceFilePath() (string, error) {
@@ -49,6 +66,9 @@ func (s *darwinLaunchdService) getServiceFilePath() (string, error) {
return "/Library/LaunchDaemons/" + s.Name + ".plist", nil return "/Library/LaunchDaemons/" + s.Name + ".plist", nil
} }
func (s *darwinLaunchdService) Interactive() bool {
return s.interactive
}
func (s *darwinLaunchdService) Install() error { func (s *darwinLaunchdService) Install() error {
confPath, err := s.getServiceFilePath() confPath, err := s.getServiceFilePath()
if err != nil { if err != nil {
@@ -120,11 +140,19 @@ func (s *darwinLaunchdService) Stop() error {
cmd := exec.Command("launchctl", "unload", confPath) cmd := exec.Command("launchctl", "unload", confPath)
return cmd.Run() return cmd.Run()
} }
func (s *darwinLaunchdService) Restart() error {
err := s.Stop()
if err != nil {
return err
}
time.Sleep(50 * time.Millisecond)
return s.Start()
}
func (s *darwinLaunchdService) Run(onStart, onStop func() error) error { func (s *darwinLaunchdService) Run() error {
var err error var err error
err = onStart() err = s.i.Start(s)
if err != nil { if err != nil {
return err return err
} }
@@ -135,18 +163,17 @@ func (s *darwinLaunchdService) Run(onStart, onStop func() error) error {
<-sigChan <-sigChan
return onStop() return s.i.Stop(s)
} }
func (s *darwinLaunchdService) Error(format string, a ...interface{}) error { func (s *darwinLaunchdService) Logger() (Logger, error) {
return s.logger.Err(fmt.Sprintf(format, a...)) if s.interactive {
return ConsoleLogger, nil
} }
func (s *darwinLaunchdService) Warning(format string, a ...interface{}) error { return s.SystemLogger()
return s.logger.Warning(fmt.Sprintf(format, a...))
} }
func (s *darwinLaunchdService) Info(format string, a ...interface{}) error { func (s *darwinLaunchdService) SystemLogger() (Logger, error) {
// On Darwin syslog.log defaults to loggint >= Notice (see /etc/asl.conf). return newSysLogger(s.Name)
return s.logger.Notice(fmt.Sprintf(format, a...))
} }
var launchdConfig = `<?xml version='1.0' encoding='UTF-8'?> var launchdConfig = `<?xml version='1.0' encoding='UTF-8'?>
+78 -55
View File
@@ -2,11 +2,11 @@ package service
import ( import (
"fmt" "fmt"
"log/syslog"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"text/template" "text/template"
"time"
"bitbucket.org/kardianos/osext" "bitbucket.org/kardianos/osext"
) )
@@ -27,23 +27,6 @@ func getFlavor() initFlavor {
return flavor return flavor
} }
func newService(c *Config) (Service, error) {
s := &linuxService{
flavor: getFlavor(),
name: c.Name,
displayName: c.DisplayName,
description: c.Description,
}
var err error
s.logger, err = syslog.New(syslog.LOG_INFO, s.name)
if err != nil {
return nil, err
}
return s, nil
}
func isUpstart() bool { func isUpstart() bool {
if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil { if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil {
return true return true
@@ -59,13 +42,38 @@ func isSystemd() bool {
} }
type linuxService struct { type linuxService struct {
flavor initFlavor i Interface
name, displayName, description string *Config
logger *syslog.Writer
interactive bool
} }
func (ls *linuxService) String() string { var flavor = getFlavor()
return fmt.Sprintf("Linux %s", ls.flavor.String())
type linuxSystem struct{}
func (ls linuxSystem) String() string {
return fmt.Sprintf("Linux %s", flavor.String())
}
var system = linuxSystem{}
func newService(i Interface, c *Config) (Service, error) {
s := &linuxService{
i: i,
Config: c,
}
var err error
s.interactive, err = isInteractive()
return s, err
}
func (s *linuxService) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
} }
type initFlavor uint8 type initFlavor uint8
@@ -109,8 +117,17 @@ func (f initFlavor) GetTemplate() *template.Template {
return template.Must(template.New(f.String() + "Script").Parse(templ)) return template.Must(template.New(f.String() + "Script").Parse(templ))
} }
func isInteractive() (bool, error) {
// TODO: Is this true for user services?
return os.Getppid() != 1, nil
}
func (s *linuxService) Interactive() bool {
return s.interactive
}
func (s *linuxService) Install() error { func (s *linuxService) Install() error {
confPath := s.flavor.ConfigPath(s.name) confPath := flavor.ConfigPath(s.Name)
_, err := os.Stat(confPath) _, err := os.Stat(confPath)
if err == nil { if err == nil {
return fmt.Errorf("Init already exists: %s", confPath) return fmt.Errorf("Init already exists: %s", confPath)
@@ -132,33 +149,33 @@ func (s *linuxService) Install() error {
Description string Description string
Path string Path string
}{ }{
s.displayName, s.DisplayName,
s.description, s.Description,
path, path,
} }
err = s.flavor.GetTemplate().Execute(f, to) err = flavor.GetTemplate().Execute(f, to)
if err != nil { if err != nil {
return err return err
} }
if s.flavor == initSystemV { if flavor == initSystemV {
if err = os.Chmod(confPath, 0755); err != nil { if err = os.Chmod(confPath, 0755); err != nil {
return err return err
} }
for _, i := range [...]string{"2", "3", "4", "5"} { for _, i := range [...]string{"2", "3", "4", "5"} {
if err = os.Symlink(confPath, "/etc/rc"+i+".d/S50"+s.name); err != nil { if err = os.Symlink(confPath, "/etc/rc"+i+".d/S50"+s.Name); err != nil {
continue continue
} }
} }
for _, i := range [...]string{"0", "1", "6"} { for _, i := range [...]string{"0", "1", "6"} {
if err = os.Symlink(confPath, "/etc/rc"+i+".d/K02"+s.name); err != nil { if err = os.Symlink(confPath, "/etc/rc"+i+".d/K02"+s.Name); err != nil {
continue continue
} }
} }
} }
if s.flavor == initSystemd { if flavor == initSystemd {
return exec.Command("systemctl", "daemon-reload").Run() return exec.Command("systemctl", "daemon-reload").Run()
} }
@@ -166,23 +183,30 @@ func (s *linuxService) Install() error {
} }
func (s *linuxService) Remove() error { func (s *linuxService) Remove() error {
if s.flavor == initSystemd { if flavor == initSystemd {
exec.Command("systemctl", "disable", s.name+".service").Run() exec.Command("systemctl", "disable", s.Name+".service").Run()
} }
if err := os.Remove(s.flavor.ConfigPath(s.name)); err != nil { if err := os.Remove(flavor.ConfigPath(s.Name)); err != nil {
return err return err
} }
return nil return nil
} }
func (s *linuxService) Run(onStart, onStop func() error) (err error) { func (s *linuxService) Logger() (Logger, error) {
err = onStart() if s.interactive {
return ConsoleLogger, nil
}
return s.SystemLogger()
}
func (s *linuxService) SystemLogger() (Logger, error) {
return newSysLogger(s.Name)
}
func (s *linuxService) Run() (err error) {
err = s.i.Start(s)
if err != nil { if err != nil {
return err return err
} }
defer func() {
err = onStop()
}()
sigChan := make(chan os.Signal, 3) sigChan := make(chan os.Signal, 3)
@@ -190,39 +214,38 @@ func (s *linuxService) Run(onStart, onStop func() error) (err error) {
<-sigChan <-sigChan
return nil return s.i.Stop(s)
} }
func (s *linuxService) Start() error { func (s *linuxService) Start() error {
switch s.flavor { switch flavor {
case initSystemd: case initSystemd:
return exec.Command("systemctl", "start", s.name+".service").Run() return exec.Command("systemctl", "start", s.Name+".service").Run()
case initUpstart: case initUpstart:
return exec.Command("initctl", "start", s.name).Run() return exec.Command("initctl", "start", s.Name).Run()
default: default:
return exec.Command("service", s.name, "start").Run() return exec.Command("service", s.Name, "start").Run()
} }
} }
func (s *linuxService) Stop() error { func (s *linuxService) Stop() error {
switch s.flavor { switch flavor {
case initSystemd: case initSystemd:
return exec.Command("systemctl", "stop", s.name+".service").Run() return exec.Command("systemctl", "stop", s.Name+".service").Run()
case initUpstart: case initUpstart:
return exec.Command("initctl", "stop", s.name).Run() return exec.Command("initctl", "stop", s.Name).Run()
default: default:
return exec.Command("service", s.name, "stop").Run() return exec.Command("service", s.Name, "stop").Run()
} }
} }
func (s *linuxService) Error(format string, a ...interface{}) error { func (s *linuxService) Restart() error {
return s.logger.Err(fmt.Sprintf(format, a...)) err := s.Stop()
if err != nil {
return err
} }
func (s *linuxService) Warning(format string, a ...interface{}) error { time.Sleep(50 * time.Millisecond)
return s.logger.Warning(fmt.Sprintf(format, a...)) return s.Start()
}
func (s *linuxService) Info(format string, a ...interface{}) error {
return s.logger.Info(fmt.Sprintf(format, a...))
} }
const systemVScript = `#!/bin/sh const systemVScript = `#!/bin/sh
+41
View File
@@ -0,0 +1,41 @@
//
// +build linux darwin
package service
import (
"fmt"
"log/syslog"
)
func newSysLogger(name string) (Logger, error) {
w, err := syslog.New(syslog.LOG_INFO, name)
if err != nil {
return nil, err
}
return sysLogger{w}, nil
}
type sysLogger struct {
*syslog.Writer
}
func (s sysLogger) Error(v ...interface{}) error {
return s.Writer.Err(fmt.Sprint(v...))
}
func (s sysLogger) Warning(v ...interface{}) error {
return s.Writer.Warning(fmt.Sprint(v...))
}
func (s sysLogger) Info(v ...interface{}) error {
return s.Writer.Info(fmt.Sprint(v...))
}
func (s sysLogger) Errorf(format string, a ...interface{}) error {
return s.Writer.Err(fmt.Sprintf(format, a...))
}
func (s sysLogger) Warningf(format string, a ...interface{}) error {
return s.Writer.Warning(fmt.Sprintf(format, a...))
}
func (s sysLogger) Infof(format string, a ...interface{}) error {
return s.Writer.Info(fmt.Sprintf(format, a...))
}
+129 -53
View File
@@ -2,6 +2,9 @@ package service
import ( import (
"fmt" "fmt"
"os"
"os/signal"
"time"
"bitbucket.org/kardianos/osext" "bitbucket.org/kardianos/osext"
"code.google.com/p/winsvc/eventlog" "code.google.com/p/winsvc/eventlog"
@@ -9,32 +12,94 @@ import (
"code.google.com/p/winsvc/svc" "code.google.com/p/winsvc/svc"
) )
func newService(c *Config) (*windowsService, error) {
return &windowsService{
name: c.Name,
displayName: c.DisplayName,
description: c.Description,
}, nil
}
type windowsService struct {
name, displayName, description string
onStart, onStop func() error
logger *eventlog.Log
}
const version = "Windows Service" const version = "Windows Service"
func (ws *windowsService) String() string { type windowsService struct {
i Interface
*Config
interactive bool
}
// WindowsLogger allows using windows specific logging methods.
type WindowsLogger struct {
ev *eventlog.Log
}
type windowsSystem struct{}
func (windowsSystem) String() string {
return version return version
} }
func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { var system = windowsSystem{}
func (l WindowsLogger) Error(v ...interface{}) error {
return l.ev.Error(3, fmt.Sprint(v...))
}
func (l WindowsLogger) Warning(v ...interface{}) error {
return l.ev.Warning(2, fmt.Sprint(v...))
}
func (l WindowsLogger) Info(v ...interface{}) error {
return l.ev.Info(1, fmt.Sprint(v...))
}
func (l WindowsLogger) Errorf(format string, a ...interface{}) error {
return l.ev.Error(3, fmt.Sprintf(format, a...))
}
func (l WindowsLogger) Warningf(format string, a ...interface{}) error {
return l.ev.Warning(2, fmt.Sprintf(format, a...))
}
func (l WindowsLogger) Infof(format string, a ...interface{}) error {
return l.ev.Info(1, fmt.Sprintf(format, a...))
}
func (l WindowsLogger) NError(eventId uint32, v ...interface{}) error {
return l.ev.Error(eventId, fmt.Sprint(v...))
}
func (l WindowsLogger) NWarning(eventId uint32, v ...interface{}) error {
return l.ev.Warning(eventId, fmt.Sprint(v...))
}
func (l WindowsLogger) NInfo(eventId uint32, v ...interface{}) error {
return l.ev.Info(eventId, fmt.Sprint(v...))
}
func (l WindowsLogger) NErrorf(eventId uint32, format string, a ...interface{}) error {
return l.ev.Error(eventId, fmt.Sprintf(format, a...))
}
func (l WindowsLogger) NWarningf(eventId uint32, format string, a ...interface{}) error {
return l.ev.Warning(eventId, fmt.Sprintf(format, a...))
}
func (l WindowsLogger) NInfof(eventId uint32, format string, a ...interface{}) error {
return l.ev.Info(eventId, fmt.Sprintf(format, a...))
}
func isInteractive() (bool, error) {
return svc.IsAnInteractiveSession()
}
func newService(i Interface, c *Config) (*windowsService, error) {
ws := &windowsService{
i: i,
Config: c,
}
var err error
ws.interactive, err = isInteractive()
return ws, err
}
func (ws *windowsService) String() string {
if len(ws.DisplayName) > 0 {
return ws.DisplayName
}
return ws.Name
}
func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending} changes <- svc.Status{State: svc.StartPending}
if err := ws.onStart(); err != nil { if err := ws.i.Start(ws); err != nil {
ws.Error(err.Error()) // TODO: log error.
// ws.Error(err.Error())
return true, 1 return true, 1
} }
@@ -47,10 +112,10 @@ loop:
changes <- c.CurrentStatus changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown: case svc.Stop, svc.Shutdown:
changes <- svc.Status{State: svc.StopPending} changes <- svc.Status{State: svc.StopPending}
if err := ws.onStop(); err != nil { if err := ws.i.Stop(ws); err != nil {
ws.Error(err.Error()) // TODO: Log error.
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} // ws.Error(err.Error())
continue loop return true, 2
} }
break loop break loop
default: default:
@@ -58,7 +123,11 @@ loop:
} }
} }
return return false, 0
}
func (ws *windowsService) Interactive() bool {
return ws.interactive
} }
func (ws *windowsService) Install() error { func (ws *windowsService) Install() error {
@@ -73,21 +142,21 @@ func (ws *windowsService) Install() error {
return err return err
} }
defer m.Disconnect() defer m.Disconnect()
s, err := m.OpenService(ws.name) s, err := m.OpenService(ws.Name)
if err == nil { if err == nil {
s.Close() s.Close()
return fmt.Errorf("service %s already exists", ws.name) return fmt.Errorf("service %s already exists", ws.Name)
} }
s, err = m.CreateService(ws.name, exepath, mgr.Config{ s, err = m.CreateService(ws.Name, exepath, mgr.Config{
DisplayName: ws.displayName, DisplayName: ws.DisplayName,
Description: ws.description, Description: ws.Description,
StartType: mgr.StartAutomatic, StartType: mgr.StartAutomatic,
}) })
if err != nil { if err != nil {
return err return err
} }
defer s.Close() defer s.Close()
err = eventlog.InstallAsEventCreate(ws.name, eventlog.Error|eventlog.Warning|eventlog.Info) err = eventlog.InstallAsEventCreate(ws.Name, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil { if err != nil {
s.Delete() s.Delete()
return fmt.Errorf("InstallAsEventCreate() failed: %s", err) return fmt.Errorf("InstallAsEventCreate() failed: %s", err)
@@ -101,34 +170,38 @@ func (ws *windowsService) Remove() error {
return err return err
} }
defer m.Disconnect() defer m.Disconnect()
s, err := m.OpenService(ws.name) s, err := m.OpenService(ws.Name)
if err != nil { if err != nil {
return fmt.Errorf("service %s is not installed", ws.name) return fmt.Errorf("service %s is not installed", ws.Name)
} }
defer s.Close() defer s.Close()
err = s.Delete() err = s.Delete()
if err != nil { if err != nil {
return err return err
} }
err = eventlog.Remove(ws.name) err = eventlog.Remove(ws.Name)
if err != nil { if err != nil {
return fmt.Errorf("RemoveEventLogSource() failed: %s", err) return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
} }
return nil return nil
} }
func (ws *windowsService) Run(onStart, onStop func() error) error { func (ws *windowsService) Run() error {
elog, err := eventlog.Open(ws.name) if !ws.interactive {
return svc.Run(ws.Name, ws)
}
err := ws.i.Start(ws)
if err != nil { if err != nil {
return err return err
} }
defer elog.Close()
ws.logger = elog sigChan := make(chan os.Signal)
ws.onStart = onStart signal.Notify(sigChan, os.Interrupt, os.Kill)
ws.onStop = onStop
return svc.Run(ws.name, ws) <-sigChan
return ws.i.Stop(ws)
} }
func (ws *windowsService) Start() error { func (ws *windowsService) Start() error {
@@ -138,7 +211,7 @@ func (ws *windowsService) Start() error {
} }
defer m.Disconnect() defer m.Disconnect()
s, err := m.OpenService(ws.name) s, err := m.OpenService(ws.Name)
if err != nil { if err != nil {
return err return err
} }
@@ -153,7 +226,7 @@ func (ws *windowsService) Stop() error {
} }
defer m.Disconnect() defer m.Disconnect()
s, err := m.OpenService(ws.name) s, err := m.OpenService(ws.Name)
if err != nil { if err != nil {
return err return err
} }
@@ -162,21 +235,24 @@ func (ws *windowsService) Stop() error {
return err return err
} }
func (ws *windowsService) Error(format string, a ...interface{}) error { func (ws *windowsService) Restart() error {
if ws.logger == nil { err := ws.Stop()
return nil if err != nil {
return err
} }
return ws.logger.Error(3, fmt.Sprintf(format, a...)) time.Sleep(50 * time.Millisecond)
return ws.Start()
} }
func (ws *windowsService) Warning(format string, a ...interface{}) error { func (ws *windowsService) Logger() (Logger, error) {
if ws.logger == nil { if ws.interactive {
return nil return ConsoleLogger, nil
} }
return ws.logger.Warning(2, fmt.Sprintf(format, a...)) return ws.SystemLogger()
} }
func (ws *windowsService) Info(format string, a ...interface{}) error { func (ws *windowsService) SystemLogger() (Logger, error) {
if ws.logger == nil { el, err := eventlog.Open(ws.Name)
return nil if err != nil {
return nil, err
} }
return ws.logger.Info(1, fmt.Sprintf(format, a...)) return WindowsLogger{el}, nil
} }
-18
View File
@@ -1,18 +0,0 @@
package stdservice
import "fmt"
type ConsoleLogger struct{}
func (ConsoleLogger) Error(format string, a ...interface{}) error {
fmt.Printf("E: "+format+"\n", a...)
return nil
}
func (ConsoleLogger) Warning(format string, a ...interface{}) error {
fmt.Printf("W: "+format+"\n", a...)
return nil
}
func (ConsoleLogger) Info(format string, a ...interface{}) error {
fmt.Printf("I: "+format+"\n", a...)
return nil
}
-153
View File
@@ -1,153 +0,0 @@
/*
Many services that run on different platforms cannot rely
on flags to be passed for configuration. Some platforms
require explicit install commands. This package handles the common
boilerplate code. The following command may be passed to the
executable as the first argument:
install | remove | run | start | stop
These commands will do the following actions:
install - Install the running executable as a service on the system.
remove - Remove the running executable as a service on the system.
run - Run the service as a command line application, output log to prompt.
start - Starts the service via system controls.
stop - Stops the service via system controls.
*/
package stdservice
import (
"fmt"
"os"
"bitbucket.org/kardianos/service"
)
// Standard service configuration. Start MUST block.
// Stop MUST NOT block for more then a second or two.
type Config struct {
// Used to register the service with the operating system.
Name, DisplayName, LongDescription string
// Called when the service starts or stops.
// Stop may be nil.
Start, Stop func(c *Config)
// Called after logging may be setup but before the service is started.
// Init is optional and may be nil.
// If Init returns an error, that error is logged to the logger
// and the service start is aborted.
// Init should not block.
Init func(c *Config) error
s service.Service
l service.Logger
}
// Get service after Run() has been called.
func (c *Config) Service() service.Service {
return c.s
}
// Get logger after Run() has been called.
func (c *Config) Logger() service.Logger {
return c.l
}
// Fill in configuration, then call Run() to setup basic handling.
// Blocks until program completes. Is intended to handle the standard
// simple cases for running a service.
func (c *Config) Run() {
run(c)
}
// Depreciated. Same as *Config.Run().
func Run(c *Config) {
run(c)
}
func run(c *Config) {
var s, err = service.NewService(c.Name, c.DisplayName, c.LongDescription)
c.s = s
c.l = s
if err != nil {
fmt.Printf("%s unable to start: %s", c.DisplayName, err)
return
}
if len(os.Args) > 1 {
var err error
verb := os.Args[1]
switch verb {
case "install":
err = s.Install()
if err != nil {
fmt.Printf("Failed to install: %s\n", err)
return
}
fmt.Printf("Service \"%s\" installed.\n", c.DisplayName)
case "remove":
err = s.Remove()
if err != nil {
fmt.Printf("Failed to remove: %s\n", err)
return
}
fmt.Printf("Service \"%s\" removed.\n", c.DisplayName)
case "run":
c.l = ConsoleLogger{}
defer func() {
if c.Stop != nil {
c.Stop(c)
}
}()
if c.Init != nil {
err := c.Init(c)
if err != nil {
c.l.Error(err.Error())
return
}
}
c.Start(c)
case "start":
err = s.Start()
if err != nil {
fmt.Printf("Failed to start: %s\n", err)
return
}
fmt.Printf("Service \"%s\" started.\n", c.DisplayName)
case "stop":
err = s.Stop()
if err != nil {
fmt.Printf("Failed to stop: %s\n", err)
return
}
fmt.Printf("Service \"%s\" stopped.\n", c.DisplayName)
default:
fmt.Printf("Options for \"%s\": (install | remove | run | start | stop)\n", os.Args[0])
}
return
}
if c.Init != nil {
err := c.Init(c)
if err != nil {
c.l.Error(err.Error())
return
}
}
err = s.Run(func() error {
// start
go c.Start(c)
return nil
}, func() error {
// stop
if c.Stop != nil {
c.Stop(c)
}
return nil
})
if err != nil {
c.l.Error(err.Error())
}
}