// +build windows package main import ( "context" "fmt" "time" "github.com/rcourtman/pulse-go-rewrite/internal/hostagent" "github.com/rs/zerolog" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" ) type windowsService struct { cfg hostagent.Config logger zerolog.Logger eventLog *eventlog.Log } func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown changes <- svc.Status{State: svc.StartPending} // Log to Windows Event Log if ws.eventLog != nil { ws.eventLog.Info(1, "Pulse Host Agent service starting") } agent, err := hostagent.New(ws.cfg) if err != nil { ws.logger.Error().Err(err).Msg("Failed to create host agent") changes <- svc.Status{State: svc.Stopped} return true, 1 } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start the agent in a goroutine errChan := make(chan error, 1) go func() { ws.logger.Info(). Str("pulse_url", ws.cfg.PulseURL). Str("agent_id", ws.cfg.AgentID). Dur("interval", ws.cfg.Interval). Msg("Starting Pulse host agent as Windows service") if err := agent.Run(ctx); err != nil && err != context.Canceled { errChan <- err } close(errChan) }() changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} ws.logger.Info().Msg("Host agent service is running") if ws.eventLog != nil { ws.eventLog.Info(1, fmt.Sprintf("Pulse Host Agent started successfully (URL: %s, Interval: %s)", ws.cfg.PulseURL, ws.cfg.Interval)) } // Service control loop loop: for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: changes <- c.CurrentStatus case svc.Stop, svc.Shutdown: ws.logger.Info().Uint32("command", uint32(c.Cmd)).Msg("Received service control command") if ws.eventLog != nil { ws.eventLog.Info(1, "Pulse Host Agent received stop command") } changes <- svc.Status{State: svc.StopPending} cancel() break loop default: ws.logger.Warn().Uint32("command", uint32(c.Cmd)).Msg("Unexpected service control command") } case err := <-errChan: if err != nil { ws.logger.Error().Err(err).Msg("Agent error") if ws.eventLog != nil { ws.eventLog.Error(1, fmt.Sprintf("Pulse Host Agent error: %v", err)) } changes <- svc.Status{State: svc.Stopped} return true, 1 } break loop } } // Wait for agent to stop gracefully (with timeout) shutdownTimeout := time.NewTimer(10 * time.Second) defer shutdownTimeout.Stop() select { case <-errChan: ws.logger.Info().Msg("Agent stopped gracefully") if ws.eventLog != nil { ws.eventLog.Info(1, "Pulse Host Agent stopped gracefully") } case <-shutdownTimeout.C: ws.logger.Warn().Msg("Agent shutdown timeout, forcing stop") if ws.eventLog != nil { ws.eventLog.Warning(1, "Pulse Host Agent shutdown timeout") } } changes <- svc.Status{State: svc.Stopped} return false, 0 } func runAsWindowsService(cfg hostagent.Config, logger zerolog.Logger) error { // Check if we're running as a Windows service isService, err := svc.IsWindowsService() if err != nil { return fmt.Errorf("failed to determine if running as service: %w", err) } if !isService { // Not running as a service, run normally return nil } logger.Info().Msg("Running as Windows service") // Open Windows Event Log (best effort - don't fail if it doesn't work) elog, err := eventlog.Open("PulseHostAgent") if err != nil { logger.Warn().Err(err).Msg("Could not open Windows Event Log, continuing without it") elog = nil } defer func() { if elog != nil { elog.Close() } }() ws := &windowsService{ cfg: cfg, logger: logger, eventLog: elog, } // Run as a Windows service err = svc.Run("PulseHostAgent", ws) if err != nil { if elog != nil { elog.Error(1, fmt.Sprintf("Failed to run service: %v", err)) } return fmt.Errorf("failed to run Windows service: %w", err) } return nil } func runServiceDebug(cfg hostagent.Config, logger zerolog.Logger) error { ws := &windowsService{ cfg: cfg, logger: logger, } return debug.Run("PulseHostAgent", ws) }