package agent import ( "context" "log/slog" "os" "os/signal" "syscall" "github.com/hexahost/gamecloud/node-agent/internal/config" dockerpkg "github.com/hexahost/gamecloud/node-agent/internal/docker" "github.com/hexahost/gamecloud/node-agent/internal/health" "github.com/hexahost/gamecloud/node-agent/internal/runtime" "github.com/hexahost/gamecloud/node-agent/internal/ws" ) const Version = "0.3.0-phase3" // Agent coordinates the node agent lifecycle until shutdown. type Agent struct { cfg *config.Config log *slog.Logger health *health.Server docker *dockerpkg.Client ws *ws.Client } // New creates an agent instance from configuration. func New(cfg *config.Config, log *slog.Logger) (*Agent, error) { dockerClient, err := dockerpkg.New(cfg.DockerSocket) if err != nil { return nil, err } wsClient := ws.NewClient(cfg, log, nil, Version) runtimeMgr := runtime.NewManager(dockerClient, wsClient, log) wsClient.SetRuntime(runtimeMgr) return &Agent{ cfg: cfg, log: log, health: health.NewServer(cfg.HealthListenAddr, Version), docker: dockerClient, ws: wsClient, }, nil } // 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, 2) go func() { a.log.Info("health server listening", "addr", a.cfg.HealthListenAddr) if err := a.health.ListenAndServe(); err != nil { errCh <- err } }() go func() { errCh <- a.ws.Run(ctx) }() a.health.SetReady(true) a.log.Info("agent started", "node_id", a.cfg.NodeID, "control_plane", a.cfg.ControlPlaneURL, "version", Version, ) for { select { case <-ctx.Done(): return a.shutdown(nil) case sig := <-sigCh: a.log.Info("shutdown signal received", "signal", sig.String()) cancel() case err := <-errCh: if ctx.Err() != nil { return a.shutdown(nil) } if err != nil { return err } } } } func (a *Agent) shutdown(reason error) error { a.health.SetReady(false) a.log.Info("agent shutting down") if a.docker != nil { if err := a.docker.Close(); err != nil { a.log.Warn("docker client close failed", "error", err) } } if reason != nil && reason != context.Canceled { a.log.Warn("shutdown with error", "error", reason) return reason } a.log.Info("agent stopped") return nil }