windows/svc/mgr: add ability to set a reboot message and command when a service fails

Added configuration options for a windows service recovery settings.
New configurations include modifying the reboot message, or command
to be run when a service fails, and getting the current reboot message
or command.

Fixes golang/go#23239

Change-Id: I3e501d66e97745b7536fd654aee2bba488083e6d
Reviewed-on: https://go-review.googlesource.com/122579
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/svc/mgr/mgr_test.go b/windows/svc/mgr/mgr_test.go
index 13f1f38..9171f5b 100644
--- a/windows/svc/mgr/mgr_test.go
+++ b/windows/svc/mgr/mgr_test.go
@@ -174,6 +174,34 @@
 	testResetPeriod(t, s, 0)
 }
 
+func testRebootMessage(t *testing.T, s *mgr.Service, should string) {
+	err := s.SetRebootMessage(should)
+	if err != nil {
+		t.Fatalf("SetRebootMessage failed: %v", err)
+	}
+	is, err := s.RebootMessage()
+	if err != nil {
+		t.Fatalf("RebootMessage failed: %v", err)
+	}
+	if should != is {
+		t.Errorf("reboot message mismatch: message is %q, but should have %q", is, should)
+	}
+}
+
+func testRecoveryCommand(t *testing.T, s *mgr.Service, should string) {
+	err := s.SetRecoveryCommand(should)
+	if err != nil {
+		t.Fatalf("SetRecoveryCommand failed: %v", err)
+	}
+	is, err := s.RecoveryCommand()
+	if err != nil {
+		t.Fatalf("RecoveryCommand failed: %v", err)
+	}
+	if should != is {
+		t.Errorf("recovery command mismatch: command is %q, but should have %q", is, should)
+	}
+}
+
 func remove(t *testing.T, s *mgr.Service) {
 	err := s.Delete()
 	if err != nil {
@@ -245,6 +273,10 @@
 	}
 
 	testSetRecoveryActions(t, s)
+	testRebootMessage(t, s, "myservice failed")
+	testRebootMessage(t, s, "") // delete reboot message
+	testRecoveryCommand(t, s, "sc query myservice")
+	testRecoveryCommand(t, s, "") // delete recovery command
 
 	remove(t, s)
 }
diff --git a/windows/svc/mgr/recovery.go b/windows/svc/mgr/recovery.go
index 9243dca..71ce2b8 100644
--- a/windows/svc/mgr/recovery.go
+++ b/windows/svc/mgr/recovery.go
@@ -8,6 +8,7 @@
 
 import (
 	"errors"
+	"syscall"
 	"time"
 	"unsafe"
 
@@ -94,3 +95,41 @@
 	p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0]))
 	return p.ResetPeriod, nil
 }
+
+// SetRebootMessage sets service s reboot message.
+// If msg is "", the reboot message is deleted and no message is broadcast.
+func (s *Service) SetRebootMessage(msg string) error {
+	rActions := windows.SERVICE_FAILURE_ACTIONS{
+		RebootMsg: syscall.StringToUTF16Ptr(msg),
+	}
+	return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions)))
+}
+
+// RebootMessage is broadcast to server users before rebooting in response to the ComputerReboot service controller action.
+func (s *Service) RebootMessage() (string, error) {
+	b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS)
+	if err != nil {
+		return "", err
+	}
+	p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0]))
+	return toString(p.RebootMsg), nil
+}
+
+// SetRecoveryCommand sets the command line of the process to execute in response to the RunCommand service controller action.
+// If cmd is "", the command is deleted and no program is run when the service fails.
+func (s *Service) SetRecoveryCommand(cmd string) error {
+	rActions := windows.SERVICE_FAILURE_ACTIONS{
+		Command: syscall.StringToUTF16Ptr(cmd),
+	}
+	return windows.ChangeServiceConfig2(s.Handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&rActions)))
+}
+
+// RecoveryCommand is the command line of the process to execute in response to the RunCommand service controller action. This process runs under the same account as the service.
+func (s *Service) RecoveryCommand() (string, error) {
+	b, err := s.queryServiceConfig2(windows.SERVICE_CONFIG_FAILURE_ACTIONS)
+	if err != nil {
+		return "", err
+	}
+	p := (*windows.SERVICE_FAILURE_ACTIONS)(unsafe.Pointer(&b[0]))
+	return toString(p.Command), nil
+}