create and delete server initial setup

This commit is contained in:
Fran Jurmanović
2025-06-01 13:43:54 +02:00
parent d08695025a
commit 8a3b11b1ef
14 changed files with 929 additions and 38 deletions

View File

@@ -3,10 +3,8 @@ package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/common"
"context"
"errors"
"strings"
"time"
"github.com/gofiber/fiber/v2"
@@ -17,6 +15,7 @@ type ApiService struct {
serverRepository *repository.ServerRepository
serverService *ServerService
statusCache *model.ServerStatusCache
windowsService *WindowsService
}
func NewApiService(repository *repository.ApiRepository,
@@ -29,6 +28,7 @@ func NewApiService(repository *repository.ApiRepository,
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
DefaultStatus: model.StatusRunning, // Default to running if throttled
}),
windowsService: NewWindowsService(),
}
}
@@ -120,11 +120,11 @@ func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) {
}
func (as *ApiService) StatusServer(serviceName string) (string, error) {
return ManageService(serviceName, "status")
return as.windowsService.Status(serviceName)
}
func (as *ApiService) StartServer(serviceName string) (string, error) {
status, err := ManageService(serviceName, "start")
status, err := as.windowsService.Start(serviceName)
if err != nil {
return "", err
}
@@ -138,7 +138,7 @@ func (as *ApiService) StartServer(serviceName string) (string, error) {
}
func (as *ApiService) StopServer(serviceName string) (string, error) {
status, err := ManageService(serviceName, "stop")
status, err := as.windowsService.Stop(serviceName)
if err != nil {
return "", err
}
@@ -153,7 +153,7 @@ func (as *ApiService) StopServer(serviceName string) (string, error) {
}
func (as *ApiService) RestartServer(serviceName string) (string, error) {
status, err := ManageService(serviceName, "restart")
status, err := as.windowsService.Restart(serviceName)
if err != nil {
return "", err
}
@@ -166,20 +166,6 @@ func (as *ApiService) RestartServer(serviceName string) (string, error) {
return status, err
}
func ManageService(serviceName string, action string) (string, error) {
output, err := common.RunElevatedCommand(action, serviceName)
if err != nil {
return "", err
}
// Clean up NSSM 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
}
func (as *ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) {
var server *model.Server
var err error

View File

@@ -0,0 +1,106 @@
package service
import (
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/logging"
"fmt"
)
type FirewallService struct {
executor *command.CommandExecutor
}
func NewFirewallService() *FirewallService {
return &FirewallService{
executor: &command.CommandExecutor{
ExePath: "netsh",
LogOutput: true,
},
}
}
func (s *FirewallService) CreateServerRules(serverName string, tcpPorts, udpPorts []int) error {
for _, port := range tcpPorts {
ruleName := fmt.Sprintf("%s-TCP-%d", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("add").
Add("rule").
AddPair("name", ruleName).
AddPair("dir", "in").
AddPair("action", "allow").
AddPair("protocol", "TCP").
AddFlag("localport", port)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to create TCP firewall rule for port %d: %v", port, err)
}
logging.Info("Created TCP firewall rule: %s", ruleName)
}
for _, port := range udpPorts {
ruleName := fmt.Sprintf("%s-UDP-%d", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("add").
Add("rule").
AddPair("name", ruleName).
AddPair("dir", "in").
AddPair("action", "allow").
AddPair("protocol", "UDP").
AddFlag("localport", port)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to create UDP firewall rule for port %d: %v", port, err)
}
logging.Info("Created UDP firewall rule: %s", ruleName)
}
return nil
}
func (s *FirewallService) DeleteServerRules(serverName string, tcpPorts, udpPorts []int) error {
for _, port := range tcpPorts {
ruleName := fmt.Sprintf("%s-TCP-%d", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("delete").
Add("rule").
AddPair("name", ruleName)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to delete TCP firewall rule for port %d: %v", port, err)
}
logging.Info("Deleted TCP firewall rule: %s", ruleName)
}
for _, port := range udpPorts {
ruleName := fmt.Sprintf("%s-UDP-%d", serverName, port)
builder := command.NewCommandBuilder().
Add("advfirewall").
Add("firewall").
Add("delete").
Add("rule").
AddPair("name", ruleName)
if err := s.executor.ExecuteWithBuilder(builder); err != nil {
return fmt.Errorf("failed to delete UDP firewall rule for port %d: %v", port, err)
}
logging.Info("Deleted UDP firewall rule: %s", ruleName)
}
return nil
}
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)
}

View File

@@ -6,6 +6,7 @@ import (
"acc-server-manager/local/utl/logging"
"acc-server-manager/local/utl/tracking"
"context"
"fmt"
"path/filepath"
"strconv"
"sync"
@@ -18,12 +19,15 @@ type ServerService struct {
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
instances sync.Map
instances sync.Map // Track instances per server
configService *ConfigService
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
}
type pendingState struct {
@@ -48,12 +52,17 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin
}()
}
func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService) *ServerService {
func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService, steamCredentialsRepo *repository.SteamCredentialsRepository) *ServerService {
steamService := NewSteamService(steamCredentialsRepo)
service := &ServerService{
repository: repository,
apiService: apiService,
configService: configService,
stateHistoryRepo: stateHistoryRepo,
steamService: steamService,
windowsService: NewWindowsService(),
firewallService: NewFirewallService(),
}
// Initialize instances for all servers
@@ -291,4 +300,141 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, e
}
return server, nil
}
func (s *ServerService) CreateServer(ctx *fiber.Ctx, server *model.Server) error {
// Validate basic server configuration
if err := server.Validate(); err != nil {
return err
}
// Install server using SteamCMD
if err := s.steamService.InstallServer(ctx.UserContext(), server.ConfigPath); err != nil {
return fmt.Errorf("failed to install server: %v", err)
}
// Create Windows service
execPath := filepath.Join(server.ConfigPath, "accServer.exe")
if err := s.windowsService.CreateService(server.ServiceName, execPath, server.ConfigPath, nil); err != nil {
// Cleanup on failure
s.steamService.UninstallServer(server.ConfigPath)
return fmt.Errorf("failed to create Windows service: %v", err)
}
// Create firewall rules
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
// Cleanup on failure
s.windowsService.DeleteService(server.ServiceName)
s.steamService.UninstallServer(server.ConfigPath)
return fmt.Errorf("failed to create firewall rules: %v", err)
}
// Insert server into database
if err := s.repository.Insert(ctx.UserContext(), server); err != nil {
// Cleanup on failure
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
s.windowsService.DeleteService(server.ServiceName)
s.steamService.UninstallServer(server.ConfigPath)
return fmt.Errorf("failed to insert server into database: %v", err)
}
// Initialize server runtime
s.StartAccServerRuntime(server)
return nil
}
func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID int) 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(server.ServiceName); err != nil {
logging.Error("Failed to delete Windows service: %v", err)
}
// Remove firewall rules
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
if err := s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
logging.Error("Failed to delete firewall rules: %v", err)
}
// Uninstall server files
if err := s.steamService.UninstallServer(server.ConfigPath); err != nil {
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)
}
s.instances.Delete(server.ID)
s.lastInsertTimes.Delete(server.ID)
s.debouncers.Delete(server.ID)
s.sessionIDs.Delete(server.ID)
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(), int(server.ID))
if err != nil {
return fmt.Errorf("failed to get existing server details: %v", err)
}
// Update server files if path changed
if existingServer.ConfigPath != server.ConfigPath {
if err := s.steamService.InstallServer(ctx.UserContext(), server.ConfigPath); err != nil {
return fmt.Errorf("failed to install server to new location: %v", err)
}
// Clean up old installation
if err := s.steamService.UninstallServer(existingServer.ConfigPath); err != nil {
logging.Error("Failed to remove old server installation: %v", err)
}
}
// Update Windows service if necessary
if existingServer.ServiceName != server.ServiceName || existingServer.ConfigPath != server.ConfigPath {
execPath := filepath.Join(server.ConfigPath, "accServer.exe")
if err := s.windowsService.UpdateService(server.ServiceName, execPath, server.ConfigPath, nil); err != nil {
return fmt.Errorf("failed to update Windows service: %v", err)
}
}
// Update firewall rules if service name changed
if existingServer.ServiceName != server.ServiceName {
tcpPorts := []int{9600} // Add all required TCP ports
udpPorts := []int{9600} // Add all required UDP ports
if err := s.firewallService.UpdateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
return fmt.Errorf("failed to update firewall rules: %v", err)
}
}
// 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
}

View File

@@ -21,7 +21,6 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewApiService)
c.Provide(NewConfigService)
c.Provide(NewLookupService)
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService) {
api.SetServerService(server)
config.SetServerService(server)

View File

@@ -0,0 +1,61 @@
package service
import (
"acc-server-manager/local/utl/command"
"strings"
)
type ServiceManager struct {
executor *command.CommandExecutor
psExecutor *command.CommandExecutor
}
func NewServiceManager() *ServiceManager {
return &ServiceManager{
executor: &command.CommandExecutor{
ExePath: "nssm",
LogOutput: true,
},
psExecutor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
}
}
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
}
func (s *ServiceManager) Status(serviceName string) (string, error) {
return s.ManageService(serviceName, "status")
}
func (s *ServiceManager) Start(serviceName string) (string, error) {
return s.ManageService(serviceName, "start")
}
func (s *ServiceManager) Stop(serviceName string) (string, error) {
return s.ManageService(serviceName, "stop")
}
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)
}

View File

@@ -0,0 +1,125 @@
package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
"os"
"path/filepath"
)
const (
SteamCMDPath = "steamcmd"
ACCServerAppID = "1430110"
)
type SteamService struct {
executor *command.CommandExecutor
repository *repository.SteamCredentialsRepository
}
func NewSteamService(repository *repository.SteamCredentialsRepository) *SteamService {
return &SteamService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
repository: repository,
}
}
func (s *SteamService) GetCredentials(ctx context.Context) (*model.SteamCredentials, error) {
return s.repository.GetCurrent(ctx)
}
func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCredentials) error {
if err := creds.Validate(); err != nil {
return err
}
return s.repository.Save(ctx, creds)
}
func (s *SteamService) ensureSteamCMD() error {
// Check if SteamCMD exists
if _, err := os.Stat(SteamCMDPath); !os.IsNotExist(err) {
return nil
}
// Download and install SteamCMD
logging.Info("Downloading SteamCMD...")
if err := s.executor.Execute("-Command",
"Invoke-WebRequest -Uri 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip' -OutFile 'steamcmd.zip'"); err != nil {
return fmt.Errorf("failed to download SteamCMD: %v", err)
}
// Extract SteamCMD
logging.Info("Extracting SteamCMD...")
if err := s.executor.Execute("-Command",
"Expand-Archive -Path 'steamcmd.zip' -DestinationPath 'steamcmd'"); 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) error {
if err := s.ensureSteamCMD(); err != nil {
return err
}
// Ensure install path exists
if err := os.MkdirAll(installPath, 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)
}
// Build SteamCMD command
args := []string{
"-nologo",
"-noprofile",
filepath.Join(SteamCMDPath, "steamcmd.exe"),
"+force_install_dir", installPath,
"+login",
}
if creds != nil && creds.Username != "" {
args = append(args, creds.Username)
if creds.Password != "" {
args = append(args, creds.Password)
}
} else {
args = append(args, "anonymous")
}
args = append(args,
"+app_update", ACCServerAppID,
"validate",
"+quit",
)
// Run SteamCMD
logging.Info("Installing ACC server to %s...", installPath)
if err := s.executor.Execute(args...); err != nil {
return fmt.Errorf("failed to install server: %v", err)
}
return nil
}
func (s *SteamService) UpdateServer(ctx context.Context, installPath string) error {
return s.InstallServer(ctx, installPath) // Same process as install
}
func (s *SteamService) UninstallServer(installPath string) error {
return os.RemoveAll(installPath)
}

View File

@@ -0,0 +1,123 @@
package service
import (
"acc-server-manager/local/utl/command"
"acc-server-manager/local/utl/logging"
"fmt"
"path/filepath"
"strings"
)
const (
NSSMPath = ".\\nssm"
)
type WindowsService struct {
executor *command.CommandExecutor
}
func NewWindowsService() *WindowsService {
return &WindowsService{
executor: &command.CommandExecutor{
ExePath: "powershell",
LogOutput: true,
},
}
}
// executeNSSM runs an NSSM command through PowerShell with elevation
func (s *WindowsService) executeNSSM(args ...string) (string, error) {
// Prepend NSSM path to arguments
nssmArgs := append([]string{"-nologo", "-noprofile", NSSMPath}, args...)
output, err := s.executor.ExecuteWithOutput(nssmArgs...)
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
}
// Service Installation/Configuration Methods
func (s *WindowsService) CreateService(serviceName, execPath, workingDir string, args []string) error {
// Ensure paths are absolute
absExecPath, err := filepath.Abs(execPath)
if err != nil {
return fmt.Errorf("failed to get absolute path for executable: %v", err)
}
absWorkingDir, err := filepath.Abs(workingDir)
if err != nil {
return fmt.Errorf("failed to get absolute path for working directory: %v", err)
}
// Install service
if _, err := s.executeNSSM("install", serviceName, absExecPath); err != nil {
return fmt.Errorf("failed to install service: %v", err)
}
// Set working directory
if _, err := s.executeNSSM("set", serviceName, "AppDirectory", absWorkingDir); err != nil {
return fmt.Errorf("failed to set working directory: %v", err)
}
// Set arguments if provided
if len(args) > 0 {
cmdArgs := append([]string{"set", serviceName, "AppParameters"}, args...)
if _, err := s.executeNSSM(cmdArgs...); err != nil {
return fmt.Errorf("failed to set arguments: %v", err)
}
}
logging.Info("Created Windows service: %s", serviceName)
return nil
}
func (s *WindowsService) DeleteService(serviceName string) error {
if _, err := s.executeNSSM("remove", serviceName, "confirm"); err != nil {
return fmt.Errorf("failed to remove service: %v", err)
}
logging.Info("Removed Windows service: %s", serviceName)
return nil
}
func (s *WindowsService) UpdateService(serviceName, execPath, workingDir string, args []string) error {
// First remove the existing service
if err := s.DeleteService(serviceName); err != nil {
return err
}
// Then create it again with new parameters
return s.CreateService(serviceName, execPath, workingDir, args)
}
// Service Control Methods
func (s *WindowsService) Status(serviceName string) (string, error) {
return s.executeNSSM("status", serviceName)
}
func (s *WindowsService) Start(serviceName string) (string, error) {
return s.executeNSSM("start", serviceName)
}
func (s *WindowsService) Stop(serviceName string) (string, error) {
return s.executeNSSM("stop", serviceName)
}
func (s *WindowsService) 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)
}