windows: allow determining if manager is locked

The SCM can be locked by NT. While traditionally any process could lock
the SCM using "LockServiceDatabase", Microsoft removed this
functionality because it created so many bugs, and that function now
does nothing. However, the system itself, via the "NT Service Control
Manager", is still allowed to lock the SCM.

For example, at boot time on Windows 8.1, the SCM is locked after a
service is started until that service reports itself in a running state.
This poses a bit of a problem: it's useful to install device drivers
from inside services as part of their initialization, and mark the
service as having started only after the device has installed. But
device installation might potentially load new drivers, and drivers
themselves exist as a special type of service. This means that if a
driver is installed before marking the service as started, the entire
SCM will deadlock, and the OS will be partially unresponsive for a
minute or two.

Fortunately Microsoft supplies an API for exactly this purpose. The
solution is to mark the service as started before installing device
drivers, only under the circumstance that the SCM is locked. So, this
commit adds the proper API for determining this. It can be used like
this:

    if m, err := mgr.Connect(); err == nil {
        if lockStatus, err := m.LockStatus(); err == nil && lockStatus.IsLocked {
            log.Printf("SCM locked for %v by %s, marking service as started", lockStatus.Age, lockStatus.Owner)
            changes <- svc.Status{State: svc.Running}
        }
        m.Disconnect()
    }
    deviceDriver.Install()

This creates messages like the following, indicating that this API
works:

    SCM locked for 1s by .\NT Service Control Manager, marking service as started

Change-Id: Ic2f5b387e23efc3a287b2ab96ff84b357b712e36
Reviewed-on: https://go-review.googlesource.com/c/sys/+/180977
Run-TryBot: Jason Donenfeld <Jason@zx2c4.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 9a59b42..03383f1 100644
--- a/windows/service.go
+++ b/windows/service.go
@@ -200,12 +200,19 @@
 	Delay uint32
 }
 
+type QUERY_SERVICE_LOCK_STATUS struct {
+	IsLocked     uint32
+	LockOwner    *uint16
+	LockDuration 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
 //sys	DeleteService(service Handle) (err error) = advapi32.DeleteService
 //sys	StartService(service Handle, numArgs uint32, argVectors **uint16) (err error) = advapi32.StartServiceW
 //sys	QueryServiceStatus(service Handle, status *SERVICE_STATUS) (err error) = advapi32.QueryServiceStatus
+//sys	QueryServiceLockStatus(mgr Handle, lockStatus *QUERY_SERVICE_LOCK_STATUS, bufSize uint32, bytesNeeded *uint32) (err error) = advapi32.QueryServiceLockStatusW
 //sys	ControlService(service Handle, control uint32, status *SERVICE_STATUS) (err error) = advapi32.ControlService
 //sys	StartServiceCtrlDispatcher(serviceTable *SERVICE_TABLE_ENTRY) (err error) = advapi32.StartServiceCtrlDispatcherW
 //sys	SetServiceStatus(service Handle, serviceStatus *SERVICE_STATUS) (err error) = advapi32.SetServiceStatus
diff --git a/windows/svc/mgr/mgr.go b/windows/svc/mgr/mgr.go
index 24638d7..ad4cd6b 100644
--- a/windows/svc/mgr/mgr.go
+++ b/windows/svc/mgr/mgr.go
@@ -13,6 +13,7 @@
 
 import (
 	"syscall"
+	"time"
 	"unicode/utf16"
 	"unsafe"
 
@@ -48,6 +49,36 @@
 	return windows.CloseServiceHandle(m.Handle)
 }
 
+type LockStatus struct {
+	IsLocked bool          // Whether the SCM has been locked.
+	Age      time.Duration // For how long the SCM has been locked.
+	Owner    string        // The name of the user who has locked the SCM.
+}
+
+// LockStatus returns whether the service control manager is locked by
+// the system, for how long, and by whom. A locked SCM indicates that
+// most service actions will block until the system unlocks the SCM.
+func (m *Mgr) LockStatus() (*LockStatus, error) {
+	bytesNeeded := uint32(unsafe.Sizeof(windows.QUERY_SERVICE_LOCK_STATUS{}) + 1024)
+	for {
+		bytes := make([]byte, bytesNeeded)
+		lockStatus := (*windows.QUERY_SERVICE_LOCK_STATUS)(unsafe.Pointer(&bytes[0]))
+		err := windows.QueryServiceLockStatus(m.Handle, lockStatus, uint32(len(bytes)), &bytesNeeded)
+		if err == windows.ERROR_INSUFFICIENT_BUFFER && bytesNeeded >= uint32(unsafe.Sizeof(windows.QUERY_SERVICE_LOCK_STATUS{})) {
+			continue
+		}
+		if err != nil {
+			return nil, err
+		}
+		status := &LockStatus{
+			IsLocked: lockStatus.IsLocked != 0,
+			Age:      time.Duration(lockStatus.LockDuration) * time.Second,
+			Owner:    windows.UTF16ToString((*[(1 << 30) - 1]uint16)(unsafe.Pointer(lockStatus.LockOwner))[:]),
+		}
+		return status, nil
+	}
+}
+
 func toPtr(s string) *uint16 {
 	if len(s) == 0 {
 		return nil
diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go
index 6c35b90..376bd04 100644
--- a/windows/zsyscall_windows.go
+++ b/windows/zsyscall_windows.go
@@ -60,6 +60,7 @@
 	procDeleteService                      = modadvapi32.NewProc("DeleteService")
 	procStartServiceW                      = modadvapi32.NewProc("StartServiceW")
 	procQueryServiceStatus                 = modadvapi32.NewProc("QueryServiceStatus")
+	procQueryServiceLockStatusW            = modadvapi32.NewProc("QueryServiceLockStatusW")
 	procControlService                     = modadvapi32.NewProc("ControlService")
 	procStartServiceCtrlDispatcherW        = modadvapi32.NewProc("StartServiceCtrlDispatcherW")
 	procSetServiceStatus                   = modadvapi32.NewProc("SetServiceStatus")
@@ -425,6 +426,18 @@
 	return
 }
 
+func QueryServiceLockStatus(mgr Handle, lockStatus *QUERY_SERVICE_LOCK_STATUS, bufSize uint32, bytesNeeded *uint32) (err error) {
+	r1, _, e1 := syscall.Syscall6(procQueryServiceLockStatusW.Addr(), 4, uintptr(mgr), uintptr(unsafe.Pointer(lockStatus)), uintptr(bufSize), uintptr(unsafe.Pointer(bytesNeeded)), 0, 0)
+	if r1 == 0 {
+		if e1 != 0 {
+			err = errnoErr(e1)
+		} else {
+			err = syscall.EINVAL
+		}
+	}
+	return
+}
+
 func ControlService(service Handle, control uint32, status *SERVICE_STATUS) (err error) {
 	r1, _, e1 := syscall.Syscall(procControlService.Addr(), 3, uintptr(service), uintptr(control), uintptr(unsafe.Pointer(status)))
 	if r1 == 0 {