diff --git a/docker-compose.yml b/docker-compose.yml index aae6f03..4899af6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,11 @@ services: ACC_IMAGE: acc-wine:latest ACC_SERVERS_PATH: /data/servers ACC_SERVERS_HOST_PATH: /opt/acc-servers + # ACC_CPUSET: Pin all ACC containers to specific CPU cores (e.g., "0-11"). + # Reduces simulation timing misses ("not enough CPU power" in accServer.exe). + # Set the CPU governor to 'performance' on the host for best results: + # echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + # ACC_CPUSET: "0-11" env_file: - .env volumes: diff --git a/local/controller/system.go b/local/controller/system.go index 19bc3a4..fbb13dd 100755 --- a/local/controller/system.go +++ b/local/controller/system.go @@ -1,39 +1,70 @@ package controller import ( + "acc-server-manager/local/middleware" + "acc-server-manager/local/model" + "acc-server-manager/local/service" "acc-server-manager/local/utl/common" "acc-server-manager/local/utl/configs" "github.com/gofiber/fiber/v2" + "go.uber.org/dig" ) type SystemController struct { + systemService *service.SystemService } -// NewSystemController -// Initializes SystemController. -// -// Args: -// *services.SystemService: Service control service -// *Fiber.RouterGroup: Fiber Router Group -// Returns: -// *SystemController: Controller for service control interactions -func NewSystemController(routeGroups *common.RouteGroups) *SystemController { - ac := &SystemController{} +type SystemControllerParams struct { + dig.In + RouteGroups *common.RouteGroups + AuthMiddleware *middleware.AuthMiddleware + SystemService *service.SystemService `optional:"true"` +} - apiGroup := routeGroups.System - apiGroup.Get("/health", ac.getFirst) +// NewSystemController initializes SystemController. +func NewSystemController(p SystemControllerParams) *SystemController { + ac := &SystemController{systemService: p.SystemService} + + apiGroup := p.RouteGroups.System + apiGroup.Get("/health", ac.getHealth) + + if p.SystemService != nil { + apiGroup.Post("/migrate-image", p.AuthMiddleware.Authenticate, ac.migrateImage) + } return ac } -// getFirst returns service control status +// getHealth returns the current version string. // -// @Summary Return service control status -// @Description Return service control status +// @Summary Return service health / version +// @Description Return service health / version // @Tags system -// @Success 200 {array} string +// @Success 200 {string} string // @Router /system/health [get] -func (ac *SystemController) getFirst(c *fiber.Ctx) error { +func (ac *SystemController) getHealth(c *fiber.Ctx) error { return c.SendString(configs.Version) } + +// migrateImage godoc +// +// @Summary Migrate all Docker server containers to current ACC image +// @Description Pulls the current ACC_IMAGE, removes old containers, optionally stops running servers +// @Tags system +// @Accept json +// @Produce json +// @Param body body model.MigrateImageRequest true "Migration options" +// @Success 200 {object} model.MigrationResult +// @Router /system/migrate-image [post] +func (ac *SystemController) migrateImage(c *fiber.Ctx) error { + var req model.MigrateImageRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request"}) + } + result, err := ac.systemService.MigrateImage(c.Context(), req.StopRunning) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(result) +} diff --git a/local/model/system.go b/local/model/system.go new file mode 100644 index 0000000..e8b3839 --- /dev/null +++ b/local/model/system.go @@ -0,0 +1,24 @@ +package model + +type MigrateImageRequest struct { + StopRunning bool `json:"stopRunning"` +} + +type ServerMigrationEntry struct { + ID string `json:"id"` + Name string `json:"name"` + WasRunning bool `json:"wasRunning"` +} + +type ServerMigrationError struct { + ID string `json:"id"` + Name string `json:"name"` + Error string `json:"error"` +} + +type MigrationResult struct { + Image string `json:"image"` + Migrated []ServerMigrationEntry `json:"migrated"` + Skipped []ServerMigrationEntry `json:"skipped"` + Failed []ServerMigrationError `json:"failed"` +} diff --git a/local/service/docker_runtime.go b/local/service/docker_runtime.go index 47692d6..5bbb6ba 100644 --- a/local/service/docker_runtime.go +++ b/local/service/docker_runtime.go @@ -62,19 +62,24 @@ func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error fmt.Sprintf("%s:/acc/game:rw", hostServerPath), } + hostConfig := &container.HostConfig{ + Binds: binds, + PortBindings: portBindings, + RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + // NET_RAW is required for tcpdump packet capture in debug mode (DEBUG_MODE=1). + CapAdd: []string{"NET_RAW"}, + } + if cpuset := env.GetACCCpuset(); cpuset != "" { + hostConfig.CpusetCpus = cpuset + } + resp, err := r.client.ContainerCreate(ctx, &container.Config{ Image: image, ExposedPorts: exposedPorts, Env: []string{ fmt.Sprintf("SERVER_PORT=%d", server.Port), }, - }, &container.HostConfig{ - Binds: binds, - PortBindings: portBindings, - RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, - // NET_RAW is required for tcpdump packet capture in debug mode (DEBUG_MODE=1). - CapAdd: []string{"NET_RAW"}, - }, nil, nil, server.ServiceName) + }, hostConfig, nil, nil, server.ServiceName) if err != nil { return fmt.Errorf("failed to create container %s: %v", server.ServiceName, err) } diff --git a/local/service/service.go b/local/service/service.go index 43271b0..d0c3c20 100755 --- a/local/service/service.go +++ b/local/service/service.go @@ -100,4 +100,7 @@ func registerDockerProviders(c *dig.Container) { c.Provide(func(client *dockerclient.Client) platform.LogStreamer { return NewDockerLogStreamer(client) }) + + c.Provide(NewSystemService) + } diff --git a/local/service/system_service.go b/local/service/system_service.go new file mode 100644 index 0000000..8dd4368 --- /dev/null +++ b/local/service/system_service.go @@ -0,0 +1,115 @@ +package service + +import ( + "acc-server-manager/local/model" + "acc-server-manager/local/platform" + "acc-server-manager/local/repository" + "acc-server-manager/local/utl/env" + "acc-server-manager/local/utl/logging" + "context" + "fmt" + "io" + + "github.com/docker/docker/api/types/container" + dockerimage "github.com/docker/docker/api/types/image" + dockerclient "github.com/docker/docker/client" +) + +type SystemService struct { + dockerClient *dockerclient.Client + repo *repository.ServerRepository + runtime platform.ServerRuntime + serverService *ServerService +} + +func NewSystemService( + dockerClient *dockerclient.Client, + repo *repository.ServerRepository, + runtime platform.ServerRuntime, + serverService *ServerService, +) *SystemService { + return &SystemService{ + dockerClient: dockerClient, + repo: repo, + runtime: runtime, + serverService: serverService, + } +} + +func (s *SystemService) MigrateImage(ctx context.Context, stopRunning bool) (*model.MigrationResult, error) { + image := env.GetACCImage() + + reader, err := s.dockerClient.ImagePull(ctx, image, dockerimage.PullOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to pull image %s: %v", image, err) + } + if _, err := io.Copy(io.Discard, reader); err != nil { + logging.Warn("Error draining image pull response for %s: %v", image, err) + } + reader.Close() + logging.Info("Pulled image %s", image) + + servers, err := s.repo.GetAll(ctx, &model.ServerFilter{}) + if err != nil { + return nil, fmt.Errorf("failed to list servers: %v", err) + } + + result := &model.MigrationResult{Image: image} + + for _, server := range *servers { + statusStr, _ := s.runtime.Status(ctx, server.ID) + isRunning := statusStr == model.StatusRunning.String() + + if isRunning && !stopRunning { + result.Skipped = append(result.Skipped, model.ServerMigrationEntry{ + ID: server.ID.String(), + Name: server.Name, + WasRunning: true, + }) + continue + } + + entry := model.ServerMigrationEntry{ + ID: server.ID.String(), + Name: server.Name, + WasRunning: isRunning, + } + + if isRunning { + if _, err := s.runtime.Stop(ctx, server.ID); err != nil { + result.Failed = append(result.Failed, model.ServerMigrationError{ + ID: server.ID.String(), + Name: server.Name, + Error: fmt.Sprintf("stop failed: %v", err), + }) + continue + } + } + + if server.ContainerID != "" { + if err := s.dockerClient.ContainerRemove(ctx, server.ContainerID, container.RemoveOptions{Force: true}); err != nil { + logging.Warn("Failed to remove container %s for server %s: %v", server.ContainerID, server.ID, err) + } + } + server.ContainerID = "" + if err := s.repo.Update(ctx, &server); err != nil { + logging.Warn("Failed to clear ContainerID for server %s: %v", server.ID, err) + } + + if isRunning { + if _, err := s.runtime.Start(ctx, server.ID); err != nil { + result.Failed = append(result.Failed, model.ServerMigrationError{ + ID: server.ID.String(), + Name: server.Name, + Error: fmt.Sprintf("restart failed: %v", err), + }) + continue + } + s.serverService.StartAccServerRuntime(&server) + } + + result.Migrated = append(result.Migrated, entry) + } + + return result, nil +} diff --git a/local/utl/env/env.go b/local/utl/env/env.go index a176d11..f59bcb3 100755 --- a/local/utl/env/env.go +++ b/local/utl/env/env.go @@ -65,6 +65,10 @@ func GetACCServersPath() string { return DefaultACCServersPath } +func GetACCCpuset() string { + return os.Getenv("ACC_CPUSET") +} + func GetACCServersHostPath() string { if p := os.Getenv("ACC_SERVERS_HOST_PATH"); p != "" { return p