mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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.
257 lines
5.6 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|