From 1ab12303aaf4cf68a53a5717a31b5acd72c57019 Mon Sep 17 00:00:00 2001 From: Daniel Theophanes Date: Fri, 5 Jun 2015 09:22:14 -0700 Subject: [PATCH] service: block on windows stop so restart is more robust. Previous behavior simply signalled the service to stop. The new behavior actually waits for the service to stop. This makes the Restart method for windows to be more robust when a service takes more time to clean up. --- example/stopPause/main.go | 62 +++++++++++++++++++++++++++++++++ service_windows.go | 72 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 example/stopPause/main.go diff --git a/example/stopPause/main.go b/example/stopPause/main.go new file mode 100644 index 0000000..8d0fdfe --- /dev/null +++ b/example/stopPause/main.go @@ -0,0 +1,62 @@ +// 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 + +// simple does nothing except block while running the service. +package main + +import ( + "log" + "os" + "time" + + "github.com/kardianos/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. + <-time.After(time.Second * 13) + return nil +} + +func main() { + svcConfig := &service.Config{ + Name: "GoServiceExampleStopPause", + DisplayName: "Go Service Example: Stop Pause", + Description: "This is an example Go service that pauses on stop.", + } + + prg := &program{} + s, err := service.New(prg, svcConfig) + if err != nil { + log.Fatal(err) + } + if len(os.Args) > 1 { + err = service.Control(s, os.Args[1]) + if err != nil { + log.Fatal(err) + } + return + } + + logger, err = s.Logger(nil) + if err != nil { + log.Fatal(err) + } + err = s.Run() + if err != nil { + logger.Error(err) + } +} diff --git a/service_windows.go b/service_windows.go index 9fb8aaf..ae5b0d8 100644 --- a/service_windows.go +++ b/service_windows.go @@ -8,9 +8,11 @@ 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" @@ -300,18 +302,78 @@ func (ws *windowsService) Stop() error { return err } defer s.Close() - _, err = s.Control(svc.Stop) - return err + + return ws.stopWait(s) } func (ws *windowsService) Restart() error { - err := ws.Stop() + m, err := mgr.Connect() if err != nil { return err } - time.Sleep(50 * time.Millisecond) - return ws.Start() + 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