package agent import ( "context" "log/slog" "os" "os/signal" "syscall" "time" "github.com/hexahost/gamecloud/node-agent/internal/config" "github.com/hexahost/gamecloud/node-agent/internal/health" ) const Version = "0.1.0-phase0" // Agent coordinates the node agent lifecycle until shutdown. type Agent struct { cfg *config.Config log *slog.Logger health *health.Server } // New creates an agent instance from configuration. func New(cfg *config.Config, log *slog.Logger) *Agent { return &Agent{ cfg: cfg, log: log, health: health.NewServer(cfg.HealthListenAddr, Version), } } // Run starts the agent loop and blocks until context cancellation or signal. func (a *Agent) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) defer signal.Stop(sigCh) errCh := make(chan error, 1) go func() { a.log.Info("health server listening", "addr", a.cfg.HealthListenAddr) if err := a.health.ListenAndServe(); err != nil { errCh <- err } }() a.health.SetReady(true) a.log.Info("agent started", "node_id", a.cfg.NodeID, "control_plane", a.cfg.ControlPlaneURL, "version", Version, ) ticker := time.NewTicker(a.cfg.HeartbeatInterval) defer ticker.Stop() for { select { case <-ctx.Done(): return a.shutdown(ctx.Err()) case sig := <-sigCh: a.log.Info("shutdown signal received", "signal", sig.String()) return a.shutdown(nil) case err := <-errCh: return err case <-ticker.C: a.log.Debug("heartbeat tick", "node_id", a.cfg.NodeID, "interval", a.cfg.HeartbeatInterval.String(), ) } } } func (a *Agent) shutdown(reason error) error { a.health.SetReady(false) a.log.Info("agent shutting down", "grace", a.cfg.ShutdownGracePeriod.String()) timer := time.NewTimer(a.cfg.ShutdownGracePeriod) defer timer.Stop() <-timer.C if reason != nil && reason != context.Canceled { a.log.Warn("shutdown with error", "error", reason) return reason } a.log.Info("agent stopped") return nil }