diff --git a/LICENSE b/LICENSE index 18527a2..fce91b4 100644 --- a/LICENSE +++ b/LICENSE @@ -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 warranty. In no event will the authors be held liable for any damages diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 8953e84..0000000 --- a/config/config.go +++ /dev/null @@ -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() -} diff --git a/console.go b/console.go new file mode 100644 index 0000000..cc46dbb --- /dev/null +++ b/console.go @@ -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 +} diff --git a/example/example b/example/example new file mode 100755 index 0000000..de20877 Binary files /dev/null and b/example/example differ diff --git a/example/main.go b/example/main.go index f4bf9db..3eb315b 100644 --- a/example/main.go +++ b/example/main.go @@ -1,95 +1,81 @@ package main import ( - "fmt" + "log" "os" "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() { - var name = "GoServiceTest" - var displayName = "Go Service Test" - var desc = "This is a test Go service. It is designed to run well." - - var s, err = service.NewService(name, displayName, desc) - log = s + 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 { - fmt.Printf("%s unable to start: %s", displayName, err) - return + panic(err) + } + logger, err = s.SystemLogger() + if err != nil { + panic(err) } 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", displayName) - case "remove": - err = s.Remove() - if err != nil { - fmt.Printf("Failed to remove: %s\n", 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) + err := service.Control(s, os.Args[1]) + if err != nil { + log.Fatal(err) } return } - err = s.Run(func() error { - // start - go doWork() - return nil - }, func() error { - // stop - stopWork() - return nil - }) + err = s.Run() if err != nil { - s.Error(err.Error()) + logger.Error(err) } } - -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{}{} -} diff --git a/service.go b/service.go index 84183dc..d6d346a 100644 --- a/service.go +++ b/service.go @@ -2,23 +2,16 @@ // Currently supports Windows, Linux/(systemd | Upstart | SysV), and OSX/Launchd. package service -import "bitbucket.org/kardianos/osext" +import ( + "errors" + "fmt" +) -// Creates a new service. name is the internal name -// 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. +// Config provides the setup for a Service. The Name field is required. 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. Arguments []string // Run with arguments. @@ -32,6 +25,18 @@ type Config struct { 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{} // 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 } -// Alpha API. Do not yet use. -func NewServiceConfig(c *Config) (Service, error) { - return newService(c) -} - -// Represents a generic way to interact with the system's service. -type Service interface { - Installer - Controller - Runner - Logger +// System represents the system and system's service being used. +type System interface { + // String returns a description of the OS and service platform. String() string } -// A Generic way to stop and start a service. -type Runner interface { - // Call quickly after initial entry point. Does not return until - // 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 +// LocalSystem get's the local system information. +func LocalSystem() System { + return system } -// Simple install and remove commands. -type Installer interface { - // Installs this service on the system. May return an - // error if this service is already installed. - Install() 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 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 - // error if this service is not already installed. - Remove() 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 } -// A service that implements ServiceController is able to -// start and stop itself. -type Controller interface { - // Starts this service on the system. +// 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 - // Stops this service on the system. + // 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 + + // 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 { - // Basic log functions in the context of the service. - Error(format string, a ...interface{}) error - Warning(format string, a ...interface{}) error - Info(format string, a ...interface{}) error -} + Error(v ...interface{}) error + Warning(v ...interface{}) error + Info(v ...interface{}) error -// Depreciated. Use osext.Executable instead. -// Returns the full path of the running executable -// as reported by the system. Includes the executable -// image name. -func GetExePath() (exePath string, err error) { - return osext.Executable() + Errorf(format string, a ...interface{}) error + Warningf(format string, a ...interface{}) error + Infof(format string, a ...interface{}) error } diff --git a/service_darwin.go b/service_darwin.go index 334e974..64efa17 100644 --- a/service_darwin.go +++ b/service_darwin.go @@ -2,40 +2,57 @@ package service import ( "fmt" - "log/syslog" "os" "os/exec" "os/signal" "os/user" "text/template" + "time" "bitbucket.org/kardianos/osext" ) const maxPathSize = 32 * 1024 -func newService(c *Config) (s *darwinLaunchdService, err error) { - s = &darwinLaunchdService{ +const version = "Darwin Launchd" + +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, } - s.logger, err = syslog.New(syslog.LOG_INFO, c.Name) - if err != nil { - return nil, err - } + var err error + s.interactive, err = isInteractive() - return s, nil + return s, err } type darwinLaunchdService struct { + i Interface *Config - logger *syslog.Writer + + interactive bool } -const version = "Darwin Launchd" - func (s *darwinLaunchdService) String() string { - return version + if len(s.DisplayName) > 0 { + return s.DisplayName + } + return s.Name } func (s *darwinLaunchdService) getServiceFilePath() (string, error) { @@ -49,6 +66,9 @@ func (s *darwinLaunchdService) getServiceFilePath() (string, error) { return "/Library/LaunchDaemons/" + s.Name + ".plist", nil } +func (s *darwinLaunchdService) Interactive() bool { + return s.interactive +} func (s *darwinLaunchdService) Install() error { confPath, err := s.getServiceFilePath() if err != nil { @@ -120,11 +140,19 @@ func (s *darwinLaunchdService) Stop() error { cmd := exec.Command("launchctl", "unload", confPath) 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 - err = onStart() + err = s.i.Start(s) if err != nil { return err } @@ -135,18 +163,17 @@ func (s *darwinLaunchdService) Run(onStart, onStop func() error) error { <-sigChan - return onStop() + return s.i.Stop(s) } -func (s *darwinLaunchdService) Error(format string, a ...interface{}) error { - return s.logger.Err(fmt.Sprintf(format, a...)) +func (s *darwinLaunchdService) Logger() (Logger, error) { + if s.interactive { + return ConsoleLogger, nil + } + return s.SystemLogger() } -func (s *darwinLaunchdService) Warning(format string, a ...interface{}) error { - return s.logger.Warning(fmt.Sprintf(format, a...)) -} -func (s *darwinLaunchdService) Info(format string, a ...interface{}) error { - // On Darwin syslog.log defaults to loggint >= Notice (see /etc/asl.conf). - return s.logger.Notice(fmt.Sprintf(format, a...)) +func (s *darwinLaunchdService) SystemLogger() (Logger, error) { + return newSysLogger(s.Name) } var launchdConfig = ` diff --git a/service_linux.go b/service_linux.go index 2474d3b..26931e7 100644 --- a/service_linux.go +++ b/service_linux.go @@ -2,11 +2,11 @@ package service import ( "fmt" - "log/syslog" "os" "os/exec" "os/signal" "text/template" + "time" "bitbucket.org/kardianos/osext" ) @@ -27,23 +27,6 @@ func getFlavor() initFlavor { 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 { if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil { return true @@ -59,13 +42,38 @@ func isSystemd() bool { } type linuxService struct { - flavor initFlavor - name, displayName, description string - logger *syslog.Writer + i Interface + *Config + + interactive bool } -func (ls *linuxService) String() string { - return fmt.Sprintf("Linux %s", ls.flavor.String()) +var flavor = getFlavor() + +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 @@ -109,8 +117,17 @@ func (f initFlavor) GetTemplate() *template.Template { 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 { - confPath := s.flavor.ConfigPath(s.name) + confPath := flavor.ConfigPath(s.Name) _, err := os.Stat(confPath) if err == nil { return fmt.Errorf("Init already exists: %s", confPath) @@ -132,33 +149,33 @@ func (s *linuxService) Install() error { Description string Path string }{ - s.displayName, - s.description, + s.DisplayName, + s.Description, path, } - err = s.flavor.GetTemplate().Execute(f, to) + err = flavor.GetTemplate().Execute(f, to) if err != nil { return err } - if s.flavor == initSystemV { + if flavor == initSystemV { if err = os.Chmod(confPath, 0755); err != nil { return err } 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 } } 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 } } } - if s.flavor == initSystemd { + if flavor == initSystemd { return exec.Command("systemctl", "daemon-reload").Run() } @@ -166,23 +183,30 @@ func (s *linuxService) Install() error { } func (s *linuxService) Remove() error { - if s.flavor == initSystemd { - exec.Command("systemctl", "disable", s.name+".service").Run() + if flavor == initSystemd { + 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 nil } -func (s *linuxService) Run(onStart, onStop func() error) (err error) { - err = onStart() +func (s *linuxService) Logger() (Logger, error) { + 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 { return err } - defer func() { - err = onStop() - }() sigChan := make(chan os.Signal, 3) @@ -190,39 +214,38 @@ func (s *linuxService) Run(onStart, onStop func() error) (err error) { <-sigChan - return nil + return s.i.Stop(s) } func (s *linuxService) Start() error { - switch s.flavor { + switch flavor { case initSystemd: - return exec.Command("systemctl", "start", s.name+".service").Run() + return exec.Command("systemctl", "start", s.Name+".service").Run() case initUpstart: - return exec.Command("initctl", "start", s.name).Run() + return exec.Command("initctl", "start", s.Name).Run() default: - return exec.Command("service", s.name, "start").Run() + return exec.Command("service", s.Name, "start").Run() } } func (s *linuxService) Stop() error { - switch s.flavor { + switch flavor { case initSystemd: - return exec.Command("systemctl", "stop", s.name+".service").Run() + return exec.Command("systemctl", "stop", s.Name+".service").Run() case initUpstart: - return exec.Command("initctl", "stop", s.name).Run() + return exec.Command("initctl", "stop", s.Name).Run() 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 { - return s.logger.Err(fmt.Sprintf(format, a...)) -} -func (s *linuxService) Warning(format string, a ...interface{}) error { - return s.logger.Warning(fmt.Sprintf(format, a...)) -} -func (s *linuxService) Info(format string, a ...interface{}) error { - return s.logger.Info(fmt.Sprintf(format, a...)) +func (s *linuxService) Restart() error { + err := s.Stop() + if err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + return s.Start() } const systemVScript = `#!/bin/sh diff --git a/service_unix.go b/service_unix.go new file mode 100644 index 0000000..c906ac4 --- /dev/null +++ b/service_unix.go @@ -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...)) +} diff --git a/service_windows.go b/service_windows.go index 5c41694..7aea57d 100644 --- a/service_windows.go +++ b/service_windows.go @@ -2,6 +2,9 @@ package service import ( "fmt" + "os" + "os/signal" + "time" "bitbucket.org/kardianos/osext" "code.google.com/p/winsvc/eventlog" @@ -9,32 +12,94 @@ import ( "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" -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 } -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 changes <- svc.Status{State: svc.StartPending} - if err := ws.onStart(); err != nil { - ws.Error(err.Error()) + if err := ws.i.Start(ws); err != nil { + // TODO: log error. + // ws.Error(err.Error()) return true, 1 } @@ -47,10 +112,10 @@ loop: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: changes <- svc.Status{State: svc.StopPending} - if err := ws.onStop(); err != nil { - ws.Error(err.Error()) - changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} - continue loop + if err := ws.i.Stop(ws); err != nil { + // TODO: Log error. + // ws.Error(err.Error()) + return true, 2 } break loop default: @@ -58,7 +123,11 @@ loop: } } - return + return false, 0 +} + +func (ws *windowsService) Interactive() bool { + return ws.interactive } func (ws *windowsService) Install() error { @@ -73,21 +142,21 @@ func (ws *windowsService) Install() error { return err } defer m.Disconnect() - s, err := m.OpenService(ws.name) + s, err := m.OpenService(ws.Name) if err == nil { 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{ - DisplayName: ws.displayName, - Description: ws.description, + s, err = m.CreateService(ws.Name, exepath, mgr.Config{ + DisplayName: ws.DisplayName, + Description: ws.Description, StartType: mgr.StartAutomatic, }) if err != nil { return err } 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 { s.Delete() return fmt.Errorf("InstallAsEventCreate() failed: %s", err) @@ -101,34 +170,38 @@ func (ws *windowsService) Remove() error { return err } defer m.Disconnect() - s, err := m.OpenService(ws.name) + s, err := m.OpenService(ws.Name) 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() err = s.Delete() if err != nil { return err } - err = eventlog.Remove(ws.name) + err = eventlog.Remove(ws.Name) if err != nil { return fmt.Errorf("RemoveEventLogSource() failed: %s", err) } return nil } -func (ws *windowsService) Run(onStart, onStop func() error) error { - elog, err := eventlog.Open(ws.name) +func (ws *windowsService) Run() error { + if !ws.interactive { + return svc.Run(ws.Name, ws) + } + err := ws.i.Start(ws) if err != nil { return err } - defer elog.Close() - ws.logger = elog + sigChan := make(chan os.Signal) - ws.onStart = onStart - ws.onStop = onStop - return svc.Run(ws.name, ws) + signal.Notify(sigChan, os.Interrupt, os.Kill) + + <-sigChan + + return ws.i.Stop(ws) } func (ws *windowsService) Start() error { @@ -138,7 +211,7 @@ func (ws *windowsService) Start() error { } defer m.Disconnect() - s, err := m.OpenService(ws.name) + s, err := m.OpenService(ws.Name) if err != nil { return err } @@ -153,7 +226,7 @@ func (ws *windowsService) Stop() error { } defer m.Disconnect() - s, err := m.OpenService(ws.name) + s, err := m.OpenService(ws.Name) if err != nil { return err } @@ -162,21 +235,24 @@ func (ws *windowsService) Stop() error { return err } -func (ws *windowsService) Error(format string, a ...interface{}) error { - if ws.logger == nil { - return nil +func (ws *windowsService) Restart() error { + err := ws.Stop() + 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 { - if ws.logger == nil { - return nil +func (ws *windowsService) Logger() (Logger, error) { + if ws.interactive { + return ConsoleLogger, nil } - return ws.logger.Warning(2, fmt.Sprintf(format, a...)) + return ws.SystemLogger() } -func (ws *windowsService) Info(format string, a ...interface{}) error { - if ws.logger == nil { - return nil +func (ws *windowsService) SystemLogger() (Logger, error) { + el, err := eventlog.Open(ws.Name) + if err != nil { + return nil, err } - return ws.logger.Info(1, fmt.Sprintf(format, a...)) + return WindowsLogger{el}, nil } diff --git a/stdservice/console_log.go b/stdservice/console_log.go deleted file mode 100644 index 7c34c48..0000000 --- a/stdservice/console_log.go +++ /dev/null @@ -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 -} diff --git a/stdservice/stdservice.go b/stdservice/stdservice.go deleted file mode 100644 index 1b7f8d7..0000000 --- a/stdservice/stdservice.go +++ /dev/null @@ -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()) - } -}