diff --git a/README.md b/README.md index 0ddd43a..9c24e9f 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ See `examples/cron/cron_job.go` - [Maximus](https://github.com/maximus12793) - [AlgorathDev](https://github.com/AlgorathDev) - [Alexis Camilleri](https://github.com/krysennn) +- [neverland4u](https://github.com/neverland4u) All the contributors are welcome. If you would like to be the contributor please accept some rules. diff --git a/daemon.go b/daemon.go index 96e31b5..45f69a4 100644 --- a/daemon.go +++ b/daemon.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. /* -Package daemon 0.10.4 for use with Go (golang) services. +Package daemon 0.11.0 for use with Go (golang) services. Package daemon provides primitives for daemonization of golang services. This package is not provide implementation of user daemon, @@ -174,6 +174,19 @@ type Daemon interface { // Status - check the service status Status() (string, error) + + // Run - run executable service + Run(e Executable) (string, error) +} + +// Executable interface defines controlling methods of executable service +type Executable interface { + // Start - non-blocking start service + Start() + // Stop - non-blocking stop service + Stop() + // Run - blocking run service + Run() } // New - Create a new daemon diff --git a/daemon_darwin.go b/daemon_darwin.go index 5fb016c..e639f3c 100644 --- a/daemon_darwin.go +++ b/daemon_darwin.go @@ -186,6 +186,13 @@ func (darwin *darwinRecord) Status() (string, error) { return statusAction, nil } +// Run - Run service +func (darwin *darwinRecord) Run(e Executable) (string, error) { + runAction := "Running " + darwin.description + ":" + e.Run() + return runAction + " completed.", nil +} + var propertyList = ` diff --git a/daemon_freebsd.go b/daemon_freebsd.go index 9451c4e..5bf6403 100644 --- a/daemon_freebsd.go +++ b/daemon_freebsd.go @@ -227,6 +227,13 @@ func (bsd *bsdRecord) Status() (string, error) { return statusAction, nil } +// Run - Run service +func (bsd *bsdRecord) Run(e Executable) (string, error) { + runAction := "Running " + bsd.description + ":" + e.Run() + return runAction + " completed.", nil +} + var bsdConfig = `#!/bin/sh # # PROVIDE: {{.Name}} diff --git a/daemon_linux_systemd.go b/daemon_linux_systemd.go index 299b8c4..bfd928b 100644 --- a/daemon_linux_systemd.go +++ b/daemon_linux_systemd.go @@ -192,6 +192,13 @@ func (linux *systemDRecord) Status() (string, error) { return statusAction, nil } +// Run - Run service +func (linux *systemDRecord) Run(e Executable) (string, error) { + runAction := "Running " + linux.description + ":" + e.Run() + return runAction + " completed.", nil +} + var systemDConfig = `[Unit] Description={{.Description}} Requires={{.Dependencies}} diff --git a/daemon_linux_systemv.go b/daemon_linux_systemv.go index 810e74c..ba2d158 100644 --- a/daemon_linux_systemv.go +++ b/daemon_linux_systemv.go @@ -200,6 +200,13 @@ func (linux *systemVRecord) Status() (string, error) { return statusAction, nil } +// Run - Run service +func (linux *systemVRecord) Run(e Executable) (string, error) { + runAction := "Running " + linux.description + ":" + e.Run() + return runAction + " completed.", nil +} + var systemVConfig = `#! /bin/sh # # /etc/rc.d/init.d/{{.Name}} diff --git a/daemon_linux_upstart.go b/daemon_linux_upstart.go index 899653b..b5f6728 100644 --- a/daemon_linux_upstart.go +++ b/daemon_linux_upstart.go @@ -178,6 +178,13 @@ func (linux *upstartRecord) Status() (string, error) { return statusAction, nil } +// Run - Run service +func (linux *upstartRecord) Run(e Executable) (string, error) { + runAction := "Running " + linux.description + ":" + e.Run() + return runAction + " completed.", nil +} + var upstatConfig = `# {{.Name}} {{.Description}} description "{{.Description}}" diff --git a/daemon_windows.go b/daemon_windows.go index 54ca1f0..1ed5e86 100644 --- a/daemon_windows.go +++ b/daemon_windows.go @@ -9,10 +9,15 @@ import ( "errors" "fmt" "os/exec" - "regexp" + "strconv" "syscall" + "time" "unicode/utf16" "unsafe" + + "golang.org/x/sys/windows/registry" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" ) // windowsRecord - standard record (struct) for windows version of daemon package @@ -37,58 +42,160 @@ func (windows *windowsRecord) Install(args ...string) (string, error) { return installAction + failed, err } - cmdArgs := []string{"create", windows.name, "start=auto", "binPath=" + execp} - cmdArgs = append(cmdArgs, args...) + m, err := mgr.Connect() + if err != nil { + return installAction + failed, err + } + defer m.Disconnect() + + s, err := m.OpenService(windows.name) + if err == nil { + s.Close() + return installAction + failed, err + } - cmd := exec.Command("sc", cmdArgs...) - _, err = cmd.Output() + s, err = m.CreateService(windows.name, execp, mgr.Config{ + DisplayName: windows.name, + Description: windows.description, + StartType: mgr.StartAutomatic, + Dependencies: windows.dependencies, + }, args...) if err != nil { - return installAction + failed, getWindowsError(err) + return installAction + failed, err } + defer s.Close() + return installAction + " completed.", nil } // Remove the service func (windows *windowsRecord) Remove() (string, error) { removeAction := "Removing " + windows.description + ":" - cmd := exec.Command("sc", "delete", windows.name, "confirm") - err := cmd.Run() + + m, err := mgr.Connect() if err != nil { return removeAction + failed, getWindowsError(err) } + defer m.Disconnect() + s, err := m.OpenService(windows.name) + if err != nil { + return removeAction + failed, getWindowsError(err) + } + defer s.Close() + err = s.Delete() + if err != nil { + return removeAction + failed, getWindowsError(err) + } + return removeAction + " completed.", nil } // Start the service func (windows *windowsRecord) Start() (string, error) { startAction := "Starting " + windows.description + ":" - cmd := exec.Command("sc", "start", windows.name) - err := cmd.Run() + + m, err := mgr.Connect() if err != nil { return startAction + failed, getWindowsError(err) } + defer m.Disconnect() + s, err := m.OpenService(windows.name) + if err != nil { + return startAction + failed, getWindowsError(err) + } + defer s.Close() + if err = s.Start(); err != nil { + return startAction + failed, getWindowsError(err) + } + return startAction + " completed.", nil } // Stop the service func (windows *windowsRecord) Stop() (string, error) { stopAction := "Stopping " + windows.description + ":" - cmd := exec.Command("sc", "stop", windows.name) - err := cmd.Run() + + m, err := mgr.Connect() + if err != nil { + return stopAction + failed, getWindowsError(err) + } + defer m.Disconnect() + s, err := m.OpenService(windows.name) if err != nil { return stopAction + failed, getWindowsError(err) } + defer s.Close() + if err := stopAndWait(s); err != nil { + return stopAction + failed, getWindowsError(err) + } + return stopAction + " completed.", nil } +func stopAndWait(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 +} + +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) +} + // Status - Get service status func (windows *windowsRecord) Status() (string, error) { - cmd := exec.Command("sc", "query", windows.name) - out, err := cmd.Output() + m, err := mgr.Connect() + if err != nil { + return "Getting status:" + failed, getWindowsError(err) + } + defer m.Disconnect() + s, err := m.OpenService(windows.name) if err != nil { return "Getting status:" + failed, getWindowsError(err) } - return "Status: " + "SERVICE_" + getWindowsServiceState(out), nil + defer s.Close() + status, err := s.Query() + if err != nil { + return "Getting status:" + failed, getWindowsError(err) + } + + return "Status: " + getWindowsServiceStateFromUint32(status.State), nil } // Get executable path @@ -123,9 +230,91 @@ func getWindowsError(inputError error) error { } // Get windows service state -func getWindowsServiceState(out []byte) string { - regex := regexp.MustCompile("STATE.*: (?P[0-9]) (?P.*) ") - service := regex.FindAllStringSubmatch(string(out), -1)[0] +func getWindowsServiceStateFromUint32(state svc.State) string { + switch state { + case svc.Stopped: + return "SERVICE_STOPPED" + case svc.StartPending: + return "SERVICE_START_PENDING" + case svc.StopPending: + return "SERVICE_STOP_PENDING" + case svc.Running: + return "SERVICE_RUNNING" + case svc.ContinuePending: + return "SERVICE_CONTINUE_PENDING" + case svc.PausePending: + return "SERVICE_PAUSE_PENDING" + case svc.Paused: + return "SERVICE_PAUSED" + } + return "SERVICE_UNKNOWN" +} + +type serviceHandler struct { + executable Executable +} + +func (sh *serviceHandler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + changes <- svc.Status{State: svc.StartPending} + + fasttick := time.Tick(500 * time.Millisecond) + slowtick := time.Tick(2 * time.Second) + tick := fasttick + + sh.executable.Start() + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + +loop: + for { + select { + case <-tick: + break + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + // Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4 + time.Sleep(100 * time.Millisecond) + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + changes <- svc.Status{State: svc.StopPending} + sh.executable.Stop() + break loop + case svc.Pause: + changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted} + tick = slowtick + case svc.Continue: + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + tick = fasttick + default: + continue loop + } + } + } + return +} + +func (windows *windowsRecord) Run(e Executable) (string, error) { + runAction := "Running " + windows.description + ":" + + interactive, err := svc.IsAnInteractiveSession() + if err != nil { + return runAction + failed, getWindowsError(err) + } + if !interactive { + // service called from windows service manager + // use API provided by golang.org/x/sys/windows + err = svc.Run(windows.name, &serviceHandler{ + executable: e, + }) + if err != nil { + return runAction + failed, getWindowsError(err) + } + } else { + // otherwise, service should be called from terminal session + e.Run() + } - return service[2] + return runAction + " completed.", nil }