From edf5a2c8c4b41156a12b9ef0ead183ef6590f623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sat, 24 May 2025 00:44:26 +0200 Subject: [PATCH] add tacking --- go.mod | 1 + go.sum | 2 + local/api/api.go | 7 +++ local/controller/config.go | 1 + local/model/config.go | 10 ++- local/model/server.go | 23 ++++++- local/service/api.go | 28 +++++++-- local/service/config.go | 7 +++ local/service/server.go | 110 +++++++++++++++++++++++++++++++-- local/service/service.go | 9 +++ local/utl/tracking/tracking.go | 24 +++++++ 11 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 local/utl/tracking/tracking.go diff --git a/go.mod b/go.mod index 6d0593e..e8d6739 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/skillian/getfiletime v0.0.0-20170819221534-37b64ac4de15 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/swag v1.16.3 // indirect github.com/urfave/cli/v2 v2.27.2 // indirect diff --git a/go.sum b/go.sum index c5772e9..f97fe55 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/skillian/getfiletime v0.0.0-20170819221534-37b64ac4de15 h1:szp/LEXP+cCpLNKJ1NgC3Vnloo4TYmHv8lrzxng+cXI= +github.com/skillian/getfiletime v0.0.0-20170819221534-37b64ac4de15/go.mod h1:QHVxyK6RvbImkJZCSS48T72l5JVj/rA0FerqWGSSvlQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/local/api/api.go b/local/api/api.go index 0424bd4..5c33980 100644 --- a/local/api/api.go +++ b/local/api/api.go @@ -40,6 +40,13 @@ func Init(di *dig.Container, app *fiber.App) { if err != nil { panic("unable to bind routes") } + err = di.Provide(func() *dig.Container { + return di + }) + if err != nil { + panic("unable to bind dig") + } controller.InitializeControllers(di) } + diff --git a/local/controller/config.go b/local/controller/config.go index 618a0dd..78e920c 100644 --- a/local/controller/config.go +++ b/local/controller/config.go @@ -59,6 +59,7 @@ func (ac *ConfigController) updateConfig(c *fiber.Ctx) error { if err != nil { return c.Status(400).SendString(err.Error()) } + log.Print("restart", restart) if restart { _, err := ac.apiService.ApiRestartServer(c) if err != nil { diff --git a/local/model/config.go b/local/model/config.go index 46e3abc..72753db 100644 --- a/local/model/config.go +++ b/local/model/config.go @@ -10,7 +10,7 @@ import ( type IntString int // Config tracks configuration modifications -type Config struct { +type Config struct { ID uint `json:"id" gorm:"primaryKey"` ServerID uint `json:"serverId" gorm:"not null"` ConfigFile string `json:"configFile" gorm:"not null"` // e.g. "settings.json" @@ -128,4 +128,12 @@ func (i *IntString) UnmarshalJSON(b []byte) error { } return fmt.Errorf("invalid postQualySeconds value") +} + +func (i IntString) ToString() string { + return strconv.Itoa(int(i)) +} + +func (i IntString) ToInt() (int) { + return int(i) } \ No newline at end of file diff --git a/local/model/server.go b/local/model/server.go index 96c51ee..31153e6 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -14,6 +14,7 @@ type Server struct { Port int `gorm:"not null" json:"-"` ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/" ServiceName string `gorm:"not null" json:"-"` // Windows service name + State ServerState `gorm:"-" json:"state"` } type PlayerState struct { @@ -30,10 +31,26 @@ type PlayerState struct { IsConnected bool } +type AccServerInstance struct { + Model *Server + State *ServerState +} + +type State struct { + Session string `json:"session"` + SessionStart time.Time `json:"sessionStart"` + PlayerCount int `json:"playerCount"` + // Players map[int]*PlayerState + // etc. +} + type ServerState struct { sync.RWMutex - Session string - PlayerCount int - Players map[int]*PlayerState + Session string `json:"session"` + SessionStart time.Time `json:"sessionStart"` + PlayerCount int `json:"playerCount"` + Track string `json:"track"` + MaxConnections int `json:"maxConnections"` + // Players map[int]*PlayerState // etc. } \ No newline at end of file diff --git a/local/service/api.go b/local/service/api.go index d85a122..16da91a 100644 --- a/local/service/api.go +++ b/local/service/api.go @@ -4,25 +4,32 @@ import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/common" + "context" "errors" "strings" "github.com/gofiber/fiber/v2" ) + type ApiService struct { repository *repository.ApiRepository serverRepository *repository.ServerRepository + serverService *ServerService } func NewApiService(repository *repository.ApiRepository, - serverRepository *repository.ServerRepository) *ApiService { + serverRepository *repository.ServerRepository,) *ApiService { return &ApiService{ repository: repository, serverRepository: serverRepository, } } +func (as ApiService) SetServerService(serverService *ServerService) { + as.serverService = serverService +} + // GetFirst // Gets first row from API table. // @@ -73,15 +80,28 @@ func (as ApiService) StatusServer(serviceName string) (string, error) { } func (as ApiService) StartServer(serviceName string) (string, error) { - return ManageService(serviceName, "start") + status, err := ManageService(serviceName, "start") + + server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + as.serverService.StartAccServerRuntime(server) + return status, err } func (as ApiService) StopServer(serviceName string) (string, error) { - return ManageService(serviceName, "stop") + status, err := ManageService(serviceName, "stop") + + server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + as.serverService.instances.Delete(server.ID) + + return status, err } func (as ApiService) RestartServer(serviceName string) (string, error) { - return ManageService(serviceName, "restart") + status, err := ManageService(serviceName, "restart") + + server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + as.serverService.StartAccServerRuntime(server) + return status, err } func ManageService(serviceName string, action string) (string, error) { diff --git a/local/service/config.go b/local/service/config.go index f9e92d7..613811c 100644 --- a/local/service/config.go +++ b/local/service/config.go @@ -63,6 +63,7 @@ func mustDecode[T any](fileName, path string) (T, error) { type ConfigService struct { repository *repository.ConfigRepository serverRepository *repository.ServerRepository + serverService *ServerService } func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService { @@ -72,6 +73,10 @@ func NewConfigService(repository *repository.ConfigRepository, serverRepository } } +func (as ConfigService) SetServerService(serverService *ServerService) { + as.serverService = serverService +} + // UpdateConfig // Updates physical config file and caches it in database. // @@ -131,6 +136,8 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{ return nil, err } + as.serverService.StartAccServerRuntime(server) + // Log change return as.repository.UpdateConfig(context, &model.Config{ ServerID: uint(serverID), diff --git a/local/service/server.go b/local/service/server.go index 650705c..8b1d497 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -3,7 +3,15 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" + "acc-server-manager/local/utl/tracking" + "context" "log" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" "github.com/gofiber/fiber/v2" ) @@ -11,13 +19,90 @@ import ( type ServerService struct { repository *repository.ServerRepository apiService *ApiService + instances sync.Map + configService *ConfigService } -func NewServerService(repository *repository.ServerRepository, apiService *ApiService) *ServerService { - return &ServerService{ +func NewServerService(repository *repository.ServerRepository, apiService *ApiService, configService *ConfigService) *ServerService { + service := &ServerService{ repository: repository, apiService: apiService, + configService: configService, } + servers := repository.GetAll(context.Background()) + for _, server := range *servers { + status, err := service.apiService.StatusServer(server.ServiceName) + if err != nil { + log.Print(err.Error()) + } + if (status == string(model.StatusRunning)) { + service.StartAccServerRuntime(&server) + } + } + return service +} + +var leaderboardUpdateRegex = regexp.MustCompile(`Updated leaderboard for (\d+) clients`) +var sessionChangeRegex = regexp.MustCompile(`Session changed: (\w+) -> (\w+)`) + +func handleLogLine(instance *model.AccServerInstance) func(string) { + return func (line string) { + state := (*instance).State + now := time.Now() + + if strings.Contains(line, "client(s) online") { + parts := strings.Fields(line) + if len(parts) >= 2 { + countStr := parts[1] + if count, err := strconv.Atoi(countStr); err == nil { + state.Lock() + state.PlayerCount = count + state.Unlock() + } + } + } + + if strings.Contains(line, "Session changed") { + match := sessionChangeRegex.FindStringSubmatch(line) + if len(match) == 3 { + newSession := match[2] + + state.Lock() + state.Session = newSession + state.SessionStart = now + state.Unlock() + } + } + + if strings.Contains(line, "Updated leaderboard for") { + match := leaderboardUpdateRegex.FindStringSubmatch(line) + if len(match) == 2 { + if count, err := strconv.Atoi(match[1]); err == nil { + state.Lock() + state.PlayerCount = count + state.Unlock() + } + } + } + } +} + +func (s *ServerService) StartAccServerRuntime(server *model.Server) { + s.instances.Delete(server.ID) + instance := &model.AccServerInstance{ + Model: server, + State: &model.ServerState{PlayerCount: 0}, + } + config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath) + cfg := config.(model.Configuration) + event, _ := DecodeFileName(EventJson)(server.ConfigPath) + evt := event.(model.EventConfig) + + instance.State.MaxConnections = cfg.MaxConnections.ToInt() + instance.State.Track = evt.Track + + go tracking.TailLogFile(filepath.Join(server.ConfigPath, "\\server\\log\\server.log"), handleLogLine(instance)) + s.instances.Store(server.ID, instance) } // GetAll @@ -36,6 +121,15 @@ func (as ServerService) GetAll(ctx *fiber.Ctx) *[]model.Server { log.Print(err.Error()) } (*servers)[i].Status = model.ServiceStatus(status) + instance, ok := as.instances.Load(server.ID) + if !ok { + log.Print("Unable to retrieve instance for server of ID: ", server.ID) + } else { + serverInstance := instance.(*model.AccServerInstance) + if (serverInstance.State != nil) { + (*servers)[i].State = *serverInstance.State + } + } } return servers @@ -55,7 +149,15 @@ func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) *model.Server { log.Print(err.Error()) } server.Status = model.ServiceStatus(status) + instance, ok := as.instances.Load(server.ID) + if !ok { + log.Print("Unable to retrieve instance for server of ID: ", server.ID) + } else { + serverInstance := instance.(*model.AccServerInstance) + if (serverInstance.State != nil) { + server.State = *serverInstance.State + } + } return server -} - +} \ No newline at end of file diff --git a/local/service/service.go b/local/service/service.go index 42e3dc6..241337e 100644 --- a/local/service/service.go +++ b/local/service/service.go @@ -2,6 +2,7 @@ package service import ( "acc-server-manager/local/repository" + "log" "go.uber.org/dig" ) @@ -18,4 +19,12 @@ func InitializeServices(c *dig.Container) { c.Provide(NewApiService) c.Provide(NewConfigService) c.Provide(NewLookupService) + + err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService) { + api.SetServerService(server) + config.SetServerService(server) + }) + if err != nil { + log.Panic("unable to initialize server service in api service") + } } diff --git a/local/utl/tracking/tracking.go b/local/utl/tracking/tracking.go new file mode 100644 index 0000000..77cee88 --- /dev/null +++ b/local/utl/tracking/tracking.go @@ -0,0 +1,24 @@ +package tracking + +import ( + "bufio" + "os" + "time" +) + +func TailLogFile(path string, callback func(string)) { + file, _ := os.Open(path) + defer file.Close() + + file.Seek(0, os.SEEK_END) // Start at end of file + reader := bufio.NewReader(file) + + for { + line, err := reader.ReadString('\n') + if err == nil { + callback(line) + } else { + time.Sleep(500 * time.Millisecond) // wait for new data + } + } +} \ No newline at end of file