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
+}