//go:build windows package main import ( "context" "fmt" "time" "github.com/rcourtman/pulse-go-rewrite/internal/agentupdate" "github.com/rcourtman/pulse-go-rewrite/internal/hostagent" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" ) type windowsService struct { cfg 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") } hostCfg := ws.cfg.HostConfig hostCfg.Logger = &ws.logger ctx, cancel := context.WithCancel(context.Background()) defer cancel() g, ctx := errgroup.WithContext(ctx) // Start Auto-Updater updater := agentupdate.New(agentupdate.Config{ PulseURL: hostCfg.PulseURL, APIToken: hostCfg.APIToken, AgentName: "pulse-host-agent", CurrentVersion: Version, CheckInterval: 1 * time.Hour, InsecureSkipVerify: hostCfg.InsecureSkipVerify, Logger: &ws.logger, Disabled: ws.cfg.DisableAutoUpdate, }) g.Go(func() error { updater.RunLoop(ctx) return nil }) // Start the host agent agent, err := hostagent.New(hostCfg) if err != nil { ws.logger.Error().Err(err).Msg("Failed to create host agent") if ws.eventLog != nil { ws.eventLog.Error(1, fmt.Sprintf("Failed to create host agent: %v", err)) } changes <- svc.Status{State: svc.Stopped} return true, 1 } g.Go(func() error { ws.logger.Info(). Str("version", Version). Str("pulse_url", hostCfg.PulseURL). Str("agent_id", hostCfg.AgentID). Dur("interval", hostCfg.Interval). Bool("auto_update", !ws.cfg.DisableAutoUpdate). Msg("Starting Pulse host agent as Windows service") return agent.Run(ctx) }) // Channel to receive errgroup completion doneChan := make(chan error, 1) go func() { doneChan <- g.Wait() }() 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)", hostCfg.PulseURL, hostCfg.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 := <-doneChan: if err != nil && err != context.Canceled { 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 <-doneChan: 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 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 }