Files
HATray/internal/service/windows.go
Xevion 8d0bb4607e refactor: simplify interactive console run path, remove 'interactive' mode, use debug service
I'm unsure why this was originally done; I hope I'm not messing up by
doing this. Based on documentation, it makes more sense that the debug
mode is the intention of the 'interactive console' concept.
2025-06-23 14:31:20 -05:00

125 lines
3.5 KiB
Go

//go:build windows
package service
import (
"fmt"
"log/slog"
"time"
"ha-tray/internal/app"
winsvc "golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/debug"
"golang.org/x/sys/windows/svc/eventlog"
)
const serviceName = "HATray"
// WindowsService implements the Service interface for Windows
type WindowsService struct {
app *app.App
logger *slog.Logger // logger instance, logs to file (and console in debug mode)
elog debug.Log // event log instance; connects to the Windows Event Log
}
// newService creates a new Windows service instance
func NewService(logger *slog.Logger) Service {
return &WindowsService{
logger: logger,
app: app.NewApp(logger),
}
}
// Run implements the Service interface for Windows
func (svc *WindowsService) Run() error {
// Determine if we're running as a Windows service
isService, err := winsvc.IsWindowsService()
if err != nil {
return fmt.Errorf("failed to determine if running as Windows service: %v", err)
}
svc.logger.Debug("isService", "value", isService)
var run func(string, winsvc.Handler) error
// Acquire the appropriate run function & eventlog instance depending on service type
if isService {
run = winsvc.Run
svc.elog, err = eventlog.Open(serviceName)
if err != nil {
return fmt.Errorf("failed to open event log: %v", err)
}
} else {
run = debug.Run
svc.elog = debug.New(serviceName)
}
defer svc.elog.Close()
svc.elog.Info(1, fmt.Sprintf("starting %s service", serviceName))
// Run the service with our handler
err = run(serviceName, &serviceHandler{
service: svc,
})
if err != nil {
svc.elog.Error(1, fmt.Sprintf("%s service failed: %v", serviceName, err))
return err
}
return nil
}
type serviceHandler struct {
service *WindowsService
}
func (handler *serviceHandler) Execute(args []string, r <-chan winsvc.ChangeRequest, changes chan<- winsvc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = winsvc.AcceptStop | winsvc.AcceptShutdown | winsvc.AcceptPauseAndContinue
changes <- winsvc.Status{State: winsvc.StartPending}
handler.service.logger.Info("service starting")
changes <- winsvc.Status{State: winsvc.Running, Accepts: cmdsAccepted}
// Service heartbeat
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case c := <-r:
switch c.Cmd {
case winsvc.Interrogate:
changes <- c.CurrentStatus
case winsvc.Stop, winsvc.Shutdown:
changes <- winsvc.Status{State: winsvc.StopPending}
handler.service.logger.Info("service stopping")
if err := handler.service.app.Stop(); err != nil {
handler.service.logger.Error("Failed to stop app layer", "error", err)
}
return
case winsvc.Pause:
changes <- winsvc.Status{State: winsvc.Paused, Accepts: cmdsAccepted}
handler.service.logger.Info("service pausing")
if err := handler.service.app.Pause(); err != nil {
handler.service.logger.Error("Failed to pause app layer", "error", err)
}
case winsvc.Continue:
changes <- winsvc.Status{State: winsvc.Running, Accepts: cmdsAccepted}
handler.service.logger.Info("service continuing")
if err := handler.service.app.Resume(); err != nil {
handler.service.logger.Error("Failed to resume app layer", "error", err)
}
default:
// Log the error to the event log & service logger
handler.service.logger.Error("unexpected control request", "request", c)
handler.service.elog.Error(uint32(1), fmt.Sprintf("unexpected control request #%d", c))
}
case <-ticker.C:
handler.service.logger.Debug("heartbeat")
}
}
}