update caching and server creation

This commit is contained in:
Fran Jurmanović
2025-06-01 19:48:39 +02:00
parent 8a3b11b1ef
commit d57013bb50
26 changed files with 888 additions and 249 deletions

View File

@@ -19,7 +19,8 @@ type ApiService struct {
}
func NewApiService(repository *repository.ApiRepository,
serverRepository *repository.ServerRepository) *ApiService {
serverRepository *repository.ServerRepository,
systemConfigService *SystemConfigService) *ApiService {
return &ApiService{
repository: repository,
serverRepository: serverRepository,
@@ -28,7 +29,7 @@ func NewApiService(repository *repository.ApiRepository,
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
DefaultStatus: model.StatusRunning, // Default to running if throttled
}),
windowsService: NewWindowsService(),
windowsService: NewWindowsService(systemConfigService),
}
}
@@ -120,11 +121,11 @@ func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) {
}
func (as *ApiService) StatusServer(serviceName string) (string, error) {
return as.windowsService.Status(serviceName)
return as.windowsService.Status(context.Background(), serviceName)
}
func (as *ApiService) StartServer(serviceName string) (string, error) {
status, err := as.windowsService.Start(serviceName)
status, err := as.windowsService.Start(context.Background(), serviceName)
if err != nil {
return "", err
}
@@ -138,7 +139,7 @@ func (as *ApiService) StartServer(serviceName string) (string, error) {
}
func (as *ApiService) StopServer(serviceName string) (string, error) {
status, err := as.windowsService.Stop(serviceName)
status, err := as.windowsService.Stop(context.Background(), serviceName)
if err != nil {
return "", err
}
@@ -153,7 +154,7 @@ func (as *ApiService) StopServer(serviceName string) (string, error) {
}
func (as *ApiService) RestartServer(serviceName string) (string, error) {
status, err := as.windowsService.Restart(serviceName)
status, err := as.windowsService.Restart(context.Background(), serviceName)
if err != nil {
return "", err
}

View File

@@ -6,6 +6,7 @@ import (
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/logging"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -70,6 +71,7 @@ type ConfigService struct {
}
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService {
logging.Debug("Initializing ConfigService with 5m expiration and 1s throttle")
return &ConfigService{
repository: repository,
serverRepository: serverRepository,
@@ -97,14 +99,19 @@ func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface
configFile := ctx.Params("file")
override := ctx.QueryBool("override", false)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
return as.updateConfigInternal(ctx.UserContext(), serverID, configFile, body, override)
}
// updateConfigInternal handles the actual config update logic without Fiber dependencies
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID int, configFile string, body *map[string]interface{}, override bool) (*model.Config, error) {
server, err := as.serverRepository.GetByID(ctx, serverID)
if err != nil {
logging.Error("Server not found")
return nil, fiber.NewError(404, "Server not found")
return nil, fmt.Errorf("server not found")
}
// Read existing config
configPath := filepath.Join(server.ConfigPath, "\\server\\cfg", configFile)
configPath := filepath.Join(server.GetConfigPath(), configFile)
oldData, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
@@ -150,8 +157,6 @@ func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface
return nil, err
}
context := ctx.UserContext()
if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil {
return nil, err
}
@@ -162,7 +167,7 @@ func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface
as.serverService.StartAccServerRuntime(server)
// Log change
return as.repository.UpdateConfig(context, &model.Config{
return as.repository.UpdateConfig(ctx, &model.Config{
ServerID: uint(serverID),
ConfigFile: configFile,
OldConfig: string(oldDataUTF8),
@@ -183,6 +188,8 @@ func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
configFile := ctx.Params("file")
serverIDStr := strconv.Itoa(serverID)
logging.Debug("Getting config for server ID: %d, file: %s", serverID, configFile)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil {
logging.Error("Server not found")
@@ -193,56 +200,77 @@ func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
switch configFile {
case ConfigurationJson:
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
logging.Debug("Returning cached configuration for server ID: %s", serverIDStr)
return cached, nil
}
case AssistRulesJson:
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
logging.Debug("Returning cached assist rules for server ID: %s", serverIDStr)
return cached, nil
}
case EventJson:
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
logging.Debug("Returning cached event config for server ID: %s", serverIDStr)
return cached, nil
}
case EventRulesJson:
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
logging.Debug("Returning cached event rules for server ID: %s", serverIDStr)
return cached, nil
}
case SettingsJson:
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
logging.Debug("Returning cached settings for server ID: %s", serverIDStr)
return cached, nil
}
}
decoded, err := DecodeFileName(configFile)(server.ConfigPath)
logging.Debug("Cache miss for server ID: %s, file: %s - loading from disk", serverIDStr, configFile)
// Not in cache, load from disk
configPath := filepath.Join(server.GetConfigPath(), configFile)
decoder := DecodeFileName(configFile)
if decoder == nil {
return nil, errors.New("invalid config file")
}
config, err := decoder(configPath)
if err != nil {
if os.IsNotExist(err) {
logging.Debug("Config file not found, creating default for server ID: %s, file: %s", serverIDStr, configFile)
// Return empty config if file doesn't exist
switch configFile {
case ConfigurationJson:
return &model.Configuration{}, nil
case AssistRulesJson:
return &model.AssistRules{}, nil
case EventJson:
return &model.EventConfig{}, nil
case EventRulesJson:
return &model.EventRules{}, nil
case SettingsJson:
return &model.ServerSettings{}, nil
}
}
return nil, err
}
// Cache the result based on config file type
// Cache the loaded config
switch configFile {
case ConfigurationJson:
if config, ok := decoded.(model.Configuration); ok {
as.configCache.UpdateConfiguration(serverIDStr, config)
}
as.configCache.UpdateConfiguration(serverIDStr, *config.(*model.Configuration))
case AssistRulesJson:
if rules, ok := decoded.(model.AssistRules); ok {
as.configCache.UpdateAssistRules(serverIDStr, rules)
}
as.configCache.UpdateAssistRules(serverIDStr, *config.(*model.AssistRules))
case EventJson:
if event, ok := decoded.(model.EventConfig); ok {
as.configCache.UpdateEvent(serverIDStr, event)
}
as.configCache.UpdateEvent(serverIDStr, *config.(*model.EventConfig))
case EventRulesJson:
if rules, ok := decoded.(model.EventRules); ok {
as.configCache.UpdateEventRules(serverIDStr, rules)
}
as.configCache.UpdateEventRules(serverIDStr, *config.(*model.EventRules))
case SettingsJson:
if settings, ok := decoded.(model.ServerSettings); ok {
as.configCache.UpdateSettings(serverIDStr, settings)
}
as.configCache.UpdateSettings(serverIDStr, *config.(*model.ServerSettings))
}
return decoded, nil
logging.Debug("Successfully loaded and cached config for server ID: %s, file: %s", serverIDStr, configFile)
return config, nil
}
// GetConfigs
@@ -261,7 +289,7 @@ func (as *ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, erro
func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configurations, error) {
serverIDStr := strconv.Itoa(int(server.ID))
logging.Info("Loading configs for server ID: %s at path: %s", serverIDStr, server.ConfigPath)
logging.Info("Loading configs for server ID: %s at path: %s", serverIDStr, server.GetConfigPath())
configs := &model.Configurations{}
// Load configuration
@@ -270,7 +298,7 @@ func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configuration
configs.Configuration = *cached
} else {
logging.Debug("Loading configuration from disk for server %s", serverIDStr)
config, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
config, err := mustDecode[model.Configuration](ConfigurationJson, server.GetConfigPath())
if err != nil {
logging.Error("Failed to load configuration for server %s: %v", serverIDStr, err)
return nil, fmt.Errorf("failed to load configuration: %v", err)
@@ -285,7 +313,7 @@ func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configuration
configs.AssistRules = *cached
} else {
logging.Debug("Loading assist rules from disk for server %s", serverIDStr)
rules, err := mustDecode[model.AssistRules](AssistRulesJson, server.ConfigPath)
rules, err := mustDecode[model.AssistRules](AssistRulesJson, server.GetConfigPath())
if err != nil {
logging.Error("Failed to load assist rules for server %s: %v", serverIDStr, err)
return nil, fmt.Errorf("failed to load assist rules: %v", err)
@@ -300,7 +328,7 @@ func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configuration
configs.Event = *cached
} else {
logging.Debug("Loading event config from disk for server %s", serverIDStr)
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
event, err := mustDecode[model.EventConfig](EventJson, server.GetConfigPath())
if err != nil {
logging.Error("Failed to load event config for server %s: %v", serverIDStr, err)
return nil, fmt.Errorf("failed to load event config: %v", err)
@@ -316,7 +344,7 @@ func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configuration
configs.EventRules = *cached
} else {
logging.Debug("Loading event rules from disk for server %s", serverIDStr)
rules, err := mustDecode[model.EventRules](EventRulesJson, server.ConfigPath)
rules, err := mustDecode[model.EventRules](EventRulesJson, server.GetConfigPath())
if err != nil {
logging.Error("Failed to load event rules for server %s: %v", serverIDStr, err)
return nil, fmt.Errorf("failed to load event rules: %v", err)
@@ -331,7 +359,7 @@ func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configuration
configs.Settings = *cached
} else {
logging.Debug("Loading settings from disk for server %s", serverIDStr)
settings, err := mustDecode[model.ServerSettings](SettingsJson, server.ConfigPath)
settings, err := mustDecode[model.ServerSettings](SettingsJson, server.GetConfigPath())
if err != nil {
logging.Error("Failed to load settings for server %s: %v", serverIDStr, err)
return nil, fmt.Errorf("failed to load settings: %v", err)
@@ -359,7 +387,7 @@ func readAndDecode[T interface{}](path string, configFile string) (T, error) {
}
func readFile(path string, configFile string) ([]byte, error) {
configPath := filepath.Join(path, "server", "cfg", configFile)
configPath := filepath.Join(path, configFile)
oldData, err := os.ReadFile(configPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
@@ -419,7 +447,7 @@ func (as *ConfigService) GetEventConfig(server *model.Server) (*model.EventConfi
return cached, nil
}
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
event, err := mustDecode[model.EventConfig](EventJson, server.GetConfigPath())
if err != nil {
return nil, err
}
@@ -433,10 +461,27 @@ func (as *ConfigService) GetConfiguration(server *model.Server) (*model.Configur
return cached, nil
}
config, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
config, err := mustDecode[model.Configuration](ConfigurationJson, server.GetConfigPath())
if err != nil {
return nil, err
}
as.configCache.UpdateConfiguration(serverIDStr, config)
return &config, nil
}
// SaveConfiguration saves the configuration for a server
func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.Configuration) error {
// Convert config to map for UpdateConfig
configMap := make(map[string]interface{})
configBytes, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal configuration: %v", err)
}
if err := json.Unmarshal(configBytes, &configMap); err != nil {
return fmt.Errorf("failed to unmarshal configuration: %v", err)
}
// Update the configuration using the internal method
_, err = as.updateConfigInternal(context.Background(), int(server.ID), ConfigurationJson, &configMap, true)
return err
}

View File

@@ -21,16 +21,16 @@ func NewFirewallService() *FirewallService {
func (s *FirewallService) CreateServerRules(serverName string, tcpPorts, udpPorts []int) error {
for _, port := range tcpPorts {
ruleName := fmt.Sprintf("%s-TCP-%d", serverName, port)
ruleName := fmt.Sprintf("\"%s-TCP-%d\"", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("add").
Add("rule").
AddPair("name", ruleName).
AddPair("dir", "in").
AddPair("action", "allow").
AddPair("protocol", "TCP").
AddFlag("name", ruleName).
AddFlag("dir", "in").
AddFlag("action", "allow").
AddFlag("protocol", "TCP").
AddFlag("localport", port)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
@@ -46,10 +46,10 @@ func (s *FirewallService) CreateServerRules(serverName string, tcpPorts, udpPort
Add("firewall").
Add("add").
Add("rule").
AddPair("name", ruleName).
AddPair("dir", "in").
AddPair("action", "allow").
AddPair("protocol", "UDP").
AddFlag("name", ruleName).
AddFlag("dir", "in").
AddFlag("action", "allow").
AddFlag("protocol", "UDP").
AddFlag("localport", port)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
@@ -63,13 +63,13 @@ func (s *FirewallService) CreateServerRules(serverName string, tcpPorts, udpPort
func (s *FirewallService) DeleteServerRules(serverName string, tcpPorts, udpPorts []int) error {
for _, port := range tcpPorts {
ruleName := fmt.Sprintf("%s-TCP-%d", serverName, port)
ruleName := fmt.Sprintf("\"%s-TCP-%d\"", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("delete").
Add("rule").
AddPair("name", ruleName)
AddFlag("name", ruleName)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to delete TCP firewall rule for port %d: %v", port, err)
@@ -78,13 +78,13 @@ func (s *FirewallService) DeleteServerRules(serverName string, tcpPorts, udpPort
}
for _, port := range udpPorts {
ruleName := fmt.Sprintf("%s-UDP-%d", serverName, port)
ruleName := fmt.Sprintf("\"%s-UDP-%d\"", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("delete").
Add("rule").
AddPair("name", ruleName)
AddFlag("name", ruleName)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to delete UDP firewall rule for port %d: %v", port, err)

View File

@@ -3,6 +3,7 @@ package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging"
"github.com/gofiber/fiber/v2"
)
@@ -12,10 +13,11 @@ type LookupService struct {
cache *model.LookupCache
}
func NewLookupService(repository *repository.LookupRepository) *LookupService {
func NewLookupService(repository *repository.LookupRepository, cache *model.LookupCache) *LookupService {
logging.Debug("Initializing LookupService")
return &LookupService{
repository: repository,
cache: model.NewLookupCache(),
cache: cache,
}
}
@@ -24,6 +26,7 @@ func (s *LookupService) GetTracks(ctx *fiber.Ctx) (interface{}, error) {
return cached, nil
}
logging.Debug("Loading tracks from database")
tracks := s.repository.GetTracks(ctx.UserContext())
s.cache.Set("tracks", tracks)
return tracks, nil
@@ -34,6 +37,7 @@ func (s *LookupService) GetCarModels(ctx *fiber.Ctx) (interface{}, error) {
return cached, nil
}
logging.Debug("Loading car models from database")
cars := s.repository.GetCarModels(ctx.UserContext())
s.cache.Set("cars", cars)
return cars, nil
@@ -44,6 +48,7 @@ func (s *LookupService) GetDriverCategories(ctx *fiber.Ctx) (interface{}, error)
return cached, nil
}
logging.Debug("Loading driver categories from database")
categories := s.repository.GetDriverCategories(ctx.UserContext())
s.cache.Set("drivers", categories)
return categories, nil
@@ -54,6 +59,7 @@ func (s *LookupService) GetCupCategories(ctx *fiber.Ctx) (interface{}, error) {
return cached, nil
}
logging.Debug("Loading cup categories from database")
categories := s.repository.GetCupCategories(ctx.UserContext())
s.cache.Set("cups", categories)
return categories, nil
@@ -64,6 +70,7 @@ func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) {
return cached, nil
}
logging.Debug("Loading session types from database")
types := s.repository.GetSessionTypes(ctx.UserContext())
s.cache.Set("sessions", types)
return types, nil
@@ -71,14 +78,17 @@ func (s *LookupService) GetSessionTypes(ctx *fiber.Ctx) (interface{}, error) {
// ClearCache clears all cached lookup data
func (s *LookupService) ClearCache() {
logging.Debug("Clearing all lookup cache data")
s.cache.Clear()
}
// PreloadCache loads all lookup data into cache
func (s *LookupService) PreloadCache(ctx *fiber.Ctx) {
logging.Debug("Preloading all lookup cache data")
s.GetTracks(ctx)
s.GetCarModels(ctx)
s.GetDriverCategories(ctx)
s.GetCupCategories(ctx)
s.GetSessionTypes(ctx)
logging.Debug("Completed preloading lookup cache data")
}

View File

@@ -12,22 +12,30 @@ import (
"sync"
"time"
"acc-server-manager/local/utl/network"
"github.com/gofiber/fiber/v2"
)
const (
DefaultStartPort = 9600
RequiredPortCount = 1 // Update this if ACC needs more ports
)
type ServerService struct {
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
instances sync.Map // Track instances per server
configService *ConfigService
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
systemConfigService *SystemConfigService
instances sync.Map // Track instances per server
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
}
type pendingState struct {
@@ -43,7 +51,7 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin
// Start tailing in a goroutine that handles file creation/deletion
go func() {
logPath := filepath.Join(server.ConfigPath, "\\server\\log\\server.log")
logPath := filepath.Join(server.GetLogPath(), "server.log")
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
s.logTailers.Store(server.ID, tailer)
@@ -52,20 +60,28 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin
}()
}
func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService, steamCredentialsRepo *repository.SteamCredentialsRepository) *ServerService {
steamService := NewSteamService(steamCredentialsRepo)
func NewServerService(
repository *repository.ServerRepository,
stateHistoryRepo *repository.StateHistoryRepository,
apiService *ApiService,
configService *ConfigService,
steamService *SteamService,
windowsService *WindowsService,
firewallService *FirewallService,
systemConfigService *SystemConfigService,
) *ServerService {
service := &ServerService{
repository: repository,
stateHistoryRepo: stateHistoryRepo,
apiService: apiService,
configService: configService,
stateHistoryRepo: stateHistoryRepo,
steamService: steamService,
windowsService: NewWindowsService(),
firewallService: NewFirewallService(),
windowsService: windowsService,
firewallService: firewallService,
systemConfigService: systemConfigService,
}
// Initialize instances for all servers
// Initialize server instances
servers, err := repository.GetAll(context.Background(), &model.ServerFilter{})
if err != nil {
logging.Error("Failed to get servers: %v", err)
@@ -144,14 +160,14 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
// Get configs using helper methods
event, err := s.configService.GetEventConfig(server)
if err != nil {
event = &model.EventConfig{}
logging.Error("Failed to get event config for server %d: %v", server.ID, err)
return
}
configuration, err := s.configService.GetConfiguration(server)
if err != nil {
configuration = &model.Configuration{}
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
return
}
if instance, ok := s.instances.Load(server.ID); ok {
@@ -181,6 +197,18 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
}
}
func (s *ServerService) GenerateServerPath(server *model.Server) {
// Get the base steamcmd path
steamCMDPath, err := s.systemConfigService.GetSteamCMDDirPath(context.Background())
if err != nil {
logging.Error("Failed to get steamcmd path: %v", err)
return
}
server.Path = server.GenerateServerPath(steamCMDPath)
}
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
// Update session duration when session changes
s.updateSessionDuration(server, state.Session)
@@ -309,34 +337,47 @@ func (s *ServerService) CreateServer(ctx *fiber.Ctx, server *model.Server) error
}
// Install server using SteamCMD
if err := s.steamService.InstallServer(ctx.UserContext(), server.ConfigPath); err != nil {
if err := s.steamService.InstallServer(ctx.UserContext(), server.GetServerPath()); err != nil {
return fmt.Errorf("failed to install server: %v", err)
}
// Create Windows service
execPath := filepath.Join(server.ConfigPath, "accServer.exe")
if err := s.windowsService.CreateService(server.ServiceName, execPath, server.ConfigPath, nil); err != nil {
// Create Windows service with correct paths
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
serverWorkingDir := filepath.Join(server.GetServerPath(), "server")
if err := s.windowsService.CreateService(ctx.UserContext(), server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
// Cleanup on failure
s.steamService.UninstallServer(server.ConfigPath)
s.steamService.UninstallServer(server.Path)
return fmt.Errorf("failed to create Windows service: %v", err)
}
// Create firewall rules
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
s.configureFirewall(server)
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
if err != nil {
return fmt.Errorf("failed to find available ports: %v", err)
}
// Use the first port for both TCP and UDP
serverPort := ports[0]
tcpPorts := []int{serverPort}
udpPorts := []int{serverPort}
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
// Cleanup on failure
s.windowsService.DeleteService(server.ServiceName)
s.steamService.UninstallServer(server.ConfigPath)
s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName)
s.steamService.UninstallServer(server.Path)
return fmt.Errorf("failed to create firewall rules: %v", err)
}
// Update server configuration with the allocated port
if err := s.updateServerPort(server, serverPort); err != nil {
return fmt.Errorf("failed to update server configuration: %v", err)
}
// Insert server into database
if err := s.repository.Insert(ctx.UserContext(), server); err != nil {
// Cleanup on failure
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
s.windowsService.DeleteService(server.ServiceName)
s.steamService.UninstallServer(server.ConfigPath)
s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName)
s.steamService.UninstallServer(server.Path)
return fmt.Errorf("failed to insert server into database: %v", err)
}
@@ -354,19 +395,24 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID int) error {
}
// Stop and remove Windows service
if err := s.windowsService.DeleteService(server.ServiceName); err != nil {
if err := s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName); err != nil {
logging.Error("Failed to delete Windows service: %v", err)
}
// Remove firewall rules
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
configuration, err := s.configService.GetConfiguration(server)
if err != nil {
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
}
tcpPorts := []int{configuration.TcpPort.ToInt()}
udpPorts := []int{configuration.UdpPort.ToInt()}
if err := s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
logging.Error("Failed to delete firewall rules: %v", err)
}
// Uninstall server files
if err := s.steamService.UninstallServer(server.ConfigPath); err != nil {
if err := s.steamService.UninstallServer(server.Path); err != nil {
logging.Error("Failed to uninstall server: %v", err)
}
@@ -401,29 +447,28 @@ func (s *ServerService) UpdateServer(ctx *fiber.Ctx, server *model.Server) error
}
// Update server files if path changed
if existingServer.ConfigPath != server.ConfigPath {
if err := s.steamService.InstallServer(ctx.UserContext(), server.ConfigPath); err != nil {
if existingServer.Path != server.Path {
if err := s.steamService.InstallServer(ctx.UserContext(), server.Path); err != nil {
return fmt.Errorf("failed to install server to new location: %v", err)
}
// Clean up old installation
if err := s.steamService.UninstallServer(existingServer.ConfigPath); err != nil {
if err := s.steamService.UninstallServer(existingServer.Path); err != nil {
logging.Error("Failed to remove old server installation: %v", err)
}
}
// Update Windows service if necessary
if existingServer.ServiceName != server.ServiceName || existingServer.ConfigPath != server.ConfigPath {
execPath := filepath.Join(server.ConfigPath, "accServer.exe")
if err := s.windowsService.UpdateService(server.ServiceName, execPath, server.ConfigPath, nil); err != nil {
if existingServer.ServiceName != server.ServiceName || existingServer.Path != server.Path {
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
serverWorkingDir := server.GetServerPath()
if err := s.windowsService.UpdateService(ctx.UserContext(), server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
return fmt.Errorf("failed to update Windows service: %v", err)
}
}
// Update firewall rules if service name changed
if existingServer.ServiceName != server.ServiceName {
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
if err := s.firewallService.UpdateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
if err := s.configureFirewall(server); err != nil {
return fmt.Errorf("failed to update firewall rules: %v", err)
}
}
@@ -436,5 +481,50 @@ func (s *ServerService) UpdateServer(ctx *fiber.Ctx, server *model.Server) error
// Restart server runtime
s.StartAccServerRuntime(server)
return nil
}
func (s *ServerService) configureFirewall(server *model.Server) error {
// Find available ports for the server
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
if err != nil {
return fmt.Errorf("failed to find available ports: %v", err)
}
// Use the first port for both TCP and UDP
serverPort := ports[0]
tcpPorts := []int{serverPort}
udpPorts := []int{serverPort}
logging.Info("Configuring firewall for server %d with port %d", server.ID, serverPort)
// Configure firewall rules
if err := s.firewallService.UpdateServerRules(server.Name, tcpPorts, udpPorts); err != nil {
return fmt.Errorf("failed to configure firewall: %v", err)
}
// Update server configuration with the allocated port
if err := s.updateServerPort(server, serverPort); err != nil {
return fmt.Errorf("failed to update server configuration: %v", err)
}
return nil
}
func (s *ServerService) updateServerPort(server *model.Server, port int) error {
// Load current configuration
config, err := s.configService.GetConfiguration(server)
if err != nil {
return fmt.Errorf("failed to load server configuration: %v", err)
}
config.TcpPort = model.IntString(port)
config.UdpPort = model.IntString(port)
// Save the updated configuration
if err := s.configService.SaveConfiguration(server, config); err != nil {
return fmt.Errorf("failed to save server configuration: %v", err)
}
return nil
}

View File

@@ -1,6 +1,7 @@
package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging"
"context"
@@ -14,25 +15,51 @@ import (
// Args:
// *dig.Container: Dig Container
func InitializeServices(c *dig.Container) {
logging.Debug("Initializing repositories")
repository.InitializeRepositories(c)
// Provide caches
logging.Debug("Creating lookup cache instance")
c.Provide(func() *model.LookupCache {
return model.NewLookupCache()
})
logging.Debug("Registering services")
// Provide services
c.Provide(NewServerService)
c.Provide(NewStateHistoryService)
c.Provide(NewApiService)
c.Provide(NewConfigService)
c.Provide(NewLookupService)
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService) {
c.Provide(NewSystemConfigService)
c.Provide(NewSteamService)
c.Provide(NewWindowsService)
c.Provide(NewFirewallService)
logging.Debug("Initializing service dependencies")
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService, systemConfig *SystemConfigService) {
logging.Debug("Setting up service cross-references")
api.SetServerService(server)
config.SetServerService(server)
logging.Debug("Initializing lookup data cache")
// Initialize lookup data using repository directly
lookup.cache.Set("tracks", lookup.repository.GetTracks(context.Background()))
lookup.cache.Set("cars", lookup.repository.GetCarModels(context.Background()))
lookup.cache.Set("drivers", lookup.repository.GetDriverCategories(context.Background()))
lookup.cache.Set("cups", lookup.repository.GetCupCategories(context.Background()))
lookup.cache.Set("sessions", lookup.repository.GetSessionTypes(context.Background()))
logging.Debug("Completed initializing lookup data cache")
logging.Debug("Initializing system config service")
// Initialize system config service
if err := systemConfig.Initialize(context.Background()); err != nil {
logging.Panic("failed to initialize system config service: " + err.Error())
}
logging.Debug("Completed initializing system config service")
})
if err != nil {
logging.Panic("unable to initialize services: " + err.Error())
}
logging.Debug("Completed service initialization")
}

View File

@@ -12,22 +12,23 @@ import (
)
const (
SteamCMDPath = "steamcmd"
ACCServerAppID = "1430110"
)
type SteamService struct {
executor *command.CommandExecutor
repository *repository.SteamCredentialsRepository
executor *command.CommandExecutor
repository *repository.SteamCredentialsRepository
configService *SystemConfigService
}
func NewSteamService(repository *repository.SteamCredentialsRepository) *SteamService {
func NewSteamService(repository *repository.SteamCredentialsRepository, configService *SystemConfigService) *SteamService {
return &SteamService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
repository: repository,
repository: repository,
configService: configService,
}
}
@@ -42,12 +43,25 @@ func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCr
return s.repository.Save(ctx, creds)
}
func (s *SteamService) ensureSteamCMD() error {
func (s *SteamService) ensureSteamCMD(ctx context.Context) error {
// Get SteamCMD path from config
steamCMDPath, err := s.configService.GetSteamCMDDirPath(ctx)
if err != nil {
return fmt.Errorf("failed to get SteamCMD path from config: %v", err)
}
steamCMDDir := filepath.Dir(steamCMDPath)
// Check if SteamCMD exists
if _, err := os.Stat(SteamCMDPath); !os.IsNotExist(err) {
if _, err := os.Stat(steamCMDPath); !os.IsNotExist(err) {
return nil
}
// Create directory if it doesn't exist
if err := os.MkdirAll(steamCMDDir, 0755); err != nil {
return fmt.Errorf("failed to create SteamCMD directory: %v", err)
}
// Download and install SteamCMD
logging.Info("Downloading SteamCMD...")
if err := s.executor.Execute("-Command",
@@ -58,7 +72,7 @@ func (s *SteamService) ensureSteamCMD() error {
// Extract SteamCMD
logging.Info("Extracting SteamCMD...")
if err := s.executor.Execute("-Command",
"Expand-Archive -Path 'steamcmd.zip' -DestinationPath 'steamcmd'"); err != nil {
fmt.Sprintf("Expand-Archive -Path 'steamcmd.zip' -DestinationPath '%s'", steamCMDDir)); err != nil {
return fmt.Errorf("failed to extract SteamCMD: %v", err)
}
@@ -68,12 +82,19 @@ func (s *SteamService) ensureSteamCMD() error {
}
func (s *SteamService) InstallServer(ctx context.Context, installPath string) error {
if err := s.ensureSteamCMD(); err != nil {
if err := s.ensureSteamCMD(ctx); err != nil {
return err
}
// Convert to absolute path and ensure proper Windows path format
absPath, err := filepath.Abs(installPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %v", err)
}
absPath = filepath.Clean(absPath)
// Ensure install path exists
if err := os.MkdirAll(installPath, 0755); err != nil {
if err := os.MkdirAll(absPath, 0755); err != nil {
return fmt.Errorf("failed to create install directory: %v", err)
}
@@ -83,12 +104,18 @@ func (s *SteamService) InstallServer(ctx context.Context, installPath string) er
return fmt.Errorf("failed to get Steam credentials: %v", err)
}
// Get SteamCMD path from config
steamCMDPath, err := s.configService.GetSteamCMDPath(ctx)
if err != nil {
return fmt.Errorf("failed to get SteamCMD path from config: %v", err)
}
// Build SteamCMD command
args := []string{
"-nologo",
"-noprofile",
filepath.Join(SteamCMDPath, "steamcmd.exe"),
"+force_install_dir", installPath,
steamCMDPath,
"+force_install_dir", absPath,
"+login",
}
@@ -108,11 +135,24 @@ func (s *SteamService) InstallServer(ctx context.Context, installPath string) er
)
// Run SteamCMD
logging.Info("Installing ACC server to %s...", installPath)
logging.Info("Installing ACC server to %s...", absPath)
if err := s.executor.Execute(args...); err != nil {
return fmt.Errorf("failed to install server: %v", err)
return fmt.Errorf("failed to run SteamCMD: %v", err)
}
// Add a delay to allow Steam to properly cleanup
logging.Info("Waiting for Steam operations to complete...")
if err := s.executor.Execute("-Command", "Start-Sleep -Seconds 5"); err != nil {
logging.Warn("Failed to wait after Steam operations: %v", err)
}
// Verify installation
exePath := filepath.Join(absPath, "server", "accServer.exe")
if _, err := os.Stat(exePath); os.IsNotExist(err) {
return fmt.Errorf("server installation failed: accServer.exe not found in %s", absPath)
}
logging.Info("Server installation completed successfully")
return nil
}

View File

@@ -0,0 +1,127 @@
package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
"path/filepath"
"go.uber.org/dig"
)
type SystemConfigService struct {
repository *repository.SystemConfigRepository
cache *model.LookupCache
}
// SystemConfigServiceParams holds the dependencies for SystemConfigService
type SystemConfigServiceParams struct {
dig.In
Repository *repository.SystemConfigRepository
Cache *model.LookupCache
}
// NewSystemConfigService creates a new SystemConfigService with dependencies injected by dig
func NewSystemConfigService(params SystemConfigServiceParams) *SystemConfigService {
logging.Debug("Initializing SystemConfigService")
return &SystemConfigService{
repository: params.Repository,
cache: params.Cache,
}
}
func (s *SystemConfigService) Initialize(ctx context.Context) error {
logging.Debug("Initializing system config cache")
// Cache all configs
configs, err := s.repository.GetAll(ctx)
if err != nil {
return fmt.Errorf("failed to get configs for caching: %v", err)
}
for _, config := range *configs {
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key)
s.cache.Set(cacheKey, &config)
logging.Debug("Cached system config: %s", config.Key)
}
logging.Debug("Completed initializing system config cache")
return nil
}
func (s *SystemConfigService) GetConfig(ctx context.Context, key string) (*model.SystemConfig, error) {
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, key)
// Try to get from cache first
if cached, exists := s.cache.Get(cacheKey); exists {
if config, ok := cached.(*model.SystemConfig); ok {
return config, nil
}
logging.Debug("Invalid type in cache for key: %s", key)
}
// If not in cache, get from database
logging.Debug("Loading system config from database: %s", key)
config, err := s.repository.Get(ctx, key)
if err != nil {
return nil, err
}
if config == nil {
logging.Error("Configuration not found for key: %s", key)
return nil, nil
}
// Cache the result
s.cache.Set(cacheKey, config)
return config, nil
}
func (s *SystemConfigService) GetAllConfigs(ctx context.Context) (*[]model.SystemConfig, error) {
logging.Debug("Loading all system configs from database")
return s.repository.GetAll(ctx)
}
func (s *SystemConfigService) UpdateConfig(ctx context.Context, config *model.SystemConfig) error {
if err := s.repository.Update(ctx, config); err != nil {
return err
}
// Update cache
cacheKey := fmt.Sprintf(model.CacheKeySystemConfig, config.Key)
s.cache.Set(cacheKey, config)
logging.Debug("Updated system config in cache: %s", config.Key)
return nil
}
func (s *SystemConfigService) GetSteamCMDDirPath(ctx context.Context) (string, error) {
steamCMDPath, err := s.GetSteamCMDPath(ctx)
if err != nil {
return "", err
}
return filepath.Dir(steamCMDPath), nil
}
// Helper methods for common configurations
func (s *SystemConfigService) GetSteamCMDPath(ctx context.Context) (string, error) {
config, err := s.GetConfig(ctx, model.ConfigKeySteamCMDPath)
if err != nil {
return "", err
}
if config == nil {
return "", nil
}
return config.GetEffectiveValue(), nil
}
func (s *SystemConfigService) GetNSSMPath(ctx context.Context) (string, error) {
config, err := s.GetConfig(ctx, model.ConfigKeyNSSMPath)
if err != nil {
return "", err
}
if config == nil {
return "", nil
}
return config.GetEffectiveValue(), nil
}

View File

@@ -3,35 +3,47 @@ package service
import (
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
"path/filepath"
"strings"
)
const (
NSSMPath = ".\\nssm"
NSSMPath = ".\\nssm.exe"
)
type WindowsService struct {
executor *command.CommandExecutor
executor *command.CommandExecutor
configService *SystemConfigService
}
func NewWindowsService() *WindowsService {
func NewWindowsService(configService *SystemConfigService) *WindowsService {
return &WindowsService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
configService: configService,
}
}
// executeNSSM runs an NSSM command through PowerShell with elevation
func (s *WindowsService) executeNSSM(args ...string) (string, error) {
func (s *WindowsService) executeNSSM(ctx context.Context, args ...string) (string, error) {
// Get NSSM path from config
nssmPath, err := s.configService.GetNSSMPath(ctx)
if err != nil {
return "", fmt.Errorf("failed to get NSSM path from config: %v", err)
}
// Prepend NSSM path to arguments
nssmArgs := append([]string{"-nologo", "-noprofile", NSSMPath}, args...)
nssmArgs := append([]string{"-NoProfile", "-NonInteractive", "-Command", "& " + nssmPath}, args...)
output, err := s.executor.ExecuteWithOutput(nssmArgs...)
if err != nil {
// Log the full command and error for debugging
logging.Error("NSSM command failed: powershell %s", strings.Join(nssmArgs, " "))
logging.Error("NSSM error output: %s", output)
return "", err
}
@@ -45,42 +57,54 @@ func (s *WindowsService) executeNSSM(args ...string) (string, error) {
// Service Installation/Configuration Methods
func (s *WindowsService) CreateService(serviceName, execPath, workingDir string, args []string) error {
// Ensure paths are absolute
func (s *WindowsService) CreateService(ctx context.Context, serviceName, execPath, workingDir string, args []string) error {
// Ensure paths are absolute and properly formatted for Windows
absExecPath, err := filepath.Abs(execPath)
if err != nil {
return fmt.Errorf("failed to get absolute path for executable: %v", err)
}
absExecPath = filepath.Clean(absExecPath)
absWorkingDir, err := filepath.Abs(workingDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for working directory: %v", err)
}
absWorkingDir = filepath.Clean(absWorkingDir)
// Log the paths being used
logging.Info("Creating service '%s' with:", serviceName)
logging.Info(" Executable: %s", absExecPath)
logging.Info(" Working Directory: %s", absWorkingDir)
// First remove any existing service with the same name
s.executeNSSM(ctx, "remove", serviceName, "confirm")
// Install service
if _, err := s.executeNSSM("install", serviceName, absExecPath); err != nil {
if _, err := s.executeNSSM(ctx, "install", serviceName, absExecPath); err != nil {
return fmt.Errorf("failed to install service: %v", err)
}
// Set working directory
if _, err := s.executeNSSM("set", serviceName, "AppDirectory", absWorkingDir); err != nil {
return fmt.Errorf("failed to set working directory: %v", err)
}
// Set arguments if provided
if len(args) > 0 {
cmdArgs := append([]string{"set", serviceName, "AppParameters"}, args...)
if _, err := s.executeNSSM(cmdArgs...); err != nil {
if _, err := s.executeNSSM(ctx, cmdArgs...); err != nil {
// Try to clean up on failure
s.executeNSSM(ctx, "remove", serviceName, "confirm")
return fmt.Errorf("failed to set arguments: %v", err)
}
}
// Verify service was created
if _, err := s.executeNSSM(ctx, "get", serviceName, "Application"); err != nil {
return fmt.Errorf("service creation verification failed: %v", err)
}
logging.Info("Created Windows service: %s", serviceName)
return nil
}
func (s *WindowsService) DeleteService(serviceName string) error {
if _, err := s.executeNSSM("remove", serviceName, "confirm"); err != nil {
func (s *WindowsService) DeleteService(ctx context.Context, serviceName string) error {
if _, err := s.executeNSSM(ctx, "remove", serviceName, "confirm"); err != nil {
return fmt.Errorf("failed to remove service: %v", err)
}
@@ -88,36 +112,36 @@ func (s *WindowsService) DeleteService(serviceName string) error {
return nil
}
func (s *WindowsService) UpdateService(serviceName, execPath, workingDir string, args []string) error {
func (s *WindowsService) UpdateService(ctx context.Context, serviceName, execPath, workingDir string, args []string) error {
// First remove the existing service
if err := s.DeleteService(serviceName); err != nil {
if err := s.DeleteService(ctx, serviceName); err != nil {
return err
}
// Then create it again with new parameters
return s.CreateService(serviceName, execPath, workingDir, args)
return s.CreateService(ctx, serviceName, execPath, workingDir, args)
}
// Service Control Methods
func (s *WindowsService) Status(serviceName string) (string, error) {
return s.executeNSSM("status", serviceName)
func (s *WindowsService) Status(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "status", serviceName)
}
func (s *WindowsService) Start(serviceName string) (string, error) {
return s.executeNSSM("start", serviceName)
func (s *WindowsService) Start(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "start", serviceName)
}
func (s *WindowsService) Stop(serviceName string) (string, error) {
return s.executeNSSM("stop", serviceName)
func (s *WindowsService) Stop(ctx context.Context, serviceName string) (string, error) {
return s.executeNSSM(ctx, "stop", serviceName)
}
func (s *WindowsService) Restart(serviceName string) (string, error) {
func (s *WindowsService) Restart(ctx context.Context, serviceName string) (string, error) {
// First stop the service
if _, err := s.Stop(serviceName); err != nil {
if _, err := s.Stop(ctx, serviceName); err != nil {
return "", err
}
// Then start it again
return s.Start(serviceName)
return s.Start(ctx, serviceName)
}