|
|
@@ -0,0 +1,303 @@
|
|
|
+// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
+// Use of this source code is governed by a BSD-style
|
|
|
+// license that can be found in the LICENSE file.
|
|
|
+
|
|
|
+package winutil
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "strings"
|
|
|
+ "unsafe"
|
|
|
+
|
|
|
+ "golang.org/x/sys/windows"
|
|
|
+ "golang.org/x/sys/windows/svc"
|
|
|
+ "golang.org/x/sys/windows/svc/mgr"
|
|
|
+ "tailscale.com/types/logger"
|
|
|
+)
|
|
|
+
|
|
|
+// LogSvcState obtains the state of the Windows service named rootSvcName and
|
|
|
+// all of its dependencies, and then emits that state to logf.
|
|
|
+func LogSvcState(logf logger.Logf, rootSvcName string) {
|
|
|
+ logEntries := []svcStateLogEntry{}
|
|
|
+
|
|
|
+ walkFn := func(svc *mgr.Service, config mgr.Config) {
|
|
|
+ status, err := svc.Query()
|
|
|
+ if err != nil {
|
|
|
+ logf("Failed retrieving Status for service %q: %v", svc.Name, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ logEntries = append(logEntries, makeLogEntry(svc, status, config))
|
|
|
+ }
|
|
|
+
|
|
|
+ err := walkServices(rootSvcName, walkFn)
|
|
|
+ if err != nil {
|
|
|
+ logf("LogSvcState error: %v", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ json, err := json.MarshalIndent(logEntries, "", " ")
|
|
|
+ if err != nil {
|
|
|
+ logf("Error marshaling service log entries: %v", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ var builder strings.Builder
|
|
|
+ builder.WriteString("State of service ")
|
|
|
+ fmt.Fprintf(&builder, "%q", rootSvcName)
|
|
|
+ builder.WriteString(" and its dependencies:")
|
|
|
+ builder.WriteString("\n")
|
|
|
+ builder.Write(json)
|
|
|
+ builder.WriteString("\n")
|
|
|
+
|
|
|
+ logf(builder.String())
|
|
|
+}
|
|
|
+
|
|
|
+// walkSvcFunc is type of the callback function invoked by WalkServices.
|
|
|
+type walkSvcFunc func(*mgr.Service, mgr.Config)
|
|
|
+
|
|
|
+// walkServices opens the service named rootSvcName and walks its dependency
|
|
|
+// graph, invoking callback for each service (including the root itself).
|
|
|
+func walkServices(rootSvcName string, callback walkSvcFunc) error {
|
|
|
+ scm, err := connectToLocalSCMForRead()
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("connecting to Service Control Manager: %w", err)
|
|
|
+ }
|
|
|
+ defer scm.Disconnect()
|
|
|
+
|
|
|
+ rootSvc, err := openServiceForRead(scm, rootSvcName)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("opening service %q: %w", rootSvcName, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ deps := []*mgr.Service{rootSvc}
|
|
|
+ defer func() {
|
|
|
+ // Any service still in deps when we return is open and must be closed.
|
|
|
+ for _, dep := range deps {
|
|
|
+ dep.Close()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ seen := make(map[string]struct{})
|
|
|
+
|
|
|
+ for err == nil && len(deps) > 0 {
|
|
|
+ err = func() error {
|
|
|
+ curSvc := deps[len(deps)-1]
|
|
|
+ defer curSvc.Close()
|
|
|
+
|
|
|
+ deps = deps[:len(deps)-1]
|
|
|
+
|
|
|
+ seen[curSvc.Name] = struct{}{}
|
|
|
+
|
|
|
+ curCfg, err := curSvc.Config()
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("retrieving Config for service %q: %w", curSvc.Name, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ callback(curSvc, curCfg)
|
|
|
+
|
|
|
+ for _, depName := range curCfg.Dependencies {
|
|
|
+ if _, ok := seen[depName]; ok {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ depSvc, err := openServiceForRead(scm, depName)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("opening service %q: %w", depName, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ deps = append(deps, depSvc)
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+ }()
|
|
|
+ }
|
|
|
+
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+type svcStateLogEntry struct {
|
|
|
+ ServiceName string `json:"serviceName"`
|
|
|
+ ServiceType string `json:"serviceType"`
|
|
|
+ State string `json:"state"`
|
|
|
+ StartupType string `json:"startupType"`
|
|
|
+ Triggers *_SERVICE_TRIGGER_INFO `json:"triggers,omitempty"`
|
|
|
+ TriggersError error `json:"triggersError,omitempty"`
|
|
|
+}
|
|
|
+
|
|
|
+type _SERVICE_TRIGGER_SPECIFIC_DATA_ITEM struct {
|
|
|
+ dataType uint32
|
|
|
+ cbData uint32
|
|
|
+ data *byte
|
|
|
+}
|
|
|
+
|
|
|
+type serviceTriggerSpecificDataItemJSONMarshal struct {
|
|
|
+ DataType uint32 `json:"dataType"`
|
|
|
+ Data string `json:"data,omitempty"`
|
|
|
+}
|
|
|
+
|
|
|
+func (tsdi *_SERVICE_TRIGGER_SPECIFIC_DATA_ITEM) MarshalJSON() ([]byte, error) {
|
|
|
+ m := serviceTriggerSpecificDataItemJSONMarshal{DataType: tsdi.dataType}
|
|
|
+
|
|
|
+ const maxDataLen = 128
|
|
|
+ data := unsafe.Slice(tsdi.data, tsdi.cbData)
|
|
|
+ if len(data) > maxDataLen {
|
|
|
+ // Only output the first maxDataLen bytes.
|
|
|
+ m.Data = fmt.Sprintf("%s... (truncated %d bytes)", hex.EncodeToString(data[:maxDataLen]), len(data)-maxDataLen)
|
|
|
+ } else {
|
|
|
+ m.Data = hex.EncodeToString(data)
|
|
|
+ }
|
|
|
+
|
|
|
+ return json.Marshal(m)
|
|
|
+}
|
|
|
+
|
|
|
+type _SERVICE_TRIGGER struct {
|
|
|
+ triggerType uint32
|
|
|
+ action uint32
|
|
|
+ triggerSubtype *windows.GUID
|
|
|
+ cDataItems uint32
|
|
|
+ pDataItems *_SERVICE_TRIGGER_SPECIFIC_DATA_ITEM
|
|
|
+}
|
|
|
+
|
|
|
+type serviceTriggerJSONMarshal struct {
|
|
|
+ TriggerType uint32 `json:"triggerType"`
|
|
|
+ Action uint32 `json:"action"`
|
|
|
+ TriggerSubtype string `json:"triggerSubtype,omitempty"`
|
|
|
+ DataItems []_SERVICE_TRIGGER_SPECIFIC_DATA_ITEM `json:"dataItems"`
|
|
|
+}
|
|
|
+
|
|
|
+func (ti *_SERVICE_TRIGGER) MarshalJSON() ([]byte, error) {
|
|
|
+ m := serviceTriggerJSONMarshal{
|
|
|
+ TriggerType: ti.triggerType,
|
|
|
+ Action: ti.action,
|
|
|
+ DataItems: unsafe.Slice(ti.pDataItems, ti.cDataItems),
|
|
|
+ }
|
|
|
+ if ti.triggerSubtype != nil {
|
|
|
+ m.TriggerSubtype = ti.triggerSubtype.String()
|
|
|
+ }
|
|
|
+ return json.Marshal(m)
|
|
|
+}
|
|
|
+
|
|
|
+type _SERVICE_TRIGGER_INFO struct {
|
|
|
+ cTriggers uint32
|
|
|
+ pTriggers *_SERVICE_TRIGGER
|
|
|
+ _ *byte // pReserved
|
|
|
+}
|
|
|
+
|
|
|
+func (sti *_SERVICE_TRIGGER_INFO) MarshalJSON() ([]byte, error) {
|
|
|
+ triggers := unsafe.Slice(sti.pTriggers, sti.cTriggers)
|
|
|
+ return json.Marshal(triggers)
|
|
|
+}
|
|
|
+
|
|
|
+// getSvcTriggerInfo obtains information about any system events that may be
|
|
|
+// used to start svc. Only relevant for demand-start (aka manual) services.
|
|
|
+func getSvcTriggerInfo(svc *mgr.Service) (*_SERVICE_TRIGGER_INFO, error) {
|
|
|
+ var desiredLen uint32
|
|
|
+ err := queryServiceConfig2(svc.Handle, windows.SERVICE_CONFIG_TRIGGER_INFO,
|
|
|
+ nil, 0, &desiredLen)
|
|
|
+ if err != windows.ERROR_INSUFFICIENT_BUFFER {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ buf := make([]byte, desiredLen)
|
|
|
+ err = queryServiceConfig2(svc.Handle, windows.SERVICE_CONFIG_TRIGGER_INFO,
|
|
|
+ &buf[0], desiredLen, &desiredLen)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return (*_SERVICE_TRIGGER_INFO)(unsafe.Pointer(&buf[0])), nil
|
|
|
+}
|
|
|
+
|
|
|
+// makeLogEntry consolidates relevant service information into a svcStateLogEntry.
|
|
|
+// We record the values of various service configuration constants as strings
|
|
|
+// so the the log entries are easy to interpret at a glance by humans.
|
|
|
+func makeLogEntry(svc *mgr.Service, status svc.Status, cfg mgr.Config) (entry svcStateLogEntry) {
|
|
|
+ entry.ServiceName = svc.Name
|
|
|
+
|
|
|
+ switch status.State {
|
|
|
+ case windows.SERVICE_STOPPED:
|
|
|
+ entry.State = "STOPPED"
|
|
|
+ case windows.SERVICE_START_PENDING:
|
|
|
+ entry.State = "START_PENDING"
|
|
|
+ case windows.SERVICE_STOP_PENDING:
|
|
|
+ entry.State = "STOP_PENDING"
|
|
|
+ case windows.SERVICE_RUNNING:
|
|
|
+ entry.State = "RUNNING"
|
|
|
+ case windows.SERVICE_CONTINUE_PENDING:
|
|
|
+ entry.State = "CONTINUE_PENDING"
|
|
|
+ case windows.SERVICE_PAUSE_PENDING:
|
|
|
+ entry.State = "PAUSE_PENDING"
|
|
|
+ case windows.SERVICE_PAUSED:
|
|
|
+ entry.State = "PAUSED"
|
|
|
+ case windows.SERVICE_NO_CHANGE:
|
|
|
+ entry.State = "NO_CHANGE"
|
|
|
+ default:
|
|
|
+ entry.State = fmt.Sprintf("Unknown constant %d", status.State)
|
|
|
+ }
|
|
|
+
|
|
|
+ switch cfg.ServiceType {
|
|
|
+ case windows.SERVICE_FILE_SYSTEM_DRIVER:
|
|
|
+ entry.ServiceType = "FILE_SYSTEM_DRIVER"
|
|
|
+ case windows.SERVICE_KERNEL_DRIVER:
|
|
|
+ entry.ServiceType = "KERNEL_DRIVER"
|
|
|
+ case windows.SERVICE_WIN32_OWN_PROCESS, windows.SERVICE_WIN32_SHARE_PROCESS:
|
|
|
+ entry.ServiceType = "WIN32"
|
|
|
+ default:
|
|
|
+ entry.ServiceType = fmt.Sprintf("Unknown constant %d", cfg.ServiceType)
|
|
|
+ }
|
|
|
+
|
|
|
+ switch cfg.StartType {
|
|
|
+ case windows.SERVICE_BOOT_START:
|
|
|
+ entry.StartupType = "BOOT_START"
|
|
|
+ case windows.SERVICE_SYSTEM_START:
|
|
|
+ entry.StartupType = "SYSTEM_START"
|
|
|
+ case windows.SERVICE_AUTO_START:
|
|
|
+ if cfg.DelayedAutoStart {
|
|
|
+ entry.StartupType = "DELAYED_AUTO_START"
|
|
|
+ } else {
|
|
|
+ entry.StartupType = "AUTO_START"
|
|
|
+ }
|
|
|
+ case windows.SERVICE_DEMAND_START:
|
|
|
+ entry.StartupType = "DEMAND_START"
|
|
|
+ triggerInfo, err := getSvcTriggerInfo(svc)
|
|
|
+ if err == nil {
|
|
|
+ entry.Triggers = triggerInfo
|
|
|
+ } else {
|
|
|
+ entry.TriggersError = err
|
|
|
+ }
|
|
|
+ case windows.SERVICE_DISABLED:
|
|
|
+ entry.StartupType = "DISABLED"
|
|
|
+ default:
|
|
|
+ entry.StartupType = fmt.Sprintf("Unknown constant %d", cfg.StartType)
|
|
|
+ }
|
|
|
+
|
|
|
+ return entry
|
|
|
+}
|
|
|
+
|
|
|
+// connectToLocalSCMForRead connects to the Windows Service Control Manager with
|
|
|
+// read-only access. x/sys/windows/svc/mgr/Connect requests read+write access,
|
|
|
+// which requires higher privileges than we want.
|
|
|
+func connectToLocalSCMForRead() (*mgr.Mgr, error) {
|
|
|
+ h, err := windows.OpenSCManager(nil, nil, windows.GENERIC_READ)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &mgr.Mgr{Handle: h}, nil
|
|
|
+}
|
|
|
+
|
|
|
+// openServiceForRead opens a service with read-only access.
|
|
|
+// x/sys/windows/svc/mgr/(*Mgr).OpenService requests read+write access,
|
|
|
+// which requires higher privileges than we want.
|
|
|
+func openServiceForRead(scm *mgr.Mgr, svcName string) (*mgr.Service, error) {
|
|
|
+ svcNamePtr, err := windows.UTF16PtrFromString(svcName)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ h, err := windows.OpenService(scm.Handle, svcNamePtr, windows.GENERIC_READ)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &mgr.Service{Name: svcName, Handle: h}, nil
|
|
|
+}
|