[winsvc] Add support for Windows Eventlog (#26441)

Defines a `winsvc.Event` type which can be sent using the `winsvc.SendEvent`
function. If nomad is running on Windows and can send to the Windows
Eventlog the event will be sent. Initial event types are defined for
starting, ready, stopped, and log message.

The `winsvc.EventLogger` provides an `io.WriteCloser` that can be included
in the logger's writers collection. It will extract the log level from
log lines and write them appropriately to the eventlog. The eventlog
only supports error, warning, and info levels so messages with other
levels will be ignored.

A new configuration block is included for enabling logging to the
eventlog. Logging must be enabled with the `log_level` option and
the `eventlog.level` value can then be of the same or higher severity.
This commit is contained in:
Chris Roberts
2025-08-05 14:37:58 -07:00
parent 48d91dc1f9
commit 61c36bdef7
17 changed files with 838 additions and 23 deletions

View File

@@ -75,6 +75,7 @@ func (c *Command) readConfig() *Config {
ACL: &ACLConfig{},
Audit: &config.AuditConfig{},
Reporting: &config.ReportingConfig{},
Eventlog: &Eventlog{},
}
flags := flag.NewFlagSet("agent", flag.ContinueOnError)
@@ -132,6 +133,10 @@ func (c *Command) readConfig() *Config {
flags.BoolVar(&cmdConfig.LogIncludeLocation, "log-include-location", false, "")
flags.StringVar(&cmdConfig.NodeName, "node", "", "")
// Eventlog options
flags.BoolVar(&cmdConfig.Eventlog.Enabled, "eventlog", false, "")
flags.StringVar(&cmdConfig.Eventlog.Level, "eventlog-level", "", "")
// Consul options
defaultConsul := cmdConfig.defaultConsul()
flags.StringVar(&defaultConsul.Auth, "consul-auth", "", "")
@@ -489,7 +494,12 @@ func (c *Command) IsValidConfig(config, cmdConfig *Config) bool {
return false
}
if err := config.RPC.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v)", err))
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v", err))
return false
}
if err := config.Eventlog.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("eventlog block invalid: %v", err))
return false
}
@@ -587,6 +597,7 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
if logLevel == "OFF" {
config.EnableSyslog = false
}
// Check if syslog is enabled
if config.EnableSyslog {
ui.Output(fmt.Sprintf("Config enable_syslog is `true` with log_level=%v", config.LogLevel))
@@ -598,6 +609,17 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
writers = append(writers, newSyslogWriter(l, config.LogJson))
}
// Check if eventlog is enabled
if config.Eventlog != nil && config.Eventlog.Enabled {
l, err := winsvc.NewEventLogger(config.Eventlog.Level)
if err != nil {
ui.Error(fmt.Sprintf("Windows event logger setup failed: %s", err))
return nil, nil
}
writers = append(writers, l)
}
// Check if file logging is enabled
if config.LogFile != "" {
dir, fileName := filepath.Split(config.LogFile)
@@ -773,6 +795,8 @@ func (c *Command) AutocompleteFlags() complete.Flags {
"-vault-tls-server-name": complete.PredictAnything,
"-acl-enabled": complete.PredictNothing,
"-acl-replication-token": complete.PredictAnything,
"-eventlog": complete.PredictNothing,
"-eventlog-level": complete.PredictSet("INFO", "WARN", "ERROR"),
}
}
@@ -920,6 +944,10 @@ func (c *Command) Run(args []string) int {
return 1
}
// Add events for the eventlog
winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceReady))
defer func() { winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceStopped)) }()
// Wait for exit
return c.handleSignals()
}
@@ -1481,6 +1509,14 @@ General Options (clients and servers):
-log-include-location
Include file and line information in each log line. The default is false.
-eventlog
Enable sending Nomad agent logs to the Windows Event Log.
-eventlog-level
Specifies the verbosity of logs the Nomad agent outputs. Valid log levels
include ERROR, WARN, or INFO in order of verbosity. Level must be
of equal or less verbosity as defined for the -log-level parameter.
-node=<name>
The name of the local agent. This name is used to identify the node
in the cluster. The name must be unique per region. The default is

View File

@@ -77,6 +77,10 @@ func TestCommand_Args(t *testing.T) {
[]string{"-client", "-node-pool=not@valid"},
"Invalid node pool",
},
{
[]string{"-client", "-eventlog-level", "DEBUG"},
"eventlog.level must be one of INFO, WARN, or ERROR",
},
}
for _, tc := range tcases {
// Make a new command. We preemptively close the shutdownCh
@@ -502,6 +506,33 @@ func TestIsValidConfig(t *testing.T) {
},
err: "unknown keyring provider",
},
{
name: "ValidEventlog",
conf: Config{
DataDir: "/tmp",
Client: &ClientConfig{
Enabled: true,
},
Eventlog: &Eventlog{
Enabled: true,
Level: "INFO",
},
},
},
{
name: "InvalidEventlog",
conf: Config{
DataDir: "/tmp",
Client: &ClientConfig{
Enabled: true,
},
Eventlog: &Eventlog{
Enabled: true,
Level: "DEBUG",
},
},
err: "eventlog.level must be one of INFO, WARN, or ERROR",
},
}
for _, tc := range cases {

View File

@@ -29,6 +29,7 @@ import (
"github.com/hashicorp/nomad/helper/ipaddr"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
@@ -199,6 +200,9 @@ type Config struct {
// ExtraKeysHCL is used by hcl to surface unexpected keys
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
// Configure logging to Windows eventlog
Eventlog *Eventlog `hcl:"eventlog"`
}
func (c *Config) defaultConsul() *config.ConsulConfig {
@@ -1399,6 +1403,60 @@ func (t *Telemetry) Validate() error {
return nil
}
// Eventlog is the configuration for the Windows Eventlog
type Eventlog struct {
// Enabled controls if Nomad agent logs are sent to the
// Windows eventlog.
Enabled bool `hcl:"enabled"`
// Level of logs to send to eventlog. May be set to higher
// severity than LogLevel but lower level will be ignored.
Level string `hcl:"level"`
}
// Copy is used to copy the Eventlog configuration
func (e *Eventlog) Copy() *Eventlog {
return &Eventlog{
Enabled: e.Enabled,
Level: e.Level,
}
}
// Merge is used to merge Eventlog configurations
func (e *Eventlog) Merge(b *Eventlog) *Eventlog {
if e == nil {
return b
}
result := *e
if b == nil {
return &result
}
if b.Enabled {
result.Enabled = b.Enabled
}
if b.Level != "" {
result.Level = b.Level
}
return &result
}
// Validate validates the eventlog configuration
func (e *Eventlog) Validate() error {
if e == nil {
return nil
}
if winsvc.EventlogLevelFromString(e.Level) == winsvc.EVENTLOG_LEVEL_UNKNOWN {
return errors.New("eventlog.level must be one of INFO, WARN, or ERROR")
}
return nil
}
// Ports encapsulates the various ports we bind to for network services. If any
// are not specified then the defaults are used instead.
type Ports struct {
@@ -1732,6 +1790,10 @@ func DefaultConfig() *Config {
collectionInterval: 1 * time.Second,
DisableAllocationHookMetrics: pointer.Of(false),
},
Eventlog: &Eventlog{
Enabled: false,
Level: "error",
},
TLSConfig: &config.TLSConfig{},
Sentinel: &config.SentinelConfig{},
Version: version.GetVersion(),
@@ -1844,6 +1906,13 @@ func (c *Config) Merge(b *Config) *Config {
result.Telemetry = result.Telemetry.Merge(b.Telemetry)
}
// Apply the eventlog config
if result.Eventlog == nil && b.Eventlog != nil {
result.Eventlog = b.Eventlog.Copy()
} else if b.Eventlog != nil {
result.Eventlog = result.Eventlog.Merge(b.Eventlog)
}
// Apply the Reporting Config
if result.Reporting == nil && b.Reporting != nil {
result.Reporting = b.Reporting.Copy()

View File

@@ -48,6 +48,7 @@ func TestConfig_Merge(t *testing.T) {
AdvertiseAddrs: &AdvertiseAddrs{},
Sentinel: &config.SentinelConfig{},
Autopilot: &config.AutopilotConfig{},
Eventlog: &Eventlog{},
}
c2 := &Config{
@@ -236,6 +237,10 @@ func TestConfig_Merge(t *testing.T) {
},
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "INFO",
},
}
c3 := &Config{
@@ -489,6 +494,10 @@ func TestConfig_Merge(t *testing.T) {
Enabled: pointer.Of(true),
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "ERROR",
},
}
result := c0.Merge(c1)
@@ -2013,3 +2022,86 @@ func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
}
}
func TestEventlog_Merge(t *testing.T) {
t.Run("nil rhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c1 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c1)
})
t.Run("nil lhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c2 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c2)
})
t.Run("full merge", func(t *testing.T) {
c1 := &Eventlog{
Enabled: false,
Level: "info",
}
c2 := &Eventlog{
Enabled: true,
Level: "error",
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
must.Eq(t, result.Level, "error")
})
t.Run("enabled merge", func(t *testing.T) {
// NOTE: Can only be enabled, not disabled
c1 := &Eventlog{
Enabled: true,
}
c2 := &Eventlog{
Enabled: false,
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
})
}
func TestEventlog_Validate(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
desc string
eventlog *Eventlog
shouldErr bool
}{
{
desc: "valid level",
eventlog: &Eventlog{Level: "info"},
},
{
desc: "invalid level",
eventlog: &Eventlog{Level: "debug"},
shouldErr: true,
},
{
desc: "nil eventlog",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ci.Parallel(t)
if tc.shouldErr {
must.Error(t, tc.eventlog.Validate())
} else {
must.NoError(t, tc.eventlog.Validate())
}
})
}
}

73
helper/winsvc/event.go Normal file
View File

@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
type WindowsEventId uint32
//go:generate stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId
const (
EventUnknown WindowsEventId = iota // unknown event
EventServiceStarting // service starting
EventServiceReady // service ready
EventServiceStopped // service stopped
EventLogMessage // log message
)
// NewEvent creates a new Event for the Windows Eventlog
func NewEvent(kind WindowsEventId, opts ...EventOption) Event {
evt := &event{
kind: kind,
level: EVENTLOG_LEVEL_INFO,
}
for _, fn := range opts {
fn(evt)
}
return evt
}
type Event interface {
Kind() WindowsEventId
Message() string
Level() EventlogLevel
}
type EventOption func(*event)
// WithEventMessage sets a custom message for the event
func WithEventMessage(msg string) EventOption {
return func(e *event) {
e.message = msg
}
}
// WithEventLevel specifies the level used for the event
func WithEventLevel(level EventlogLevel) EventOption {
return func(e *event) {
e.level = level
}
}
type event struct {
kind WindowsEventId
message string
level EventlogLevel
}
func (e *event) Kind() WindowsEventId {
return e.kind
}
func (e *event) Message() string {
if e.message != "" {
return e.message
}
return e.kind.String()
}
func (e *event) Level() EventlogLevel {
return e.level
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"regexp"
"strings"
)
type EventlogLevel uint8
//go:generate stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel
const (
EVENTLOG_LEVEL_UNKNOWN EventlogLevel = iota
EVENTLOG_LEVEL_INFO
EVENTLOG_LEVEL_WARN
EVENTLOG_LEVEL_ERROR
)
// EventlogLevelFromString converts a log level string to the correct constant
func EventlogLevelFromString(level string) EventlogLevel {
switch strings.ToUpper(level) {
case EVENTLOG_LEVEL_INFO.String():
return EVENTLOG_LEVEL_INFO
case EVENTLOG_LEVEL_WARN.String():
return EVENTLOG_LEVEL_WARN
case EVENTLOG_LEVEL_ERROR.String():
return EVENTLOG_LEVEL_ERROR
}
return EVENTLOG_LEVEL_UNKNOWN
}
var logPattern = regexp.MustCompile(`(?s)\[(ERROR|WARN|INFO)\] (.+)`)
type Eventlog interface {
Info(uint32, string) error
Warning(uint32, string) error
Error(uint32, string) error
Close() error
}
type eventLogger struct {
evtLog Eventlog
level EventlogLevel
}
// Close closes the eventlog
func (e *eventLogger) Close() error {
return e.evtLog.Close()
}
// Write writes logging message to the eventlog
func (e *eventLogger) Write(p []byte) (int, error) {
matches := logPattern.FindStringSubmatch(string(p))
// If no match was found, or the incorrect number of
// elements detected then ignore
if matches == nil || len(matches) != 3 {
return len(p), nil
}
level := EventlogLevelFromString(matches[1])
// If the detected level of the message isn't currently
// allowed then ignore
if !e.allowed(level) {
return len(p), nil
}
// Still here so send the message to the eventlog
switch level {
case EVENTLOG_LEVEL_INFO:
e.evtLog.Info(uint32(EventLogMessage), matches[2])
case EVENTLOG_LEVEL_WARN:
e.evtLog.Warning(uint32(EventLogMessage), matches[2])
case EVENTLOG_LEVEL_ERROR:
e.evtLog.Error(uint32(EventLogMessage), matches[2])
}
return len(p), nil
}
// Check if level is allowed
func (e *eventLogger) allowed(level EventlogLevel) bool {
return level >= e.level
}
type nullEventlog struct{}
func (n *nullEventlog) Info(uint32, string) error {
return nil
}
func (n *nullEventlog) Warning(uint32, string) error {
return nil
}
func (n *nullEventlog) Error(uint32, string) error {
return nil
}
func (n *nullEventlog) Close() error {
return nil
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
import (
"errors"
"io"
)
// NewEventLogger is a stub for non-Windows platforms to generate
// and error when used.
func NewEventLogger(_ string) (io.WriteCloser, error) {
return nil, errors.New("EventLogger is not supported on this platform")
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"io"
"testing"
"github.com/shoenig/test/must"
)
func testEventLogger(e Eventlog, l EventlogLevel) io.WriteCloser {
return &eventLogger{
level: l,
evtLog: e,
}
}
func TestEventlogLevelFromString(t *testing.T) {
t.Run("INFO", func(t *testing.T) {
for _, val := range []string{"INFO", "info"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_INFO, l)
}
})
t.Run("WARN", func(t *testing.T) {
for _, val := range []string{"WARN", "warn"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_WARN, l)
}
})
t.Run("ERROR", func(t *testing.T) {
for _, val := range []string{"ERROR", "error"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_ERROR, l)
}
})
}
func TestEventLogger(t *testing.T) {
defaultmsgs := []string{
"1970-01-01T16:27:16.116Z [INFO] Information line",
"1970-01-01T16:27:16.116Z [WARN] Warning line",
"1970-01-01T16:27:16.116Z [ERROR] Error line",
}
testCases := []struct {
desc string
msgs []string
level EventlogLevel
setup func(*MockEventlog)
}{
{
desc: "basic usage",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
},
},
{
desc: "higher level",
level: EVENTLOG_LEVEL_ERROR,
setup: func(m *MockEventlog) {
m.ExpectError(EventLogMessage, "Error line")
},
},
{
desc: "debug and trace logs",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
},
msgs: append(defaultmsgs, []string{
"[DEBUG] Debug line",
"[TRACE] Trace line",
}...),
},
{
desc: "with multi-line logs",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
m.ExpectInfo(EventLogMessage, "Information log\nthat includes\nmultiple lines")
m.ExpectWarning(EventLogMessage, "Warning log\nthat includes second line")
},
msgs: append(defaultmsgs, []string{
"[INFO] Information log\nthat includes\nmultiple lines",
"[WARN] Warning log\nthat includes second line",
}...),
},
}
for _, tc := range testCases {
if len(tc.msgs) < 1 {
tc.msgs = defaultmsgs
}
el := NewMockEventlog(t)
tc.setup(el)
eventLogger := testEventLogger(el, tc.level)
for _, msg := range tc.msgs {
eventLogger.Write([]byte(msg))
}
el.AssertExpectations()
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"fmt"
"io"
"golang.org/x/sys/windows/svc/eventlog"
)
// NewEventLogger creates a new event logger instance
func NewEventLogger(level string) (io.WriteCloser, error) {
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
if err != nil {
return nil, fmt.Errorf("Failed to open Windows eventlog: %w", err)
}
return &eventLogger{
evtLog: evtLog,
level: EventlogLevelFromString(level),
}, nil
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"testing"
"github.com/shoenig/test/must"
)
func TestNewEvent(t *testing.T) {
t.Run("simple", func(t *testing.T) {
event := NewEvent(EventServiceReady)
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
must.Eq(t, EventServiceReady.String(), event.Message())
})
t.Run("WithEventMessage", func(t *testing.T) {
event := NewEvent(EventServiceReady, WithEventMessage("Custom service ready message"))
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
must.Eq(t, "Custom service ready message", event.Message())
})
t.Run("WithEventLevel", func(t *testing.T) {
event := NewEvent(EventServiceReady, WithEventLevel(EVENTLOG_LEVEL_ERROR))
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_ERROR, event.Level())
must.Eq(t, EventServiceReady.String(), event.Message())
})
t.Run("multiple options", func(t *testing.T) {
event := NewEvent(EventServiceStopped,
WithEventMessage("Custom service stopped message"),
WithEventLevel(EVENTLOG_LEVEL_WARN),
)
must.Eq(t, EventServiceStopped, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_WARN, event.Level())
must.Eq(t, "Custom service stopped message", event.Message())
})
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
// SendEvent sends an event to the Windows eventlog
func SendEvent(e Event) {}

View File

@@ -0,0 +1,26 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/helper"
)
var chanEvents = make(chan Event)
// SendEvent sends an event to the Windows eventlog
func SendEvent(e Event) {
timer, stop := helper.NewSafeTimer(100 * time.Millisecond)
defer stop()
select {
case chanEvents <- e:
case <-timer.C:
hclog.L().Error("failed to send event to windows eventlog, timed out",
"event", e)
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"testing"
"github.com/shoenig/test/must"
)
func NewMockEventlog(t *testing.T) *MockEventlog {
return &MockEventlog{
t: t,
}
}
type MockEventlog struct {
infos []mockArgs
warnings []mockArgs
errors []mockArgs
t *testing.T
}
type mockArgs struct {
winId uint32
msg string
}
func (m *MockEventlog) ExpectInfo(v1 WindowsEventId, v2 string) {
m.infos = append(m.infos, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) ExpectWarning(v1 WindowsEventId, v2 string) {
m.warnings = append(m.warnings, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) ExpectError(v1 WindowsEventId, v2 string) {
m.errors = append(m.errors, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) Info(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.infos[0]
m.infos = m.infos[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Warning(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.warnings[0]
m.warnings = m.warnings[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Error(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.errors[0]
m.errors = m.errors[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Close() error { return nil }
func (m *MockEventlog) AssertExpectations() {
must.SliceEmpty(m.t, m.infos, must.Sprintf("Info expecting %d more invocations", len(m.infos)))
must.SliceEmpty(m.t, m.warnings, must.Sprintf("Warning expecting %d more invocations", len(m.warnings)))
must.SliceEmpty(m.t, m.errors, must.Sprintf("Error expecting %d more invocations", len(m.errors)))
}

View File

@@ -15,10 +15,10 @@ const (
WINDOWS_SERVICE_STATE_TIMEOUT = "1m"
)
var chanGraceExit = make(chan int)
var chanGraceExit = make(chan struct{})
// ShutdownChannel returns a channel that sends a message that a shutdown
// signal has been received for the service.
func ShutdownChannel() <-chan int {
func ShutdownChannel() <-chan struct{} {
return chanGraceExit
}

View File

@@ -1,46 +1,84 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build windows
// +build windows
package winsvc
import (
wsvc "golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
)
type serviceWindows struct{}
// Commands that are currently supported
const SERVICE_ACCEPTED_COMMANDS = svc.AcceptStop | svc.AcceptShutdown
type serviceWindows struct {
evtLog Eventlog
}
func init() {
interactive, err := wsvc.IsAnInteractiveSession()
isSvc, err := svc.IsWindowsService()
if err != nil {
panic(err)
}
// Cannot run as a service when running interactively
if interactive {
// This should only run when running
// as a service
if !isSvc {
return
}
go wsvc.Run("", serviceWindows{})
go executeWindowsService()
}
// Execute implements the Windows service Handler type. It will be
// called at the start of the service, and the service will exit
// once Execute completes.
func (serviceWindows) Execute(args []string, r <-chan wsvc.ChangeRequest, s chan<- wsvc.Status) (svcSpecificEC bool, exitCode uint32) {
const accCommands = wsvc.AcceptStop | wsvc.AcceptShutdown
s <- wsvc.Status{State: wsvc.Running, Accepts: accCommands}
func (srv serviceWindows) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
s <- svc.Status{State: svc.Running, Accepts: SERVICE_ACCEPTED_COMMANDS}
srv.evtLog.Info(uint32(EventServiceStarting), "service starting")
LOOP:
for {
c := <-r
switch c.Cmd {
case wsvc.Interrogate:
s <- c.CurrentStatus
case wsvc.Stop, wsvc.Shutdown:
s <- wsvc.Status{State: wsvc.StopPending}
chanGraceExit <- 1
return false, 0
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
s <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
srv.evtLog.Info(uint32(EventLogMessage), "service stop requested")
s <- svc.Status{State: svc.StopPending}
close(chanGraceExit)
}
case e := <-chanEvents:
switch e.Level() {
case EVENTLOG_LEVEL_INFO:
srv.evtLog.Info(uint32(e.Kind()), e.Message())
case EVENTLOG_LEVEL_WARN:
srv.evtLog.Warning(uint32(e.Kind()), e.Message())
case EVENTLOG_LEVEL_ERROR:
srv.evtLog.Error(uint32(e.Kind()), e.Message())
}
if e.Kind() == EventServiceStopped {
break LOOP
}
}
}
return false, 0
}
func executeWindowsService() {
var evtLog Eventlog
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
if err != nil {
// Eventlog will only be available if the
// service was properly registered. If the
// service was manually setup, it will likely
// not have been registered with the eventlog
// so it will not be available. In that case
// just stub out the eventlog.
evtLog = &nullEventlog{}
}
defer evtLog.Close()
svc.Run(WINDOWS_SERVICE_NAME, serviceWindows{evtLog: evtLog})
}

View File

@@ -0,0 +1,26 @@
// Code generated by "stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel"; DO NOT EDIT.
package winsvc
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[EVENTLOG_LEVEL_UNKNOWN-0]
_ = x[EVENTLOG_LEVEL_INFO-1]
_ = x[EVENTLOG_LEVEL_WARN-2]
_ = x[EVENTLOG_LEVEL_ERROR-3]
}
const _EventlogLevel_name = "UNKNOWNINFOWARNERROR"
var _EventlogLevel_index = [...]uint8{0, 7, 11, 15, 20}
func (i EventlogLevel) String() string {
if i >= EventlogLevel(len(_EventlogLevel_index)-1) {
return "EventlogLevel(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _EventlogLevel_name[_EventlogLevel_index[i]:_EventlogLevel_index[i+1]]
}

View File

@@ -0,0 +1,27 @@
// Code generated by "stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId"; DO NOT EDIT.
package winsvc
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[EventUnknown-0]
_ = x[EventServiceStarting-1]
_ = x[EventServiceReady-2]
_ = x[EventServiceStopped-3]
_ = x[EventLogMessage-4]
}
const _WindowsEventId_name = "unknown eventservice startingservice readyservice stoppedlog message"
var _WindowsEventId_index = [...]uint8{0, 13, 29, 42, 57, 68}
func (i WindowsEventId) String() string {
if i >= WindowsEventId(len(_WindowsEventId_index)-1) {
return "WindowsEventId(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _WindowsEventId_name[_WindowsEventId_index[i]:_WindowsEventId_index[i+1]]
}