windows, windows/svc/mgr: add ability to modify service recovery settings.
Added configuration options for a windows service recovery settings.
Current configurations include modifying actions taken when a service
fails, setting the reset period, and getting the current recovery
settings.
Updates golang/go#23239
Change-Id: I4e91b2068122731e6eba3332afb0fe300b298c97
Reviewed-on: https://go-review.googlesource.com/104635
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
diff --git a/windows/service.go b/windows/service.go
index 24aa90b..62fc31b 100644
--- a/windows/service.go
+++ b/windows/service.go
@@ -43,6 +43,11 @@
SC_STATUS_PROCESS_INFO = 0
+ SC_ACTION_NONE = 0
+ SC_ACTION_RESTART = 1
+ SC_ACTION_REBOOT = 2
+ SC_ACTION_RUN_COMMAND = 3
+
SERVICE_STOPPED = 1
SERVICE_START_PENDING = 2
SERVICE_STOP_PENDING = 3
@@ -148,6 +153,19 @@
ServiceStatusProcess SERVICE_STATUS_PROCESS
}
+type SERVICE_FAILURE_ACTIONS struct {
+ ResetPeriod uint32
+ RebootMsg *uint16
+ Command *uint16
+ ActionsCount uint32
+ Actions *SC_ACTION
+}
+
+type SC_ACTION struct {
+ Type uint32
+ Delay uint32
+}
+
//sys CloseServiceHandle(handle Handle) (err error) = advapi32.CloseServiceHandle
//sys CreateService(mgr Handle, serviceName *uint16, displayName *uint16, access uint32, srvType uint32, startType uint32, errCtl uint32, pathName *uint16, loadOrderGroup *uint16, tagId *uint32, dependencies *uint16, serviceStartName *uint16, password *uint16) (handle Handle, err error) [failretval==0] = advapi32.CreateServiceW
//sys OpenService(mgr Handle, serviceName *uint16, access uint32) (handle Handle, err error) [failretval==0] = advapi32.OpenServiceW
diff --git a/windows/svc/mgr/config.go b/windows/svc/mgr/config.go
index 03bf41f..d804e31 100644
--- a/windows/svc/mgr/config.go
+++ b/windows/svc/mgr/config.go
@@ -88,23 +88,11 @@
}
}
- var p2 *windows.SERVICE_DESCRIPTION
- n = uint32(1024)
- for {
- b := make([]byte, n)
- p2 = (*windows.SERVICE_DESCRIPTION)(unsafe.Pointer(&b[0]))
- err := windows.QueryServiceConfig2(s.Handle,
- windows.SERVICE_CONFIG_DESCRIPTION, &b[0], n, &n)
- if err == nil {
- break
- }
- if err.(syscall.Errno) != syscall.ERROR_INSUFFICIENT_BUFFER {
- return Config{}, err
- }
- if n <= uint32(len(b)) {
- return Config{}, err
- }
+ b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_DESCRIPTION)
+ if err != nil {
+ return Config{}, err
}
+ p2 := (*windows.SERVICE_DESCRIPTION)(unsafe.Pointer(&b[0]))
return Config{
ServiceType: p.ServiceType,
@@ -137,3 +125,21 @@
}
return updateDescription(s.Handle, c.Description)
}
+
+// queryServiceConfig2 calls Windows QueryServiceConfig2 with infoLevel parameter and returns retrieved service configuration information.
+func (s *Service) queryServiceConfig2(infoLevel uint32) ([]byte, error) {
+ n := uint32(1024)
+ for {
+ b := make([]byte, n)
+ err := windows.QueryServiceConfig2(s.Handle, infoLevel, &b[0], n, &n)
+ if err == nil {
+ return b, nil
+ }
+ if err.(syscall.Errno) != syscall.ERROR_INSUFFICIENT_BUFFER {
+ return nil, err
+ }
+ if n <= uint32(len(b)) {
+ return nil, err
+ }
+ }
+}
diff --git a/windows/svc/mgr/mgr_test.go b/windows/svc/mgr/mgr_test.go
index 1569a22..13f1f38 100644
--- a/windows/svc/mgr/mgr_test.go
+++ b/windows/svc/mgr/mgr_test.go
@@ -95,6 +95,85 @@
return is
}
+func testRecoveryActions(t *testing.T, s *mgr.Service, should []mgr.RecoveryAction) {
+ is, err := s.RecoveryActions()
+ if err != nil {
+ t.Fatalf("RecoveryActions failed: %s", err)
+ }
+ if len(should) != len(is) {
+ t.Errorf("recovery action mismatch: contains %v actions, but should have %v", len(is), len(should))
+ }
+ for i, _ := range is {
+ if should[i].Type != is[i].Type {
+ t.Errorf("recovery action mismatch: Type is %v, but should have %v", is[i].Type, should[i].Type)
+ }
+ if should[i].Delay != is[i].Delay {
+ t.Errorf("recovery action mismatch: Delay is %v, but should have %v", is[i].Delay, should[i].Delay)
+ }
+ }
+}
+
+func testResetPeriod(t *testing.T, s *mgr.Service, should uint32) {
+ is, err := s.ResetPeriod()
+ if err != nil {
+ t.Fatalf("ResetPeriod failed: %s", err)
+ }
+ if should != is {
+ t.Errorf("reset period mismatch: reset period is %v, but should have %v", is, should)
+ }
+}
+
+func testSetRecoveryActions(t *testing.T, s *mgr.Service) {
+ r := []mgr.RecoveryAction{
+ mgr.RecoveryAction{
+ Type: mgr.NoAction,
+ Delay: 60000 * time.Millisecond,
+ },
+ mgr.RecoveryAction{
+ Type: mgr.ServiceRestart,
+ Delay: 4 * time.Minute,
+ },
+ mgr.RecoveryAction{
+ Type: mgr.ServiceRestart,
+ Delay: time.Minute,
+ },
+ mgr.RecoveryAction{
+ Type: mgr.RunCommand,
+ Delay: 4000 * time.Millisecond,
+ },
+ }
+
+ // 4 recovery actions with reset period
+ err := s.SetRecoveryActions(r, uint32(10000))
+ if err != nil {
+ t.Fatalf("SetRecoveryActions failed: %v", err)
+ }
+ testRecoveryActions(t, s, r)
+ testResetPeriod(t, s, uint32(10000))
+
+ // Infinite reset period
+ err = s.SetRecoveryActions(r, syscall.INFINITE)
+ if err != nil {
+ t.Fatalf("SetRecoveryActions failed: %v", err)
+ }
+ testRecoveryActions(t, s, r)
+ testResetPeriod(t, s, syscall.INFINITE)
+
+ // nil recovery actions
+ err = s.SetRecoveryActions(nil, 0)
+ if err.Error() != "recoveryActions cannot be nil" {
+ t.Fatalf("SetRecoveryActions failed with unexpected error message of %q", err)
+ }
+
+ // Delete all recovery actions and reset period
+ err = s.ResetRecoveryActions()
+ if err != nil {
+ t.Fatalf("ResetRecoveryActions failed: %v", err)
+ }
+ testRecoveryActions(t, s, nil)
+ testResetPeriod(t, s, 0)
+}
+
func remove(t *testing.T, s *mgr.Service) {
err := s.Delete()
if err != nil {
@@ -165,5 +244,7 @@
t.Errorf("ListServices failed to find %q service", name)
}
+ testSetRecoveryActions(t, s)
+
remove(t, s)
}
diff --git a/windows/svc/mgr/recovery.go b/windows/svc/mgr/recovery.go
new file mode 100644
index 0000000..9243dca
--- /dev/null
+++ b/windows/svc/mgr/recovery.go
@@ -0,0 +1,96 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package mgr
+
+import (
+ "errors"
+ "time"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+const (
+ // Possible recovery actions that the service control manager can perform.
+ NoAction = windows.SC_ACTION_NONE // no action
+ ComputerReboot = windows.SC_ACTION_REBOOT // reboot the computer
+ ServiceRestart = windows.SC_ACTION_RESTART // restart the service
+ RunCommand = windows.SC_ACTION_RUN_COMMAND // run a command
+)
+
+// RecoveryAction represents an action that the service control manager can perform when service fails.
+// A service is considered failed when it terminates without reporting a status of SERVICE_STOPPED to the service controller.
+type RecoveryAction struct {
+ Type int // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
+ Delay time.Duration // the time to wait before performing the specified action
+}
+
+// SetRecoveryActions sets actions that service controller performs when service fails and
+// the time after which to reset the service failure count to zero if there are no failures, in seconds.
+// Specify INFINITE to indicate that service failure count should never be reset.
+func (s *Service) SetRecoveryActions(recoveryActions []RecoveryAction, resetPeriod uint32) error {
+ if recoveryActions == nil {
+ return errors.New("recoveryActions cannot be nil")
+ }
+ actions := []windows.SC_ACTION{}
+ for _, a := range recoveryActions {
+ action := windows.SC_ACTION{
+ Type: uint32(a.Type),
+ Delay: uint32(a.Delay.Nanoseconds() / 1000000),
+ }
+ actions = append(actions, action)
+ }
+ rActions := windows.SERVICE_FAILURE_ACTIONS{
+ ActionsCount: uint32(len(actions)),
+ Actions: &actions[0],
+ ResetPeriod: resetPeriod,
+ }
+ return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions)))
+}
+
+// RecoveryActions returns actions that service controller performs when service fails.
+// The service control manager counts the number of times service s has failed since the system booted.
+// The count is reset to 0 if the service has not failed for ResetPeriod seconds.
+// When the service fails for the Nth time, the service controller performs the action specified in element [N-1] of returned slice.
+// If N is greater than slice length, the service controller repeats the last action in the slice.
+func (s *Service) RecoveryActions() ([]RecoveryAction, error) {
+ b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS)
+ if err != nil {
+ return nil, err
+ }
+ p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0]))
+ if p.Actions == nil {
+ return nil, err
+ }
+
+ var recoveryActions []RecoveryAction
+ actions := (*[1024]windows.SC_ACTION)(unsafe.Pointer(p.Actions))[:p.ActionsCount]
+ for _, action := range actions {
+ recoveryActions = append(recoveryActions, RecoveryAction{Type: int(action.Type), Delay: time.Duration(action.Delay) * time.Millisecond})
+ }
+ return recoveryActions, nil
+}
+
+// ResetRecoveryActions deletes both reset period and array of failure actions.
+func (s *Service) ResetRecoveryActions() error {
+ actions := make([]windows.SC_ACTION, 1)
+ rActions := windows.SERVICE_FAILURE_ACTIONS{
+ Actions: &actions[0],
+ }
+ return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions)))
+}
+
+// ResetPeriod is the time after which to reset the service failure
+// count to zero if there are no failures, in seconds.
+func (s *Service) ResetPeriod() (uint32, error) {
+ b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS)
+ if err != nil {
+ return 0, err
+ }
+ p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0]))
+ return p.ResetPeriod, nil
+}