// 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 import ( "fmt" "os" "os/signal" "strconv" "sync" "time" "golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" ) const version = "windows-service" type windowsService struct { i Interface *Config errSync sync.Mutex stopStartErr error } // WindowsLogger allows using windows specific logging methods. type WindowsLogger struct { ev *eventlog.Log errs chan<- error } type windowsSystem struct{} func (windowsSystem) String() string { return version } func (windowsSystem) Detect() bool { return true } func (windowsSystem) Interactive() bool { return interactive } func (windowsSystem) New(i Interface, c *Config) (Service, error) { ws := &windowsService{ i: i, Config: c, } return ws, nil } func init() { ChooseSystem(windowsSystem{}) } func (l WindowsLogger) send(err error) error { if err == nil { return nil } if l.errs != nil { l.errs <- err } return err } // Error logs an error message. func (l WindowsLogger) Error(v ...interface{}) error { return l.send(l.ev.Error(3, fmt.Sprint(v...))) } // Warning logs an warning message. func (l WindowsLogger) Warning(v ...interface{}) error { return l.send(l.ev.Warning(2, fmt.Sprint(v...))) } // Info logs an info message. func (l WindowsLogger) Info(v ...interface{}) error { return l.send(l.ev.Info(1, fmt.Sprint(v...))) } // Errorf logs an error message. func (l WindowsLogger) Errorf(format string, a ...interface{}) error { return l.send(l.ev.Error(3, fmt.Sprintf(format, a...))) } // Warningf logs an warning message. func (l WindowsLogger) Warningf(format string, a ...interface{}) error { return l.send(l.ev.Warning(2, fmt.Sprintf(format, a...))) } // Infof logs an info message. func (l WindowsLogger) Infof(format string, a ...interface{}) error { return l.send(l.ev.Info(1, fmt.Sprintf(format, a...))) } // NError logs an error message and an event ID. func (l WindowsLogger) NError(eventID uint32, v ...interface{}) error { return l.send(l.ev.Error(eventID, fmt.Sprint(v...))) } // NWarning logs an warning message and an event ID. func (l WindowsLogger) NWarning(eventID uint32, v ...interface{}) error { return l.send(l.ev.Warning(eventID, fmt.Sprint(v...))) } // NInfo logs an info message and an event ID. func (l WindowsLogger) NInfo(eventID uint32, v ...interface{}) error { return l.send(l.ev.Info(eventID, fmt.Sprint(v...))) } // NErrorf logs an error message and an event ID. func (l WindowsLogger) NErrorf(eventID uint32, format string, a ...interface{}) error { return l.send(l.ev.Error(eventID, fmt.Sprintf(format, a...))) } // NWarningf logs an warning message and an event ID. func (l WindowsLogger) NWarningf(eventID uint32, format string, a ...interface{}) error { return l.send(l.ev.Warning(eventID, fmt.Sprintf(format, a...))) } // NInfof logs an info message and an event ID. func (l WindowsLogger) NInfof(eventID uint32, format string, a ...interface{}) error { return l.send(l.ev.Info(eventID, fmt.Sprintf(format, a...))) } var interactive = false func init() { var err error interactive, err = svc.IsAnInteractiveSession() if err != nil { panic(err) } } func (ws *windowsService) String() string { if len(ws.DisplayName) > 0 { return ws.DisplayName } return ws.Name } func (ws *windowsService) setError(err error) { ws.errSync.Lock() defer ws.errSync.Unlock() ws.stopStartErr = err } func (ws *windowsService) getError() error { ws.errSync.Lock() defer ws.errSync.Unlock() return ws.stopStartErr } 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.i.Start(ws); err != nil { ws.setError(err) return true, 1 } changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} loop: for { c := <-r switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: changes <- svc.Status{State: svc.StopPending} if err := ws.i.Stop(ws); err != nil { ws.setError(err) return true, 2 } break loop default: continue loop } } return false, 0 } func (ws *windowsService) Install() error { exepath, err := ws.execPath() if err != nil { return err } m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ws.Name) if err == nil { s.Close() return fmt.Errorf("service %s already exists", ws.Name) } s, err = m.CreateService(ws.Name, exepath, mgr.Config{ DisplayName: ws.DisplayName, Description: ws.Description, StartType: mgr.StartAutomatic, ServiceStartName: ws.UserName, Password: ws.Option.string("Password", ""), Dependencies: ws.Dependencies, }, ws.Arguments...) if err != nil { return err } defer s.Close() err = eventlog.InstallAsEventCreate(ws.Name, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { s.Delete() return fmt.Errorf("InstallAsEventCreate() failed: %s", err) } return nil } func (ws *windowsService) Uninstall() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ws.Name) if err != nil { 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) if err != nil { return fmt.Errorf("RemoveEventLogSource() failed: %s", err) } return nil } func (ws *windowsService) Run() error { ws.setError(nil) if !interactive { // Return error messages from start and stop routines // that get executed in the Execute method. // Guarded with a mutex as it may run a different thread // (callback from windows). runErr := svc.Run(ws.Name, ws) startStopErr := ws.getError() if startStopErr != nil { return startStopErr } if runErr != nil { return runErr } return nil } err := ws.i.Start(ws) if err != nil { return err } sigChan := make(chan os.Signal) signal.Notify(sigChan, os.Interrupt, os.Kill) <-sigChan return ws.i.Stop(ws) } func (ws *windowsService) Start() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ws.Name) if err != nil { return err } defer s.Close() return s.Start() } func (ws *windowsService) Stop() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ws.Name) if err != nil { return err } defer s.Close() return ws.stopWait(s) } func (ws *windowsService) Restart() error { m, err := mgr.Connect() if err != nil { return err } defer m.Disconnect() s, err := m.OpenService(ws.Name) if err != nil { return err } defer s.Close() err = ws.stopWait(s) if err != nil { return err } return s.Start() } func (ws *windowsService) stopWait(s *mgr.Service) error { // First stop the service. Then wait for the service to // actually stop before starting it. status, err := s.Control(svc.Stop) if err != nil { return err } timeDuration := time.Millisecond * 50 timeout := time.After(getStopTimeout() + (timeDuration * 2)) tick := time.NewTicker(timeDuration) defer tick.Stop() for status.State != svc.Stopped { select { case <-tick.C: status, err = s.Query() if err != nil { return err } case <-timeout: break } } return nil } // getStopTimeout fetches the time before windows will kill the service. func getStopTimeout() time.Duration { // For default and paths see https://support.microsoft.com/en-us/kb/146092 defaultTimeout := time.Millisecond * 20000 key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control`, registry.READ) if err != nil { return defaultTimeout } sv, _, err := key.GetStringValue("WaitToKillServiceTimeout") if err != nil { return defaultTimeout } v, err := strconv.Atoi(sv) if err != nil { return defaultTimeout } return time.Millisecond * time.Duration(v) } func (ws *windowsService) Logger(errs chan<- error) (Logger, error) { if interactive { return ConsoleLogger, nil } return ws.SystemLogger(errs) } func (ws *windowsService) SystemLogger(errs chan<- error) (Logger, error) { el, err := eventlog.Open(ws.Name) if err != nil { return nil, err } return WindowsLogger{el, errs}, nil }