Files
nomad/helper/winsvc/windows_service_windows.go
Chris Roberts 48d91dc1f9 [winsvc] Add interfaces for Windows services and service manager
Provides interfaces to the Windows service manager and Windows
services. These interfaces support creating new Windows services,
deleting Windows services, configuring Windows services, and
registering/deregistering services with Windows Eventlog.

A path helper is included to support expansion of paths using a
subset of known folder IDs.

A privileged helper is included to check that the process is
currently being executed with elevated privileges, which are
required for managing Windows services and modifying the registry.
2025-09-02 16:39:45 -07:00

257 lines
5.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"reflect"
"slices"
"time"
"github.com/hashicorp/nomad/helper"
"golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"golang.org/x/sys/windows/svc/mgr"
)
// Base registry path for eventlog registrations
const EVENTLOG_REGISTRY_PATH = `SYSTEM\CurrentControlSet\Services\EventLog\Application`
// Registry value name for supported event types
const EVENTLOG_SUPPORTED_EVENTS_KEY = "TypesSupported"
// Event types registered as supported
const EVENTLOG_SUPPORTED_EVENTS uint32 = eventlog.Error | eventlog.Warning | eventlog.Info
// NewWindowsServiceManager creates a new instance of the wrapper
// to interact with the Windows service manager.
func NewWindowsServiceManager() (WindowsServiceManager, error) {
m, err := mgr.Connect()
if err != nil {
return nil, err
}
return &windowsServiceManager{manager: m}, nil
}
type windowsServiceManager struct {
manager *mgr.Mgr
}
func (m *windowsServiceManager) IsServiceRegistered(name string) (bool, error) {
list, err := m.manager.ListServices()
if err != nil {
return false, err
}
if slices.Contains(list, name) {
return true, nil
}
return false, nil
}
func (m *windowsServiceManager) GetService(name string) (WindowsService, error) {
service, err := m.manager.OpenService(name)
if err != nil {
return nil, err
}
return &windowsService{service: service}, nil
}
func (m *windowsServiceManager) CreateService(name, bin string, config WindowsServiceConfiguration) (WindowsService, error) {
wsvc, err := m.manager.CreateService(name, bin, mgr.Config{})
if err != nil {
return nil, err
}
service := &windowsService{service: wsvc}
// Only apply configuration if configuration is provided
if !reflect.ValueOf(config).IsZero() {
if err := service.Configure(config); err != nil {
return nil, err
}
}
return service, nil
}
func (m *windowsServiceManager) Close() error {
return m.manager.Disconnect()
}
type windowsService struct {
service *mgr.Service
}
func (s *windowsService) Name() string {
return s.service.Name
}
func (s *windowsService) Configure(config WindowsServiceConfiguration) error {
serviceCfg, err := s.service.Config()
if err != nil {
return err
}
serviceCfg.StartType = uint32(config.StartType)
serviceCfg.DisplayName = config.DisplayName
serviceCfg.Description = config.Description
serviceCfg.BinaryPathName = config.BinaryPathName
if err := s.service.UpdateConfig(serviceCfg); err != nil {
return err
}
return nil
}
func (s *windowsService) Start() error {
if running, _ := s.IsRunning(); running {
return nil
}
if err := s.service.Start(); err != nil {
return err
}
if err := waitFor(context.Background(), s.IsRunning); err != nil {
return err
}
return nil
}
func (s *windowsService) Stop() error {
if stopped, _ := s.IsStopped(); stopped {
return nil
}
if _, err := s.service.Control(svc.Stop); err != nil {
return err
}
if err := waitFor(context.Background(), s.IsStopped); err != nil {
return err
}
return nil
}
func (s *windowsService) Close() error {
return s.service.Close()
}
func (s *windowsService) Delete() error {
return s.service.Delete()
}
func (s *windowsService) IsRunning() (bool, error) {
return s.isService(svc.Running)
}
func (s *windowsService) IsStopped() (bool, error) {
return s.isService(svc.Stopped)
}
func (s *windowsService) EnableEventlog() error {
// Check if the service is already setup in the eventlog
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
registry.ALL_ACCESS,
)
// If it could not be opened, assume error is caused
// due to nonexistence. If it was for some other reason
// the error will be encountered again when attempting to
// create.
if err != nil {
if err := eventlog.InstallAsEventCreate(s.Name(), EVENTLOG_SUPPORTED_EVENTS); err != nil {
return err
}
} else {
defer key.Close()
// Since the service is already registered, just
// ensure it is properly configured. Currently
// that just means the supported events.
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
if err != nil || uint32(val) != EVENTLOG_SUPPORTED_EVENTS {
if err := key.SetDWordValue(EVENTLOG_SUPPORTED_EVENTS_KEY, EVENTLOG_SUPPORTED_EVENTS); err != nil {
return err
}
}
}
return nil
}
func (s *windowsService) DisableEventlog() error {
// Check if the service is currently enabled in the eventlog
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
registry.READ,
)
if errors.Is(err, fs.ErrNotExist) {
return nil
}
defer key.Close()
return eventlog.Remove(s.Name())
}
func (s *windowsService) isService(state svc.State) (bool, error) {
status, err := s.service.Query()
if err != nil {
return false, err
}
return status.State == state, nil
}
func waitFor(ctx context.Context, condition func() (bool, error)) error {
d, err := time.ParseDuration(WINDOWS_SERVICE_STATE_TIMEOUT)
if err != nil {
return err
}
// Setup a deadline
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(d))
defer cancel()
// Watch for any interrupts
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()
pauseDur := time.Millisecond * 250
t, timerStop := helper.NewSafeTimer(pauseDur)
defer timerStop()
for {
t.Reset(pauseDur)
complete, err := condition()
if err != nil {
return err
}
if complete {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("timeout exceeded waiting for process")
case <-t.C:
}
}
}