add tacking

This commit is contained in:
Fran Jurmanović
2025-05-24 00:44:26 +02:00
parent 31a2b73cf9
commit edf5a2c8c4
11 changed files with 210 additions and 12 deletions

1
go.mod
View File

@@ -30,6 +30,7 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.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/files/v2 v2.0.0 // indirect
github.com/swaggo/swag v1.16.3 // indirect github.com/swaggo/swag v1.16.3 // indirect
github.com/urfave/cli/v2 v2.27.2 // indirect github.com/urfave/cli/v2 v2.27.2 // indirect

2
go.sum
View File

@@ -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/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 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@@ -40,6 +40,13 @@ func Init(di *dig.Container, app *fiber.App) {
if err != nil { if err != nil {
panic("unable to bind routes") panic("unable to bind routes")
} }
err = di.Provide(func() *dig.Container {
return di
})
if err != nil {
panic("unable to bind dig")
}
controller.InitializeControllers(di) controller.InitializeControllers(di)
} }

View File

@@ -59,6 +59,7 @@ func (ac *ConfigController) updateConfig(c *fiber.Ctx) error {
if err != nil { if err != nil {
return c.Status(400).SendString(err.Error()) return c.Status(400).SendString(err.Error())
} }
log.Print("restart", restart)
if restart { if restart {
_, err := ac.apiService.ApiRestartServer(c) _, err := ac.apiService.ApiRestartServer(c)
if err != nil { if err != nil {

View File

@@ -10,7 +10,7 @@ import (
type IntString int type IntString int
// Config tracks configuration modifications // Config tracks configuration modifications
type Config struct { type Config struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
ServerID uint `json:"serverId" gorm:"not null"` ServerID uint `json:"serverId" gorm:"not null"`
ConfigFile string `json:"configFile" gorm:"not null"` // e.g. "settings.json" ConfigFile string `json:"configFile" gorm:"not null"` // e.g. "settings.json"
@@ -129,3 +129,11 @@ func (i *IntString) UnmarshalJSON(b []byte) error {
return fmt.Errorf("invalid postQualySeconds value") return fmt.Errorf("invalid postQualySeconds value")
} }
func (i IntString) ToString() string {
return strconv.Itoa(int(i))
}
func (i IntString) ToInt() (int) {
return int(i)
}

View File

@@ -14,6 +14,7 @@ type Server struct {
Port int `gorm:"not null" json:"-"` Port int `gorm:"not null" json:"-"`
ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/" ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/"
ServiceName string `gorm:"not null" json:"-"` // Windows service name ServiceName string `gorm:"not null" json:"-"` // Windows service name
State ServerState `gorm:"-" json:"state"`
} }
type PlayerState struct { type PlayerState struct {
@@ -30,10 +31,26 @@ type PlayerState struct {
IsConnected bool IsConnected bool
} }
type ServerState struct { type AccServerInstance struct {
sync.RWMutex Model *Server
Session string State *ServerState
PlayerCount int }
Players map[int]*PlayerState
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 `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
// Players map[int]*PlayerState
// etc. // etc.
} }

View File

@@ -4,25 +4,32 @@ import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/common" "acc-server-manager/local/utl/common"
"context"
"errors" "errors"
"strings" "strings"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type ApiService struct { type ApiService struct {
repository *repository.ApiRepository repository *repository.ApiRepository
serverRepository *repository.ServerRepository serverRepository *repository.ServerRepository
serverService *ServerService
} }
func NewApiService(repository *repository.ApiRepository, func NewApiService(repository *repository.ApiRepository,
serverRepository *repository.ServerRepository) *ApiService { serverRepository *repository.ServerRepository,) *ApiService {
return &ApiService{ return &ApiService{
repository: repository, repository: repository,
serverRepository: serverRepository, serverRepository: serverRepository,
} }
} }
func (as ApiService) SetServerService(serverService *ServerService) {
as.serverService = serverService
}
// GetFirst // GetFirst
// Gets first row from API table. // 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) { 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) { 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) { 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) { func ManageService(serviceName string, action string) (string, error) {

View File

@@ -63,6 +63,7 @@ func mustDecode[T any](fileName, path string) (T, error) {
type ConfigService struct { type ConfigService struct {
repository *repository.ConfigRepository repository *repository.ConfigRepository
serverRepository *repository.ServerRepository serverRepository *repository.ServerRepository
serverService *ServerService
} }
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService { 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 // UpdateConfig
// Updates physical config file and caches it in database. // 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 return nil, err
} }
as.serverService.StartAccServerRuntime(server)
// Log change // Log change
return as.repository.UpdateConfig(context, &model.Config{ return as.repository.UpdateConfig(context, &model.Config{
ServerID: uint(serverID), ServerID: uint(serverID),

View File

@@ -3,7 +3,15 @@ package service
import ( import (
"acc-server-manager/local/model" "acc-server-manager/local/model"
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"acc-server-manager/local/utl/tracking"
"context"
"log" "log"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -11,13 +19,90 @@ import (
type ServerService struct { type ServerService struct {
repository *repository.ServerRepository repository *repository.ServerRepository
apiService *ApiService apiService *ApiService
instances sync.Map
configService *ConfigService
} }
func NewServerService(repository *repository.ServerRepository, apiService *ApiService) *ServerService { func NewServerService(repository *repository.ServerRepository, apiService *ApiService, configService *ConfigService) *ServerService {
return &ServerService{ service := &ServerService{
repository: repository, repository: repository,
apiService: apiService, 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 // GetAll
@@ -36,6 +121,15 @@ func (as ServerService) GetAll(ctx *fiber.Ctx) *[]model.Server {
log.Print(err.Error()) log.Print(err.Error())
} }
(*servers)[i].Status = model.ServiceStatus(status) (*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 return servers
@@ -55,7 +149,15 @@ func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) *model.Server {
log.Print(err.Error()) log.Print(err.Error())
} }
server.Status = model.ServiceStatus(status) 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 return server
} }

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"acc-server-manager/local/repository" "acc-server-manager/local/repository"
"log"
"go.uber.org/dig" "go.uber.org/dig"
) )
@@ -18,4 +19,12 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewApiService) c.Provide(NewApiService)
c.Provide(NewConfigService) c.Provide(NewConfigService)
c.Provide(NewLookupService) 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")
}
} }

View File

@@ -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
}
}
}