add step list for server creation
This commit is contained in:
@@ -77,8 +77,8 @@ func NewConfigService(repository *repository.ConfigRepository, serverRepository
|
||||
repository: repository,
|
||||
serverRepository: serverRepository,
|
||||
configCache: model.NewServerConfigCache(model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute, // Cache configs for 5 minutes
|
||||
ThrottleTime: 1 * time.Second, // Prevent rapid re-reads
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}),
|
||||
}
|
||||
@@ -88,10 +88,6 @@ func (as *ConfigService) SetServerService(serverService *ServerService) {
|
||||
as.serverService = serverService
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
// Updates physical config file and caches it in database.
|
||||
//
|
||||
// Args:
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
@@ -103,7 +99,62 @@ func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface
|
||||
return as.updateConfigInternal(ctx.UserContext(), serverID, configFile, body, override)
|
||||
}
|
||||
|
||||
// updateConfigInternal handles the actual config update logic without Fiber dependencies
|
||||
func (as *ConfigService) updateConfigFiles(ctx context.Context, server *model.Server, configFile string, body *map[string]interface{}, override bool) ([]byte, []byte, error) {
|
||||
if server == nil {
|
||||
logging.Error("Server not found")
|
||||
return nil, nil, fmt.Errorf("server not found")
|
||||
}
|
||||
|
||||
configPath := filepath.Join(server.GetConfigPath(), configFile)
|
||||
oldData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte("{}"), 0644); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
oldData = []byte("{}")
|
||||
} else {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
oldDataUTF8, err := DecodeUTF16LEBOM(oldData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newData, err := json.Marshal(&body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !override {
|
||||
newData, err = jsons.Merge(oldDataUTF8, newData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
newData, err = common.IndentJson(newData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
newDataUTF16, err := EncodeUTF16LEBOM(newData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return oldDataUTF8, newData, nil
|
||||
}
|
||||
|
||||
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID string, configFile string, body *map[string]interface{}, override bool) (*model.Config, error) {
|
||||
serverUUID, err := uuid.Parse(serverID)
|
||||
if err != nil {
|
||||
@@ -117,63 +168,14 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID stri
|
||||
return nil, fmt.Errorf("server not found")
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
configPath := filepath.Join(server.GetConfigPath(), configFile)
|
||||
oldData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Create directory if it doesn't exist
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create empty JSON file
|
||||
if err := os.WriteFile(configPath, []byte("{}"), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldData = []byte("{}")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
oldDataUTF8, err := DecodeUTF16LEBOM(oldData)
|
||||
oldDataUTF8, newData, err := as.updateConfigFiles(ctx, server, configFile, body, override)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write new config
|
||||
newData, err := json.Marshal(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !override {
|
||||
newData, err = jsons.Merge(oldDataUTF8, newData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
newData, err = common.IndentJson(newData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newDataUTF16, err := EncodeUTF16LEBOM(newData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Invalidate all configs for this server since configs can be interdependent
|
||||
as.configCache.InvalidateServerCache(serverID)
|
||||
|
||||
as.serverService.StartAccServerRuntime(server)
|
||||
|
||||
// Log change
|
||||
return as.repository.UpdateConfig(ctx, &model.Config{
|
||||
ServerID: serverUUID,
|
||||
ConfigFile: configFile,
|
||||
@@ -183,10 +185,6 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID stri
|
||||
}), nil
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
// Gets physical config file and caches it in database.
|
||||
//
|
||||
// Args:
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
@@ -197,44 +195,47 @@ func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
|
||||
logging.Debug("Getting config for server ID: %s, file: %s", serverIDStr, configFile)
|
||||
|
||||
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverIDStr)
|
||||
|
||||
if err != nil {
|
||||
logging.Error("Server not found")
|
||||
return nil, fiber.NewError(404, "Server not found")
|
||||
}
|
||||
return as.getConfigFile(server, configFile)
|
||||
}
|
||||
|
||||
// Try to get from cache based on config file type
|
||||
func (as *ConfigService) getConfigFile(server *model.Server, configFile string) (interface{}, error) {
|
||||
serverIDStr := server.ID.String()
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
return *cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
configPath := server.GetConfigPath()
|
||||
decoder := DecodeFileName(configFile)
|
||||
if decoder == nil {
|
||||
return nil, errors.New("invalid config file")
|
||||
@@ -244,43 +245,39 @@ func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
|
||||
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
|
||||
return model.Configuration{}, nil
|
||||
case AssistRulesJson:
|
||||
return &model.AssistRules{}, nil
|
||||
return model.AssistRules{}, nil
|
||||
case EventJson:
|
||||
return &model.EventConfig{}, nil
|
||||
return model.EventConfig{}, nil
|
||||
case EventRulesJson:
|
||||
return &model.EventRules{}, nil
|
||||
return model.EventRules{}, nil
|
||||
case SettingsJson:
|
||||
return &model.ServerSettings{}, nil
|
||||
return model.ServerSettings{}, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the loaded config
|
||||
switch configFile {
|
||||
case ConfigurationJson:
|
||||
as.configCache.UpdateConfiguration(serverIDStr, *config.(*model.Configuration))
|
||||
as.configCache.UpdateConfiguration(serverIDStr, config.(model.Configuration))
|
||||
case AssistRulesJson:
|
||||
as.configCache.UpdateAssistRules(serverIDStr, *config.(*model.AssistRules))
|
||||
as.configCache.UpdateAssistRules(serverIDStr, config.(model.AssistRules))
|
||||
case EventJson:
|
||||
as.configCache.UpdateEvent(serverIDStr, *config.(*model.EventConfig))
|
||||
as.configCache.UpdateEvent(serverIDStr, config.(model.EventConfig))
|
||||
case EventRulesJson:
|
||||
as.configCache.UpdateEventRules(serverIDStr, *config.(*model.EventRules))
|
||||
as.configCache.UpdateEventRules(serverIDStr, config.(model.EventRules))
|
||||
case SettingsJson:
|
||||
as.configCache.UpdateSettings(serverIDStr, *config.(*model.ServerSettings))
|
||||
as.configCache.UpdateSettings(serverIDStr, config.(model.ServerSettings))
|
||||
}
|
||||
|
||||
logging.Debug("Successfully loaded and cached config for server ID: %s, file: %s", serverIDStr, configFile)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetConfigs
|
||||
// Gets all configurations for a server, using cache when possible.
|
||||
func (as *ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) {
|
||||
serverID := ctx.Params("id")
|
||||
|
||||
@@ -296,82 +293,33 @@ func (as *ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, erro
|
||||
func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configurations, error) {
|
||||
serverIDStr := server.ID.String()
|
||||
logging.Info("Loading configs for server ID: %s at path: %s", serverIDStr, server.GetConfigPath())
|
||||
configs := &model.Configurations{}
|
||||
|
||||
// Load configuration
|
||||
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
|
||||
logging.Debug("Using cached configuration for server %s", serverIDStr)
|
||||
configs.Configuration = *cached
|
||||
} else {
|
||||
logging.Debug("Loading configuration from disk for server %s", serverIDStr)
|
||||
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)
|
||||
}
|
||||
configs.Configuration = config
|
||||
as.configCache.UpdateConfiguration(serverIDStr, config)
|
||||
settingsConf, err := as.getConfigFile(server, SettingsJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load assist rules
|
||||
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
|
||||
logging.Debug("Using cached assist rules for server %s", serverIDStr)
|
||||
configs.AssistRules = *cached
|
||||
} else {
|
||||
logging.Debug("Loading assist rules from disk for server %s", serverIDStr)
|
||||
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)
|
||||
}
|
||||
configs.AssistRules = rules
|
||||
as.configCache.UpdateAssistRules(serverIDStr, rules)
|
||||
eventRulesConf, err := as.getConfigFile(server, EventRulesJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load event config
|
||||
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
|
||||
logging.Debug("Using cached event config for server %s", serverIDStr)
|
||||
configs.Event = *cached
|
||||
} else {
|
||||
logging.Debug("Loading event config from disk for server %s", serverIDStr)
|
||||
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)
|
||||
}
|
||||
configs.Event = event
|
||||
logging.Debug("Updating event config for server %s with track: %s", serverIDStr, event.Track)
|
||||
as.configCache.UpdateEvent(serverIDStr, event)
|
||||
eventConf, err := as.getConfigFile(server, EventJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load event rules
|
||||
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
|
||||
logging.Debug("Using cached event rules for server %s", serverIDStr)
|
||||
configs.EventRules = *cached
|
||||
} else {
|
||||
logging.Debug("Loading event rules from disk for server %s", serverIDStr)
|
||||
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)
|
||||
}
|
||||
configs.EventRules = rules
|
||||
as.configCache.UpdateEventRules(serverIDStr, rules)
|
||||
assistRulesConf, err := as.getConfigFile(server, AssistRulesJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load settings
|
||||
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
|
||||
logging.Debug("Using cached settings for server %s", serverIDStr)
|
||||
configs.Settings = *cached
|
||||
} else {
|
||||
logging.Debug("Loading settings from disk for server %s", serverIDStr)
|
||||
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)
|
||||
}
|
||||
configs.Settings = settings
|
||||
as.configCache.UpdateSettings(serverIDStr, settings)
|
||||
configurationConf, err := as.getConfigFile(server, ConfigurationJson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs := &model.Configurations{
|
||||
Settings: settingsConf.(model.ServerSettings),
|
||||
EventRules: eventRulesConf.(model.EventRules),
|
||||
Event: eventConf.(model.EventConfig),
|
||||
AssistRules: assistRulesConf.(model.AssistRules),
|
||||
Configuration: configurationConf.(model.Configuration),
|
||||
}
|
||||
|
||||
logging.Info("Successfully loaded all configs for server %s", serverIDStr)
|
||||
@@ -396,9 +344,6 @@ func readFile(path string, configFile string) ([]byte, error) {
|
||||
configPath := filepath.Join(path, configFile)
|
||||
oldData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("config file %s does not exist at %s", configFile, configPath)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return oldData, nil
|
||||
@@ -475,9 +420,7 @@ func (as *ConfigService) GetConfiguration(server *model.Server) (*model.Configur
|
||||
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 {
|
||||
@@ -487,7 +430,6 @@ func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.C
|
||||
return fmt.Errorf("failed to unmarshal configuration: %v", err)
|
||||
}
|
||||
|
||||
// Update the configuration using the internal method
|
||||
_, err = as.updateConfigInternal(context.Background(), server.ID.String(), ConfigurationJson, &configMap, true)
|
||||
_, _, err = as.updateConfigFiles(context.Background(), server, ConfigurationJson, &configMap, true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -96,11 +96,9 @@ func (s *FirewallService) DeleteServerRules(serverName string, tcpPorts, udpPort
|
||||
}
|
||||
|
||||
func (s *FirewallService) UpdateServerRules(serverName string, tcpPorts, udpPorts []int) error {
|
||||
// First delete existing rules
|
||||
if err := s.DeleteServerRules(serverName, tcpPorts, udpPorts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then create new rules
|
||||
return s.CreateServerRules(serverName, tcpPorts, udpPorts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CacheInvalidator interface for cache invalidation
|
||||
type CacheInvalidator interface {
|
||||
InvalidateUserPermissions(userID string)
|
||||
InvalidateAllUserPermissions()
|
||||
}
|
||||
|
||||
// MembershipService provides business logic for membership-related operations.
|
||||
type MembershipService struct {
|
||||
repo *repository.MembershipRepository
|
||||
cacheInvalidator CacheInvalidator
|
||||
@@ -26,29 +24,25 @@ type MembershipService struct {
|
||||
openJwtHandler *jwt.OpenJWTHandler
|
||||
}
|
||||
|
||||
// NewMembershipService creates a new MembershipService.
|
||||
func NewMembershipService(repo *repository.MembershipRepository, jwtHandler *jwt.JWTHandler, openJwtHandler *jwt.OpenJWTHandler) *MembershipService {
|
||||
return &MembershipService{
|
||||
repo: repo,
|
||||
cacheInvalidator: nil, // Will be set later via SetCacheInvalidator
|
||||
cacheInvalidator: nil,
|
||||
jwtHandler: jwtHandler,
|
||||
openJwtHandler: openJwtHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// SetCacheInvalidator sets the cache invalidator after service initialization
|
||||
func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
|
||||
s.cacheInvalidator = invalidator
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a JWT.
|
||||
func (s *MembershipService) HandleLogin(ctx context.Context, username, password string) (*model.User, error) {
|
||||
user, err := s.repo.FindUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Use secure password verification with constant-time comparison
|
||||
if err := user.VerifyPassword(password); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
@@ -56,7 +50,6 @@ func (s *MembershipService) HandleLogin(ctx context.Context, username, password
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a JWT.
|
||||
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
|
||||
user, err := s.HandleLogin(ctx, username, password)
|
||||
if err != nil {
|
||||
@@ -70,7 +63,6 @@ func (s *MembershipService) GenerateOpenToken(ctx context.Context, userId string
|
||||
return s.openJwtHandler.GenerateToken(userId)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
|
||||
|
||||
role, err := s.repo.FindRoleByName(ctx, roleName)
|
||||
@@ -94,43 +86,35 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password,
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ListUsers retrieves all users.
|
||||
func (s *MembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
|
||||
return s.repo.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// GetUser retrieves a single user by ID.
|
||||
func (s *MembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
|
||||
return s.repo.FindUserByID(ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserWithPermissions retrieves a single user by ID with their role and permissions.
|
||||
func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
|
||||
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||
}
|
||||
|
||||
// UpdateUserRequest defines the request body for updating a user.
|
||||
type UpdateUserRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
RoleID *uuid.UUID `json:"roleId"`
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
||||
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
||||
// Get user with role information
|
||||
user, err := s.repo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Get role to check if it's Super Admin
|
||||
role, err := s.repo.FindRoleByID(ctx, user.RoleID)
|
||||
if err != nil {
|
||||
return errors.New("user role not found")
|
||||
}
|
||||
|
||||
// Prevent deletion of Super Admin users
|
||||
if role.Name == "Super Admin" {
|
||||
return errors.New("cannot delete Super Admin user")
|
||||
}
|
||||
@@ -140,7 +124,6 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate cache for deleted user
|
||||
if s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||
}
|
||||
@@ -149,7 +132,6 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUser updates a user's details.
|
||||
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) {
|
||||
user, err := s.repo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -161,12 +143,10 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
||||
}
|
||||
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
// Password will be automatically hashed in BeforeUpdate hook
|
||||
user.Password = *req.Password
|
||||
}
|
||||
|
||||
if req.RoleID != nil {
|
||||
// Check if role exists
|
||||
_, err := s.repo.FindRoleByID(ctx, *req.RoleID)
|
||||
if err != nil {
|
||||
return nil, errors.New("role not found")
|
||||
@@ -178,7 +158,6 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Invalidate cache if role was changed
|
||||
if req.RoleID != nil && s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||
}
|
||||
@@ -187,14 +166,12 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// HasPermission checks if a user has a specific permission.
|
||||
func (s *MembershipService) HasPermission(ctx context.Context, userID string, permissionName string) (bool, error) {
|
||||
user, err := s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Super admin and Admin have all permissions
|
||||
if user.Role.Name == "Super Admin" || user.Role.Name == "Admin" {
|
||||
return true, nil
|
||||
}
|
||||
@@ -208,15 +185,13 @@ func (s *MembershipService) HasPermission(ctx context.Context, userID string, pe
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SetupInitialData creates the initial roles and permissions.
|
||||
func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
// Define all permissions
|
||||
permissions := model.AllPermissions()
|
||||
|
||||
createdPermissions := make([]model.Permission, 0)
|
||||
for _, pName := range permissions {
|
||||
perm, err := s.repo.FindPermissionByName(ctx, pName)
|
||||
if err != nil { // Assuming error means not found
|
||||
if err != nil {
|
||||
perm = &model.Permission{Name: pName}
|
||||
if err := s.repo.CreatePermission(ctx, perm); err != nil {
|
||||
return err
|
||||
@@ -225,7 +200,6 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
createdPermissions = append(createdPermissions, *perm)
|
||||
}
|
||||
|
||||
// Create Super Admin role with all permissions
|
||||
superAdminRole, err := s.repo.FindRoleByName(ctx, "Super Admin")
|
||||
if err != nil {
|
||||
superAdminRole = &model.Role{Name: "Super Admin"}
|
||||
@@ -237,7 +211,6 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Admin role with same permissions as Super Admin
|
||||
adminRole, err := s.repo.FindRoleByName(ctx, "Admin")
|
||||
if err != nil {
|
||||
adminRole = &model.Role{Name: "Admin"}
|
||||
@@ -249,7 +222,6 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Manager role with limited permissions (excluding membership, role, user, server create/delete)
|
||||
managerRole, err := s.repo.FindRoleByName(ctx, "Manager")
|
||||
if err != nil {
|
||||
managerRole = &model.Role{Name: "Manager"}
|
||||
@@ -258,7 +230,6 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Define manager permissions (limited set)
|
||||
managerPermissionNames := []string{
|
||||
model.ServerView,
|
||||
model.ServerUpdate,
|
||||
@@ -282,16 +253,14 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate all caches after role setup changes
|
||||
if s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateAllUserPermissions()
|
||||
}
|
||||
|
||||
// Create a default admin user if one doesn't exist
|
||||
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
||||
if err != nil {
|
||||
logging.Debug("Creating default admin user")
|
||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -300,7 +269,6 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllRoles retrieves all roles for dropdown selection.
|
||||
func (s *MembershipService) GetAllRoles(ctx context.Context) ([]*model.Role, error) {
|
||||
return s.repo.ListRoles(ctx)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
const (
|
||||
DefaultStartPort = 9600
|
||||
RequiredPortCount = 1 // Update this if ACC needs more ports
|
||||
RequiredPortCount = 1
|
||||
)
|
||||
|
||||
type ServerService struct {
|
||||
@@ -45,18 +45,15 @@ type pendingState struct {
|
||||
}
|
||||
|
||||
func (s *ServerService) ensureLogTailing(server *model.Server, instance *tracking.AccServerInstance) {
|
||||
// Check if we already have a tailer
|
||||
if _, exists := s.logTailers.Load(server.ID); exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Start tailing in a goroutine that handles file creation/deletion
|
||||
go func() {
|
||||
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
||||
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
|
||||
s.logTailers.Store(server.ID, tailer)
|
||||
|
||||
// Start tailing and automatically handle file changes
|
||||
tailer.Start()
|
||||
}()
|
||||
}
|
||||
@@ -82,7 +79,6 @@ func NewServerService(
|
||||
webSocketService: webSocketService,
|
||||
}
|
||||
|
||||
// Initialize server instances
|
||||
servers, err := repository.GetAll(context.Background(), &model.ServerFilter{})
|
||||
if err != nil {
|
||||
logging.Error("Failed to get servers: %v", err)
|
||||
@@ -90,7 +86,6 @@ func NewServerService(
|
||||
}
|
||||
|
||||
for i := range *servers {
|
||||
// Initialize instance regardless of status
|
||||
logging.Info("Starting server runtime for server ID: %d", (*servers)[i].ID)
|
||||
service.StartAccServerRuntime(&(*servers)[i])
|
||||
}
|
||||
@@ -99,7 +94,7 @@ func NewServerService(
|
||||
}
|
||||
|
||||
func (s *ServerService) shouldInsertStateHistory(serverID uuid.UUID) bool {
|
||||
insertInterval := 5 * time.Minute // Configure this as needed
|
||||
insertInterval := 5 * time.Minute
|
||||
|
||||
lastInsertInterface, exists := s.lastInsertTimes.Load(serverID)
|
||||
if !exists {
|
||||
@@ -122,16 +117,15 @@ func (s *ServerService) getNextSessionID(serverID uuid.UUID) uuid.UUID {
|
||||
lastID, err := s.stateHistoryRepo.GetLastSessionID(context.Background(), serverID)
|
||||
if err != nil {
|
||||
logging.Error("Failed to get last session ID for server %s: %v", serverID, err)
|
||||
return uuid.New() // Return new UUID as fallback
|
||||
return uuid.New()
|
||||
}
|
||||
if lastID == uuid.Nil {
|
||||
return uuid.New() // Return new UUID if no previous session
|
||||
return uuid.New()
|
||||
}
|
||||
return uuid.New() // Always generate new UUID for each session
|
||||
return uuid.New()
|
||||
}
|
||||
|
||||
func (s *ServerService) insertStateHistory(serverID uuid.UUID, state *model.ServerState) {
|
||||
// Get or create session ID when session changes
|
||||
currentSessionInterface, exists := s.instances.Load(serverID)
|
||||
var sessionID uuid.UUID
|
||||
if !exists {
|
||||
@@ -163,7 +157,6 @@ func (s *ServerService) insertStateHistory(serverID uuid.UUID, state *model.Serv
|
||||
}
|
||||
|
||||
func (s *ServerService) updateSessionDuration(server *model.Server, sessionType model.TrackSession) {
|
||||
// Get configs using helper methods
|
||||
event, err := s.configService.GetEventConfig(server)
|
||||
if err != nil {
|
||||
event = &model.EventConfig{}
|
||||
@@ -181,9 +174,7 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
|
||||
serverInstance.State.Track = event.Track
|
||||
serverInstance.State.MaxConnections = configuration.MaxConnections.ToInt()
|
||||
|
||||
// Check if session type has changed
|
||||
if serverInstance.State.Session != sessionType {
|
||||
// Get new session ID for the new session
|
||||
sessionID := s.getNextSessionID(server.ID)
|
||||
s.sessionIDs.Store(server.ID, sessionID)
|
||||
}
|
||||
@@ -204,33 +195,27 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
|
||||
}
|
||||
|
||||
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
||||
// Get the base steamcmd path from environment variable
|
||||
steamCMDPath := env.GetSteamCMDDirPath()
|
||||
server.FromSteamCMD = true
|
||||
server.Path = server.GenerateServerPath(steamCMDPath)
|
||||
server.FromSteamCMD = true
|
||||
}
|
||||
|
||||
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
||||
// Update session duration when session changes
|
||||
s.updateSessionDuration(server, state.Session)
|
||||
|
||||
// Invalidate status cache when server state changes
|
||||
s.apiService.statusCache.InvalidateStatus(server.ServiceName)
|
||||
|
||||
// Cancel existing timer if any
|
||||
if debouncer, exists := s.debouncers.Load(server.ID); exists {
|
||||
pending := debouncer.(*pendingState)
|
||||
pending.timer.Stop()
|
||||
}
|
||||
|
||||
// Create new timer
|
||||
timer := time.NewTimer(5 * time.Minute)
|
||||
s.debouncers.Store(server.ID, &pendingState{
|
||||
timer: timer,
|
||||
state: state,
|
||||
})
|
||||
|
||||
// Start goroutine to handle the delayed insert
|
||||
go func() {
|
||||
<-timer.C
|
||||
if debouncer, exists := s.debouncers.Load(server.ID); exists {
|
||||
@@ -240,14 +225,12 @@ func (s *ServerService) handleStateChange(server *model.Server, state *model.Ser
|
||||
}
|
||||
}()
|
||||
|
||||
// If enough time has passed since last insert, insert immediately
|
||||
if s.shouldInsertStateHistory(server.ID) {
|
||||
s.insertStateHistory(server.ID, state)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) StartAccServerRuntime(server *model.Server) {
|
||||
// Get or create instance
|
||||
instanceInterface, exists := s.instances.Load(server.ID)
|
||||
var instance *tracking.AccServerInstance
|
||||
if !exists {
|
||||
@@ -259,20 +242,14 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
|
||||
instance = instanceInterface.(*tracking.AccServerInstance)
|
||||
}
|
||||
|
||||
// Invalidate config cache for this server before loading new configs
|
||||
serverIDStr := server.ID.String()
|
||||
s.configService.configCache.InvalidateServerCache(serverIDStr)
|
||||
|
||||
s.updateSessionDuration(server, instance.State.Session)
|
||||
|
||||
// Ensure log tailing is running (regardless of server status)
|
||||
s.ensureLogTailing(server, instance)
|
||||
}
|
||||
|
||||
// GetAll
|
||||
// Gets All rows from Server table.
|
||||
//
|
||||
// Args:
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
@@ -304,10 +281,6 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// GetById
|
||||
// Gets rows by ID from Server table.
|
||||
//
|
||||
// Args:
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
@@ -334,22 +307,19 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Ser
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// CreateServerAsync starts server creation asynchronously and returns immediately
|
||||
func (s *ServerService) CreateServerAsync(ctx *fiber.Ctx, server *model.Server) error {
|
||||
// Perform basic validation first
|
||||
logging.Info("create server start")
|
||||
if err := server.Validate(); err != nil {
|
||||
logging.Info("create server validation failed")
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate server path
|
||||
s.GenerateServerPath(server)
|
||||
|
||||
// Create a background context that won't be cancelled when the HTTP request ends
|
||||
bgCtx := context.Background()
|
||||
|
||||
// Start the actual creation process in a goroutine
|
||||
go func() {
|
||||
// Create server in background without using fiber.Ctx
|
||||
logging.Info("create server start background")
|
||||
if err := s.createServerBackground(bgCtx, server); err != nil {
|
||||
logging.Error("Async server creation failed for server %s: %v", server.ID, err)
|
||||
s.webSocketService.BroadcastError(server.ID, "Server creation failed", err.Error())
|
||||
@@ -360,127 +330,128 @@ func (s *ServerService) CreateServerAsync(ctx *fiber.Ctx, server *model.Server)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) CreateServer(ctx *fiber.Ctx, server *model.Server) error {
|
||||
// Broadcast step: validation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepValidation), "")
|
||||
type createServerStep struct {
|
||||
stepType model.ServerCreationStep
|
||||
important bool
|
||||
callback func() (string, error)
|
||||
description string
|
||||
}
|
||||
|
||||
// Validate basic server configuration
|
||||
if err := server.Validate(); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusFailed,
|
||||
"", fmt.Sprintf("Validation failed: %v", err))
|
||||
return err
|
||||
func (s *ServerService) createServerBackground(ctx context.Context, server *model.Server) error {
|
||||
var serverPort int
|
||||
var tcpPorts, udpPorts []int
|
||||
|
||||
steps := []createServerStep{
|
||||
{
|
||||
stepType: model.StepValidation,
|
||||
important: true,
|
||||
description: "Server configuration validated successfully",
|
||||
callback: func() (string, error) {
|
||||
if err := server.Validate(); err != nil {
|
||||
return "", fmt.Errorf("validation failed: %v", err)
|
||||
}
|
||||
return "Server configuration validated successfully", nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepDirectoryCreation,
|
||||
important: true,
|
||||
description: "Server directories prepared",
|
||||
callback: func() (string, error) {
|
||||
return "Server directories prepared", nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepSteamDownload,
|
||||
important: true,
|
||||
description: "Server files downloaded successfully",
|
||||
callback: func() (string, error) {
|
||||
if err := s.steamService.InstallServerWithWebSocket(ctx, server.Path, &server.ID, s.webSocketService); err != nil {
|
||||
return "", fmt.Errorf("failed to install server: %v", err)
|
||||
}
|
||||
return "Server files downloaded successfully", nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepConfigGeneration,
|
||||
important: true,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find available ports: %v", err)
|
||||
}
|
||||
|
||||
serverPort = ports[0]
|
||||
|
||||
if err := s.updateServerPort(server, serverPort); err != nil {
|
||||
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepServiceCreation,
|
||||
important: true,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
|
||||
serverWorkingDir := filepath.Join(server.GetServerPath(), "server")
|
||||
if err := s.windowsService.CreateService(ctx, server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to create Windows service: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Windows service '%s' created successfully", server.ServiceName), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepFirewallRules,
|
||||
important: false,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
s.configureFirewall(server)
|
||||
tcpPorts = []int{serverPort}
|
||||
udpPorts = []int{serverPort}
|
||||
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
return "", fmt.Errorf("failed to create firewall rules: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Firewall rules created for port %d", serverPort), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
stepType: model.StepDatabaseSave,
|
||||
important: true,
|
||||
description: "Server saved to database successfully",
|
||||
callback: func() (string, error) {
|
||||
if err := s.repository.Insert(ctx, server); err != nil {
|
||||
return "", fmt.Errorf("failed to insert server into database: %v", err)
|
||||
}
|
||||
return "Server saved to database successfully", nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusCompleted,
|
||||
"Server configuration validated successfully", "")
|
||||
for i, step := range steps {
|
||||
s.webSocketService.BroadcastStep(server.ID, step.stepType, model.StatusInProgress,
|
||||
model.GetStepDescription(step.stepType), "")
|
||||
|
||||
// Broadcast step: directory creation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDirectoryCreation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepDirectoryCreation), "")
|
||||
successMessage, err := step.callback()
|
||||
if err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, step.stepType, model.StatusFailed,
|
||||
"", err.Error())
|
||||
|
||||
// Directory creation is handled within InstallServer, so we mark it as completed
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDirectoryCreation, model.StatusCompleted,
|
||||
"Server directories prepared", "")
|
||||
if step.important {
|
||||
s.rollbackSteps(ctx, server, steps[:i], tcpPorts, udpPorts)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast step: Steam download
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepSteamDownload), "")
|
||||
|
||||
// Install server using SteamCMD with streaming support
|
||||
if err := s.steamService.InstallServerWithWebSocket(ctx.UserContext(), server.Path, &server.ID, s.webSocketService); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusFailed,
|
||||
"", fmt.Sprintf("Steam installation failed: %v", err))
|
||||
return fmt.Errorf("failed to install server: %v", err)
|
||||
s.webSocketService.BroadcastStep(server.ID, step.stepType, model.StatusCompleted,
|
||||
successMessage, "")
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusCompleted,
|
||||
"Server files downloaded successfully", "")
|
||||
|
||||
// Broadcast step: config generation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepConfigGeneration), "")
|
||||
|
||||
// Find available ports for server
|
||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||
if err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to find available ports: %v", err))
|
||||
return fmt.Errorf("failed to find available ports: %v", err)
|
||||
}
|
||||
|
||||
// Use the first port for both TCP and UDP
|
||||
serverPort := ports[0]
|
||||
|
||||
// Update server configuration with the allocated port
|
||||
if err := s.updateServerPort(server, serverPort); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to update server configuration: %v", err))
|
||||
return fmt.Errorf("failed to update server configuration: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusCompleted,
|
||||
fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), "")
|
||||
|
||||
// Broadcast step: service creation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepServiceCreation), "")
|
||||
|
||||
// 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 {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to create Windows service: %v", err))
|
||||
// Cleanup on failure
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to create Windows service: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusCompleted,
|
||||
fmt.Sprintf("Windows service '%s' created successfully", server.ServiceName), "")
|
||||
|
||||
// Broadcast step: firewall rules
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepFirewallRules), "")
|
||||
|
||||
s.configureFirewall(server)
|
||||
tcpPorts := []int{serverPort}
|
||||
udpPorts := []int{serverPort}
|
||||
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to create firewall rules: %v", err))
|
||||
// Cleanup on failure
|
||||
s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName)
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to create firewall rules: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusCompleted,
|
||||
fmt.Sprintf("Firewall rules created for port %d", serverPort), "")
|
||||
|
||||
// Broadcast step: database save
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepDatabaseSave), "")
|
||||
|
||||
// Insert server into database
|
||||
if err := s.repository.Insert(ctx.UserContext(), server); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to save server to database: %v", err))
|
||||
// Cleanup on failure
|
||||
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||
s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName)
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to insert server into database: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusCompleted,
|
||||
"Server saved to database successfully", "")
|
||||
|
||||
// Initialize server runtime
|
||||
s.StartAccServerRuntime(server)
|
||||
|
||||
// Broadcast completion
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepCompleted, model.StatusCompleted,
|
||||
model.GetStepDescription(model.StepCompleted), "")
|
||||
|
||||
@@ -490,150 +461,34 @@ func (s *ServerService) CreateServer(ctx *fiber.Ctx, server *model.Server) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// createServerBackground performs server creation in background without fiber.Ctx
|
||||
func (s *ServerService) createServerBackground(ctx context.Context, server *model.Server) error {
|
||||
// Broadcast step: validation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepValidation), "")
|
||||
|
||||
// Validate basic server configuration (already done in async method, but double-check)
|
||||
if err := server.Validate(); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusFailed,
|
||||
"", fmt.Sprintf("Validation failed: %v", err))
|
||||
return err
|
||||
func (s *ServerService) rollbackSteps(ctx context.Context, server *model.Server, completedSteps []createServerStep, tcpPorts, udpPorts []int) {
|
||||
for i := len(completedSteps) - 1; i >= 0; i-- {
|
||||
step := completedSteps[i]
|
||||
switch step.stepType {
|
||||
case model.StepDatabaseSave:
|
||||
s.repository.Delete(ctx, server.ID)
|
||||
case model.StepFirewallRules:
|
||||
if len(tcpPorts) > 0 && len(udpPorts) > 0 {
|
||||
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||
}
|
||||
case model.StepServiceCreation:
|
||||
s.windowsService.DeleteService(ctx, server.ServiceName)
|
||||
case model.StepSteamDownload:
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
}
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepValidation, model.StatusCompleted,
|
||||
"Server configuration validated successfully", "")
|
||||
|
||||
// Broadcast step: directory creation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDirectoryCreation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepDirectoryCreation), "")
|
||||
|
||||
// Directory creation is handled within InstallServer, so we mark it as completed
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDirectoryCreation, model.StatusCompleted,
|
||||
"Server directories prepared", "")
|
||||
|
||||
// Broadcast step: Steam download
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepSteamDownload), "")
|
||||
|
||||
// Install server using SteamCMD with streaming support
|
||||
if err := s.steamService.InstallServerWithWebSocket(ctx, server.GetServerPath(), &server.ID, s.webSocketService); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusFailed,
|
||||
"", fmt.Sprintf("Steam installation failed: %v", err))
|
||||
return fmt.Errorf("failed to install server: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepSteamDownload, model.StatusCompleted,
|
||||
"Server files downloaded successfully", "")
|
||||
|
||||
// Broadcast step: config generation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepConfigGeneration), "")
|
||||
|
||||
// Find available ports for server
|
||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||
if err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to find available ports: %v", err))
|
||||
return fmt.Errorf("failed to find available ports: %v", err)
|
||||
}
|
||||
|
||||
// Use the first port for both TCP and UDP
|
||||
serverPort := ports[0]
|
||||
|
||||
// Update server configuration with the allocated port
|
||||
if err := s.updateServerPort(server, serverPort); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to update server configuration: %v", err))
|
||||
return fmt.Errorf("failed to update server configuration: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepConfigGeneration, model.StatusCompleted,
|
||||
fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), "")
|
||||
|
||||
// Broadcast step: service creation
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepServiceCreation), "")
|
||||
|
||||
// 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, server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to create Windows service: %v", err))
|
||||
// Cleanup on failure
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to create Windows service: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepServiceCreation, model.StatusCompleted,
|
||||
fmt.Sprintf("Windows service '%s' created successfully", server.ServiceName), "")
|
||||
|
||||
// Broadcast step: firewall rules
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepFirewallRules), "")
|
||||
|
||||
s.configureFirewall(server)
|
||||
tcpPorts := []int{serverPort}
|
||||
udpPorts := []int{serverPort}
|
||||
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to create firewall rules: %v", err))
|
||||
// Cleanup on failure
|
||||
s.windowsService.DeleteService(ctx, server.ServiceName)
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to create firewall rules: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepFirewallRules, model.StatusCompleted,
|
||||
fmt.Sprintf("Firewall rules created for port %d", serverPort), "")
|
||||
|
||||
// Broadcast step: database save
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusInProgress,
|
||||
model.GetStepDescription(model.StepDatabaseSave), "")
|
||||
|
||||
// Insert server into database
|
||||
if err := s.repository.Insert(ctx, server); err != nil {
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusFailed,
|
||||
"", fmt.Sprintf("Failed to save server to database: %v", err))
|
||||
// Cleanup on failure
|
||||
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||
s.windowsService.DeleteService(ctx, server.ServiceName)
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
return fmt.Errorf("failed to insert server into database: %v", err)
|
||||
}
|
||||
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepDatabaseSave, model.StatusCompleted,
|
||||
"Server saved to database successfully", "")
|
||||
|
||||
// Initialize server runtime
|
||||
s.StartAccServerRuntime(server)
|
||||
|
||||
// Broadcast completion
|
||||
s.webSocketService.BroadcastStep(server.ID, model.StepCompleted, model.StatusCompleted,
|
||||
model.GetStepDescription(model.StepCompleted), "")
|
||||
|
||||
s.webSocketService.BroadcastComplete(server.ID, true,
|
||||
fmt.Sprintf("Server '%s' created successfully on port %d", server.Name, serverPort))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
|
||||
// Get server details
|
||||
server, err := s.repository.GetByID(ctx.UserContext(), serverID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get server details: %v", err)
|
||||
}
|
||||
|
||||
// Stop and remove Windows service
|
||||
if err := s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName); err != nil {
|
||||
logging.Error("Failed to delete Windows service: %v", err)
|
||||
}
|
||||
|
||||
// Remove firewall rules
|
||||
configuration, err := s.configService.GetConfiguration(server)
|
||||
if err != nil {
|
||||
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
|
||||
@@ -644,17 +499,14 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
|
||||
logging.Error("Failed to delete firewall rules: %v", err)
|
||||
}
|
||||
|
||||
// Uninstall server files
|
||||
if err := s.steamService.UninstallServer(server.Path); err != nil {
|
||||
logging.Error("Failed to uninstall server: %v", err)
|
||||
}
|
||||
|
||||
// Remove from database
|
||||
if err := s.repository.Delete(ctx.UserContext(), serverID); err != nil {
|
||||
return fmt.Errorf("failed to delete server from database: %v", err)
|
||||
}
|
||||
|
||||
// Cleanup runtime resources
|
||||
if tailer, exists := s.logTailers.Load(server.ID); exists {
|
||||
tailer.(*tracking.LogTailer).Stop()
|
||||
s.logTailers.Delete(server.ID)
|
||||
@@ -664,84 +516,27 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
|
||||
s.debouncers.Delete(server.ID)
|
||||
s.sessionIDs.Delete(server.ID)
|
||||
|
||||
// Invalidate status cache for deleted server
|
||||
s.apiService.statusCache.InvalidateStatus(server.ServiceName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateServer(ctx *fiber.Ctx, server *model.Server) error {
|
||||
// Validate server configuration
|
||||
if err := server.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get existing server details
|
||||
existingServer, err := s.repository.GetByID(ctx.UserContext(), server.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get existing server details: %v", err)
|
||||
}
|
||||
|
||||
// Update server files if path changed
|
||||
if existingServer.Path != server.Path {
|
||||
if err := s.steamService.InstallServer(ctx.UserContext(), server.Path, &server.ID); err != nil {
|
||||
return fmt.Errorf("failed to install server to new location: %v", err)
|
||||
}
|
||||
// Clean up old installation
|
||||
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.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 {
|
||||
if err := s.configureFirewall(server); err != nil {
|
||||
return fmt.Errorf("failed to update firewall rules: %v", err)
|
||||
}
|
||||
// Invalidate cache for old service name
|
||||
s.apiService.statusCache.InvalidateStatus(existingServer.ServiceName)
|
||||
}
|
||||
|
||||
// Update database record
|
||||
if err := s.repository.Update(ctx.UserContext(), server); err != nil {
|
||||
return fmt.Errorf("failed to update server in database: %v", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -750,7 +545,6 @@ func (s *ServerService) configureFirewall(server *model.Server) error {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -759,7 +553,6 @@ func (s *ServerService) updateServerPort(server *model.Server, port int) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,12 @@ import (
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// InitializeServices
|
||||
// Initializes Dependency Injection modules for services
|
||||
//
|
||||
// Args:
|
||||
// *dig.Container: Dig Container
|
||||
// *dig.Container: Dig Container
|
||||
func InitializeServices(c *dig.Container) {
|
||||
logging.Debug("Initializing repositories")
|
||||
repository.InitializeRepositories(c)
|
||||
|
||||
logging.Debug("Registering services")
|
||||
// Provide services
|
||||
c.Provide(NewSteamService)
|
||||
c.Provide(NewServerService)
|
||||
c.Provide(NewStateHistoryService)
|
||||
|
||||
@@ -24,9 +24,9 @@ func NewServiceControlService(repository *repository.ServiceControlRepository,
|
||||
repository: repository,
|
||||
serverRepository: serverRepository,
|
||||
statusCache: model.NewServerStatusCache(model.CacheConfig{
|
||||
ExpirationTime: 30 * time.Second, // Cache expires after 30 seconds
|
||||
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
|
||||
DefaultStatus: model.StatusRunning, // Default to running if throttled
|
||||
ExpirationTime: 30 * time.Second,
|
||||
ThrottleTime: 5 * time.Second,
|
||||
DefaultStatus: model.StatusRunning,
|
||||
}),
|
||||
windowsService: NewWindowsService(),
|
||||
}
|
||||
@@ -42,18 +42,15 @@ func (as *ServiceControlService) GetStatus(ctx *fiber.Ctx) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try to get status from cache
|
||||
if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck {
|
||||
return status.String(), nil
|
||||
}
|
||||
|
||||
// If cache miss or expired, check actual status
|
||||
statusStr, err := as.StatusServer(serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse and update cache with new status
|
||||
status := model.ParseServiceStatus(statusStr)
|
||||
as.statusCache.UpdateStatus(serviceName, status)
|
||||
return status.String(), nil
|
||||
@@ -65,7 +62,6 @@ func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (stri
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update status cache for this service before starting
|
||||
as.statusCache.UpdateStatus(serviceName, model.StatusStarting)
|
||||
|
||||
_, err = as.StartServer(serviceName)
|
||||
@@ -77,7 +73,6 @@ func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (stri
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse and update cache with new status
|
||||
status := model.ParseServiceStatus(statusStr)
|
||||
as.statusCache.UpdateStatus(serviceName, status)
|
||||
return status.String(), nil
|
||||
@@ -89,7 +84,6 @@ func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update status cache for this service before stopping
|
||||
as.statusCache.UpdateStatus(serviceName, model.StatusStopping)
|
||||
|
||||
_, err = as.StopServer(serviceName)
|
||||
@@ -101,7 +95,6 @@ func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse and update cache with new status
|
||||
status := model.ParseServiceStatus(statusStr)
|
||||
as.statusCache.UpdateStatus(serviceName, status)
|
||||
return status.String(), nil
|
||||
@@ -113,7 +106,6 @@ func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (st
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update status cache for this service before restarting
|
||||
as.statusCache.UpdateStatus(serviceName, model.StatusRestarting)
|
||||
|
||||
_, err = as.RestartServer(serviceName)
|
||||
@@ -125,7 +117,6 @@ func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (st
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse and update cache with new status
|
||||
status := model.ParseServiceStatus(statusStr)
|
||||
as.statusCache.UpdateStatus(serviceName, status)
|
||||
return status.String(), nil
|
||||
@@ -135,20 +126,16 @@ func (as *ServiceControlService) StatusServer(serviceName string) (string, error
|
||||
return as.windowsService.Status(context.Background(), serviceName)
|
||||
}
|
||||
|
||||
// GetCachedStatus gets the cached status for a service name without requiring fiber context
|
||||
func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, error) {
|
||||
// Try to get status from cache
|
||||
if status, shouldCheck := as.statusCache.GetStatus(serviceName); !shouldCheck {
|
||||
return status.String(), nil
|
||||
}
|
||||
|
||||
// If cache miss or expired, check actual status
|
||||
statusStr, err := as.StatusServer(serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse and update cache with new status
|
||||
status := model.ParseServiceStatus(statusStr)
|
||||
as.statusCache.UpdateStatus(serviceName, status)
|
||||
return status.String(), nil
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type ServiceManager struct {
|
||||
executor *command.CommandExecutor
|
||||
executor *command.CommandExecutor
|
||||
psExecutor *command.CommandExecutor
|
||||
}
|
||||
|
||||
@@ -24,17 +24,14 @@ func NewServiceManager() *ServiceManager {
|
||||
}
|
||||
|
||||
func (s *ServiceManager) ManageService(serviceName, action string) (string, error) {
|
||||
// Run NSSM command through PowerShell to ensure elevation
|
||||
output, err := s.psExecutor.ExecuteWithOutput("-nologo", "-noprofile", ".\\nssm", action, serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Clean up output by removing null bytes and trimming whitespace
|
||||
cleaned := strings.TrimSpace(strings.ReplaceAll(output, "\x00", ""))
|
||||
// Remove \r\n from status strings
|
||||
cleaned = strings.TrimSuffix(cleaned, "\r\n")
|
||||
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
@@ -51,11 +48,9 @@ func (s *ServiceManager) Stop(serviceName string) (string, error) {
|
||||
}
|
||||
|
||||
func (s *ServiceManager) Restart(serviceName string) (string, error) {
|
||||
// First stop the service
|
||||
if _, err := s.Stop(serviceName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Then start it again
|
||||
return s.Start(serviceName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
|
||||
eg, gCtx := errgroup.WithContext(ctx.UserContext())
|
||||
|
||||
// Get Summary Stats (Peak/Avg Players, Total Sessions)
|
||||
eg.Go(func() error {
|
||||
summary, err := s.repository.GetSummaryStats(gCtx, filter)
|
||||
if err != nil {
|
||||
@@ -61,7 +60,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
return nil
|
||||
})
|
||||
|
||||
// Get Total Playtime
|
||||
eg.Go(func() error {
|
||||
playtime, err := s.repository.GetTotalPlaytime(gCtx, filter)
|
||||
if err != nil {
|
||||
@@ -74,7 +72,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
return nil
|
||||
})
|
||||
|
||||
// Get Player Count Over Time
|
||||
eg.Go(func() error {
|
||||
playerCount, err := s.repository.GetPlayerCountOverTime(gCtx, filter)
|
||||
if err != nil {
|
||||
@@ -87,7 +84,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
return nil
|
||||
})
|
||||
|
||||
// Get Session Types
|
||||
eg.Go(func() error {
|
||||
sessionTypes, err := s.repository.GetSessionTypes(gCtx, filter)
|
||||
if err != nil {
|
||||
@@ -100,7 +96,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
return nil
|
||||
})
|
||||
|
||||
// Get Daily Activity
|
||||
eg.Go(func() error {
|
||||
dailyActivity, err := s.repository.GetDailyActivity(gCtx, filter)
|
||||
if err != nil {
|
||||
@@ -113,7 +108,6 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
|
||||
return nil
|
||||
})
|
||||
|
||||
// Get Recent Sessions
|
||||
eg.Go(func() error {
|
||||
recentSessions, err := s.repository.GetRecentSessions(gCtx, filter)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,12 +22,11 @@ const (
|
||||
)
|
||||
|
||||
type SteamService struct {
|
||||
executor *command.CommandExecutor
|
||||
interactiveExecutor *command.InteractiveCommandExecutor
|
||||
repository *repository.SteamCredentialsRepository
|
||||
tfaManager *model.Steam2FAManager
|
||||
pathValidator *security.PathValidator
|
||||
downloadVerifier *security.DownloadVerifier
|
||||
executor *command.CommandExecutor
|
||||
repository *repository.SteamCredentialsRepository
|
||||
tfaManager *model.Steam2FAManager
|
||||
pathValidator *security.PathValidator
|
||||
downloadVerifier *security.DownloadVerifier
|
||||
}
|
||||
|
||||
func NewSteamService(repository *repository.SteamCredentialsRepository, tfaManager *model.Steam2FAManager) *SteamService {
|
||||
@@ -37,12 +36,11 @@ func NewSteamService(repository *repository.SteamCredentialsRepository, tfaManag
|
||||
}
|
||||
|
||||
return &SteamService{
|
||||
executor: baseExecutor,
|
||||
interactiveExecutor: command.NewInteractiveCommandExecutor(baseExecutor, tfaManager),
|
||||
repository: repository,
|
||||
tfaManager: tfaManager,
|
||||
pathValidator: security.NewPathValidator(),
|
||||
downloadVerifier: security.NewDownloadVerifier(),
|
||||
executor: baseExecutor,
|
||||
repository: repository,
|
||||
tfaManager: tfaManager,
|
||||
pathValidator: security.NewPathValidator(),
|
||||
downloadVerifier: security.NewDownloadVerifier(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,21 +56,17 @@ func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCr
|
||||
}
|
||||
|
||||
func (s *SteamService) ensureSteamCMD(_ context.Context) error {
|
||||
// Get SteamCMD path from environment variable
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
steamCMDDir := filepath.Dir(steamCMDPath)
|
||||
|
||||
// Check if SteamCMD exists
|
||||
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 securely
|
||||
logging.Info("Downloading SteamCMD...")
|
||||
steamCMDZip := filepath.Join(steamCMDDir, "steamcmd.zip")
|
||||
if err := s.downloadVerifier.VerifyAndDownload(
|
||||
@@ -82,150 +76,27 @@ func (s *SteamService) ensureSteamCMD(_ context.Context) error {
|
||||
return fmt.Errorf("failed to download SteamCMD: %v", err)
|
||||
}
|
||||
|
||||
// Extract SteamCMD
|
||||
logging.Info("Extracting SteamCMD...")
|
||||
if err := s.executor.Execute("-Command",
|
||||
fmt.Sprintf("Expand-Archive -Path 'steamcmd.zip' -DestinationPath '%s'", steamCMDDir)); err != nil {
|
||||
return fmt.Errorf("failed to extract SteamCMD: %v", err)
|
||||
}
|
||||
|
||||
// Clean up zip file
|
||||
os.Remove("steamcmd.zip")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SteamService) InstallServer(ctx context.Context, installPath string, serverID *uuid.UUID) error {
|
||||
if err := s.ensureSteamCMD(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate installation path for security
|
||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||
return fmt.Errorf("invalid installation path: %v", 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(absPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create install directory: %v", err)
|
||||
}
|
||||
|
||||
// Get Steam credentials
|
||||
creds, err := s.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||
}
|
||||
|
||||
// Get SteamCMD path from environment variable
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
|
||||
// Build SteamCMD command arguments
|
||||
steamCMDArgs := []string{
|
||||
"+force_install_dir", absPath,
|
||||
"+login",
|
||||
}
|
||||
|
||||
if creds != nil && creds.Username != "" {
|
||||
logging.Info("Using Steam credentials for user: %s", creds.Username)
|
||||
steamCMDArgs = append(steamCMDArgs, creds.Username)
|
||||
if creds.Password != "" {
|
||||
steamCMDArgs = append(steamCMDArgs, creds.Password)
|
||||
}
|
||||
} else {
|
||||
logging.Info("Using anonymous Steam login")
|
||||
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
||||
}
|
||||
|
||||
steamCMDArgs = append(steamCMDArgs,
|
||||
"+app_update", ACCServerAppID,
|
||||
"validate",
|
||||
"+quit",
|
||||
)
|
||||
|
||||
// Execute SteamCMD directly without PowerShell wrapper to get better output capture
|
||||
args := steamCMDArgs
|
||||
|
||||
// Use interactive executor to handle potential 2FA prompts with timeout
|
||||
logging.Info("Installing ACC server to %s...", absPath)
|
||||
logging.Info("SteamCMD command: %s %s", steamCMDPath, strings.Join(args, " "))
|
||||
|
||||
// Create a context with timeout to prevent hanging indefinitely
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute) // Increased timeout
|
||||
defer cancel()
|
||||
|
||||
// Update the executor to use SteamCMD directly
|
||||
originalExePath := s.interactiveExecutor.ExePath
|
||||
s.interactiveExecutor.ExePath = steamCMDPath
|
||||
defer func() {
|
||||
s.interactiveExecutor.ExePath = originalExePath
|
||||
}()
|
||||
|
||||
if err := s.interactiveExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
|
||||
logging.Error("SteamCMD execution failed: %v", err)
|
||||
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||
return fmt.Errorf("SteamCMD operation timed out after 15 minutes - this usually means Steam Guard confirmation is required")
|
||||
}
|
||||
return fmt.Errorf("failed to run SteamCMD: %v", err)
|
||||
}
|
||||
|
||||
logging.Info("SteamCMD execution completed successfully, proceeding with verification...")
|
||||
|
||||
// Add a delay to allow Steam to properly cleanup
|
||||
logging.Info("Waiting for Steam operations to complete...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Verify installation
|
||||
exePath := filepath.Join(absPath, "server", "accServer.exe")
|
||||
logging.Info("Checking for ACC server executable at: %s", exePath)
|
||||
|
||||
if _, err := os.Stat(exePath); os.IsNotExist(err) {
|
||||
// Log directory contents to help debug
|
||||
logging.Info("accServer.exe not found, checking directory contents...")
|
||||
if entries, dirErr := os.ReadDir(absPath); dirErr == nil {
|
||||
logging.Info("Contents of %s:", absPath)
|
||||
for _, entry := range entries {
|
||||
logging.Info(" - %s (dir: %v)", entry.Name(), entry.IsDir())
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a server subdirectory
|
||||
serverDir := filepath.Join(absPath, "server")
|
||||
if entries, dirErr := os.ReadDir(serverDir); dirErr == nil {
|
||||
logging.Info("Contents of %s:", serverDir)
|
||||
for _, entry := range entries {
|
||||
logging.Info(" - %s (dir: %v)", entry.Name(), entry.IsDir())
|
||||
}
|
||||
} else {
|
||||
logging.Info("Server directory %s does not exist or cannot be read: %v", serverDir, dirErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("server installation failed: accServer.exe not found in %s", exePath)
|
||||
}
|
||||
|
||||
logging.Info("Server installation completed successfully - accServer.exe found at %s", exePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallServerWithWebSocket installs a server with WebSocket output streaming
|
||||
func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPath string, serverID *uuid.UUID, wsService *WebSocketService) error {
|
||||
if err := s.ensureSteamCMD(ctx); err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Error ensuring SteamCMD: %v", err), true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate installation path for security
|
||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||
return fmt.Errorf("invalid installation path: %v", err)
|
||||
}
|
||||
|
||||
// Convert to absolute path and ensure proper Windows path format
|
||||
absPath, err := filepath.Abs(installPath)
|
||||
if err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Failed to get absolute path: %v", err), true)
|
||||
@@ -233,7 +104,6 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
}
|
||||
absPath = filepath.Clean(absPath)
|
||||
|
||||
// Ensure install path exists
|
||||
if err := os.MkdirAll(absPath, 0755); err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Failed to create install directory: %v", err), true)
|
||||
return fmt.Errorf("failed to create install directory: %v", err)
|
||||
@@ -241,17 +111,14 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Installation directory prepared: %s", absPath), false)
|
||||
|
||||
// Get Steam credentials
|
||||
creds, err := s.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Failed to get Steam credentials: %v", err), true)
|
||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||
}
|
||||
|
||||
// Get SteamCMD path from environment variable
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
|
||||
// Build SteamCMD command arguments
|
||||
steamCMDArgs := []string{
|
||||
"+force_install_dir", absPath,
|
||||
"+login",
|
||||
@@ -274,27 +141,32 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
"+quit",
|
||||
)
|
||||
|
||||
// Execute SteamCMD with WebSocket output streaming
|
||||
args := steamCMDArgs
|
||||
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Starting SteamCMD: %s %s", steamCMDPath, strings.Join(args, " ")), false)
|
||||
|
||||
// Create a context with timeout to prevent hanging indefinitely
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Update the executor to use SteamCMD directly
|
||||
originalExePath := s.interactiveExecutor.ExePath
|
||||
s.interactiveExecutor.ExePath = steamCMDPath
|
||||
defer func() {
|
||||
s.interactiveExecutor.ExePath = originalExePath
|
||||
}()
|
||||
callbackConfig := &command.CallbackConfig{
|
||||
OnOutput: func(serverID uuid.UUID, output string, isError bool) {
|
||||
wsService.BroadcastSteamOutput(serverID, output, isError)
|
||||
},
|
||||
OnCommand: func(serverID uuid.UUID, command string, args []string, completed bool, success bool, error string) {
|
||||
if completed {
|
||||
if success {
|
||||
wsService.BroadcastSteamOutput(serverID, "Command completed successfully", false)
|
||||
} else {
|
||||
wsService.BroadcastSteamOutput(serverID, fmt.Sprintf("Command failed: %s", error), true)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Create a modified interactive executor that streams output to WebSocket
|
||||
wsInteractiveExecutor := command.NewInteractiveCommandExecutorWithWebSocket(s.executor, s.tfaManager, wsService, *serverID)
|
||||
wsInteractiveExecutor.ExePath = steamCMDPath
|
||||
callbackInteractiveExecutor := command.NewCallbackInteractiveCommandExecutor(s.executor, s.tfaManager, callbackConfig, *serverID)
|
||||
callbackInteractiveExecutor.ExePath = steamCMDPath
|
||||
|
||||
if err := wsInteractiveExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
|
||||
if err := callbackInteractiveExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("SteamCMD execution failed: %v", err), true)
|
||||
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||
return fmt.Errorf("SteamCMD operation timed out after 15 minutes - this usually means Steam Guard confirmation is required")
|
||||
@@ -304,11 +176,9 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
|
||||
wsService.BroadcastSteamOutput(*serverID, "SteamCMD execution completed successfully, proceeding with verification...", false)
|
||||
|
||||
// Add a delay to allow Steam to properly cleanup
|
||||
wsService.BroadcastSteamOutput(*serverID, "Waiting for Steam operations to complete...", false)
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Verify installation
|
||||
exePath := filepath.Join(absPath, "server", "accServer.exe")
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Checking for ACC server executable at: %s", exePath), false)
|
||||
|
||||
@@ -322,7 +192,6 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a server subdirectory
|
||||
serverDir := filepath.Join(absPath, "server")
|
||||
if entries, dirErr := os.ReadDir(serverDir); dirErr == nil {
|
||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Contents of %s:", serverDir), false)
|
||||
@@ -341,8 +210,117 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SteamService) UpdateServer(ctx context.Context, installPath string, serverID *uuid.UUID) error {
|
||||
return s.InstallServer(ctx, installPath, serverID) // Same process as install
|
||||
func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPath string, serverID *uuid.UUID, outputCallback command.OutputCallback) error {
|
||||
if err := s.ensureSteamCMD(ctx); err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Error ensuring SteamCMD: %v", err), true)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||
return fmt.Errorf("invalid installation path: %v", err)
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(installPath)
|
||||
if err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Failed to get absolute path: %v", err), true)
|
||||
return fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
absPath = filepath.Clean(absPath)
|
||||
|
||||
if err := os.MkdirAll(absPath, 0755); err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Failed to create install directory: %v", err), true)
|
||||
return fmt.Errorf("failed to create install directory: %v", err)
|
||||
}
|
||||
|
||||
outputCallback(*serverID, fmt.Sprintf("Installation directory prepared: %s", absPath), false)
|
||||
|
||||
creds, err := s.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Failed to get Steam credentials: %v", err), true)
|
||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||
}
|
||||
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
|
||||
steamCMDArgs := []string{
|
||||
"+force_install_dir", absPath,
|
||||
"+login",
|
||||
}
|
||||
|
||||
if creds != nil && creds.Username != "" {
|
||||
outputCallback(*serverID, fmt.Sprintf("Using Steam credentials for user: %s", creds.Username), false)
|
||||
steamCMDArgs = append(steamCMDArgs, creds.Username)
|
||||
if creds.Password != "" {
|
||||
steamCMDArgs = append(steamCMDArgs, creds.Password)
|
||||
}
|
||||
} else {
|
||||
outputCallback(*serverID, "Using anonymous Steam login", false)
|
||||
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
||||
}
|
||||
|
||||
steamCMDArgs = append(steamCMDArgs,
|
||||
"+app_update", ACCServerAppID,
|
||||
"validate",
|
||||
"+quit",
|
||||
)
|
||||
|
||||
args := steamCMDArgs
|
||||
|
||||
outputCallback(*serverID, fmt.Sprintf("Starting SteamCMD: %s %s", steamCMDPath, strings.Join(args, " ")), false)
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
callbacks := &command.CallbackConfig{
|
||||
OnOutput: outputCallback,
|
||||
}
|
||||
|
||||
callbackExecutor := command.NewCallbackInteractiveCommandExecutor(s.executor, s.tfaManager, callbacks, *serverID)
|
||||
callbackExecutor.ExePath = steamCMDPath
|
||||
|
||||
if err := callbackExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("SteamCMD execution failed: %v", err), true)
|
||||
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||
return fmt.Errorf("SteamCMD operation timed out after 15 minutes - this usually means Steam Guard confirmation is required")
|
||||
}
|
||||
return fmt.Errorf("failed to run SteamCMD: %v", err)
|
||||
}
|
||||
|
||||
outputCallback(*serverID, "SteamCMD execution completed successfully, proceeding with verification...", false)
|
||||
|
||||
outputCallback(*serverID, "Waiting for Steam operations to complete...", false)
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
exePath := filepath.Join(absPath, "server", "accServer.exe")
|
||||
outputCallback(*serverID, fmt.Sprintf("Checking for ACC server executable at: %s", exePath), false)
|
||||
|
||||
if _, err := os.Stat(exePath); os.IsNotExist(err) {
|
||||
outputCallback(*serverID, "accServer.exe not found, checking directory contents...", false)
|
||||
|
||||
if entries, dirErr := os.ReadDir(absPath); dirErr == nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Contents of %s:", absPath), false)
|
||||
for _, entry := range entries {
|
||||
outputCallback(*serverID, fmt.Sprintf(" - %s (dir: %v)", entry.Name(), entry.IsDir()), false)
|
||||
}
|
||||
}
|
||||
|
||||
serverDir := filepath.Join(absPath, "server")
|
||||
if entries, dirErr := os.ReadDir(serverDir); dirErr == nil {
|
||||
outputCallback(*serverID, fmt.Sprintf("Contents of %s:", serverDir), false)
|
||||
for _, entry := range entries {
|
||||
outputCallback(*serverID, fmt.Sprintf(" - %s (dir: %v)", entry.Name(), entry.IsDir()), false)
|
||||
}
|
||||
} else {
|
||||
outputCallback(*serverID, fmt.Sprintf("Server directory %s does not exist or cannot be read: %v", serverDir, dirErr), true)
|
||||
}
|
||||
|
||||
outputCallback(*serverID, fmt.Sprintf("Server installation failed: accServer.exe not found in %s", exePath), true)
|
||||
return fmt.Errorf("server installation failed: accServer.exe not found in %s", exePath)
|
||||
}
|
||||
|
||||
outputCallback(*serverID, fmt.Sprintf("Server installation completed successfully - accServer.exe found at %s", exePath), false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SteamService) UninstallServer(installPath string) error {
|
||||
|
||||
@@ -11,25 +11,21 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// WebSocketConnection represents a single WebSocket connection
|
||||
type WebSocketConnection struct {
|
||||
conn *websocket.Conn
|
||||
serverID *uuid.UUID // If connected to a specific server creation process
|
||||
userID *uuid.UUID // User who owns this connection
|
||||
serverID *uuid.UUID
|
||||
userID *uuid.UUID
|
||||
}
|
||||
|
||||
// WebSocketService manages WebSocket connections and message broadcasting
|
||||
type WebSocketService struct {
|
||||
connections sync.Map // map[string]*WebSocketConnection - key is connection ID
|
||||
connections sync.Map
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWebSocketService creates a new WebSocket service
|
||||
func NewWebSocketService() *WebSocketService {
|
||||
return &WebSocketService{}
|
||||
}
|
||||
|
||||
// AddConnection adds a new WebSocket connection
|
||||
func (ws *WebSocketService) AddConnection(connID string, conn *websocket.Conn, userID *uuid.UUID) {
|
||||
wsConn := &WebSocketConnection{
|
||||
conn: conn,
|
||||
@@ -39,7 +35,6 @@ func (ws *WebSocketService) AddConnection(connID string, conn *websocket.Conn, u
|
||||
logging.Info("WebSocket connection added: %s for user: %v", connID, userID)
|
||||
}
|
||||
|
||||
// RemoveConnection removes a WebSocket connection
|
||||
func (ws *WebSocketService) RemoveConnection(connID string) {
|
||||
if conn, exists := ws.connections.LoadAndDelete(connID); exists {
|
||||
if wsConn, ok := conn.(*WebSocketConnection); ok {
|
||||
@@ -49,7 +44,6 @@ func (ws *WebSocketService) RemoveConnection(connID string) {
|
||||
logging.Info("WebSocket connection removed: %s", connID)
|
||||
}
|
||||
|
||||
// SetServerID associates a connection with a specific server creation process
|
||||
func (ws *WebSocketService) SetServerID(connID string, serverID uuid.UUID) {
|
||||
if conn, exists := ws.connections.Load(connID); exists {
|
||||
if wsConn, ok := conn.(*WebSocketConnection); ok {
|
||||
@@ -58,7 +52,6 @@ func (ws *WebSocketService) SetServerID(connID string, serverID uuid.UUID) {
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastStep sends a step update to all connections associated with a server
|
||||
func (ws *WebSocketService) BroadcastStep(serverID uuid.UUID, step model.ServerCreationStep, status model.StepStatus, message string, errorMsg string) {
|
||||
stepMsg := model.StepMessage{
|
||||
Step: step,
|
||||
@@ -77,7 +70,6 @@ func (ws *WebSocketService) BroadcastStep(serverID uuid.UUID, step model.ServerC
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
// BroadcastSteamOutput sends Steam command output to all connections associated with a server
|
||||
func (ws *WebSocketService) BroadcastSteamOutput(serverID uuid.UUID, output string, isError bool) {
|
||||
steamMsg := model.SteamOutputMessage{
|
||||
Output: output,
|
||||
@@ -94,7 +86,6 @@ func (ws *WebSocketService) BroadcastSteamOutput(serverID uuid.UUID, output stri
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
// BroadcastError sends an error message to all connections associated with a server
|
||||
func (ws *WebSocketService) BroadcastError(serverID uuid.UUID, error string, details string) {
|
||||
errorMsg := model.ErrorMessage{
|
||||
Error: error,
|
||||
@@ -111,7 +102,6 @@ func (ws *WebSocketService) BroadcastError(serverID uuid.UUID, error string, det
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
// BroadcastComplete sends a completion message to all connections associated with a server
|
||||
func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool, message string) {
|
||||
completeMsg := model.CompleteMessage{
|
||||
ServerID: serverID,
|
||||
@@ -129,7 +119,6 @@ func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool,
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
// broadcastToServer sends a message to all connections associated with a specific server
|
||||
func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.WebSocketMessage) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
@@ -137,22 +126,35 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
|
||||
return
|
||||
}
|
||||
|
||||
sentToAssociatedConnections := false
|
||||
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||
// Send to connections associated with this server
|
||||
if wsConn.serverID != nil && *wsConn.serverID == serverID {
|
||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||
// Remove the connection if it's broken
|
||||
ws.RemoveConnection(key.(string))
|
||||
} else {
|
||||
sentToAssociatedConnections = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if !sentToAssociatedConnections && (message.Type == model.MessageTypeStep || message.Type == model.MessageTypeError || message.Type == model.MessageTypeComplete) {
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||
ws.RemoveConnection(key.(string))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastToUser sends a message to all connections owned by a specific user
|
||||
func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebSocketMessage) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
@@ -162,11 +164,9 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
|
||||
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||
// Send to connections owned by this user
|
||||
if wsConn.userID != nil && *wsConn.userID == userID {
|
||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||
// Remove the connection if it's broken
|
||||
ws.RemoveConnection(key.(string))
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,6 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveConnections returns the count of active connections
|
||||
func (ws *WebSocketService) GetActiveConnections() int {
|
||||
count := 0
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
|
||||
@@ -23,34 +23,25 @@ func NewWindowsService() *WindowsService {
|
||||
}
|
||||
}
|
||||
|
||||
// executeNSSM runs an NSSM command through PowerShell with elevation
|
||||
func (s *WindowsService) ExecuteNSSM(ctx context.Context, args ...string) (string, error) {
|
||||
// Get NSSM path from environment variable
|
||||
nssmPath := env.GetNSSMPath()
|
||||
|
||||
// Prepend NSSM path to arguments
|
||||
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
|
||||
}
|
||||
|
||||
// Clean up output by removing null bytes and trimming whitespace
|
||||
cleaned := strings.TrimSpace(strings.ReplaceAll(output, "\x00", ""))
|
||||
// Remove \r\n from status strings
|
||||
cleaned = strings.TrimSuffix(cleaned, "\r\n")
|
||||
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
// Service Installation/Configuration Methods
|
||||
|
||||
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)
|
||||
@@ -63,30 +54,24 @@ func (s *WindowsService) CreateService(ctx context.Context, serviceName, execPat
|
||||
}
|
||||
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(ctx, "install", serviceName, absExecPath); err != nil {
|
||||
return fmt.Errorf("failed to install service: %v", err)
|
||||
}
|
||||
|
||||
// Set arguments if provided
|
||||
if len(args) > 0 {
|
||||
cmdArgs := append([]string{"set", serviceName, "AppParameters"}, args...)
|
||||
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)
|
||||
}
|
||||
@@ -105,17 +90,13 @@ func (s *WindowsService) DeleteService(ctx context.Context, serviceName string)
|
||||
}
|
||||
|
||||
func (s *WindowsService) UpdateService(ctx context.Context, serviceName, execPath, workingDir string, args []string) error {
|
||||
// First remove the existing service
|
||||
if err := s.DeleteService(ctx, serviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then create it again with new parameters
|
||||
return s.CreateService(ctx, serviceName, execPath, workingDir, args)
|
||||
}
|
||||
|
||||
// Service Control Methods
|
||||
|
||||
func (s *WindowsService) Status(ctx context.Context, serviceName string) (string, error) {
|
||||
return s.ExecuteNSSM(ctx, "status", serviceName)
|
||||
}
|
||||
@@ -129,11 +110,9 @@ func (s *WindowsService) Stop(ctx context.Context, serviceName string) (string,
|
||||
}
|
||||
|
||||
func (s *WindowsService) Restart(ctx context.Context, serviceName string) (string, error) {
|
||||
// First stop the service
|
||||
if _, err := s.Stop(ctx, serviceName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Then start it again
|
||||
return s.Start(ctx, serviceName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user