create and delete server initial setup
This commit is contained in:
@@ -1,12 +1,20 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BaseServerPath = "servers"
|
||||||
|
ServiceNamePrefix = "ACC-Server"
|
||||||
|
)
|
||||||
|
|
||||||
// Server represents an ACC server instance
|
// Server represents an ACC server instance
|
||||||
type Server struct {
|
type Server struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
@@ -14,9 +22,10 @@ type Server struct {
|
|||||||
Status ServiceStatus `json:"status" gorm:"-"`
|
Status ServiceStatus `json:"status" gorm:"-"`
|
||||||
IP string `gorm:"not null" json:"-"`
|
IP string `gorm:"not null" json:"-"`
|
||||||
Port int `gorm:"not null" json:"-"`
|
Port int `gorm:"not null" json:"-"`
|
||||||
ConfigPath string `gorm:"not null" json:"-"` // e.g. "/acc/servers/server1/"
|
ConfigPath string `gorm:"not null" json:"configPath"` // e.g. "/acc/servers/server1/"
|
||||||
ServiceName string `gorm:"not null" json:"-"` // Windows service name
|
ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name
|
||||||
State ServerState `gorm:"-" json:"state"`
|
State ServerState `gorm:"-" json:"state"`
|
||||||
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerState struct {
|
type PlayerState struct {
|
||||||
@@ -70,4 +79,52 @@ func (f *ServerFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is a GORM hook that runs before creating a new server
|
||||||
|
func (s *Server) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("server name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate service name and config path if not set
|
||||||
|
if s.ServiceName == "" {
|
||||||
|
s.ServiceName = s.GenerateServiceName()
|
||||||
|
}
|
||||||
|
if s.ConfigPath == "" {
|
||||||
|
s.ConfigPath = s.GenerateConfigPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set creation date if not set
|
||||||
|
if s.DateCreated.IsZero() {
|
||||||
|
s.DateCreated = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateServiceName creates a unique service name based on the server name
|
||||||
|
func (s *Server) GenerateServiceName() string {
|
||||||
|
// If ID is set, use it
|
||||||
|
if s.ID > 0 {
|
||||||
|
return fmt.Sprintf("%s-%d", ServiceNamePrefix, s.ID)
|
||||||
|
}
|
||||||
|
// Otherwise use a timestamp-based unique identifier
|
||||||
|
return fmt.Sprintf("%s-%d", ServiceNamePrefix, time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateConfigPath creates the config path based on the service name
|
||||||
|
func (s *Server) GenerateConfigPath() string {
|
||||||
|
// Ensure service name is set
|
||||||
|
if s.ServiceName == "" {
|
||||||
|
s.ServiceName = s.GenerateServiceName()
|
||||||
|
}
|
||||||
|
return filepath.Join(BaseServerPath, s.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Validate() error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("server name is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
@@ -58,5 +58,5 @@ type StateHistory struct {
|
|||||||
DateCreated time.Time `json:"dateCreated"`
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
SessionStart time.Time `json:"sessionStart"`
|
SessionStart time.Time `json:"sessionStart"`
|
||||||
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
SessionDurationMinutes int `json:"sessionDurationMinutes"`
|
||||||
SessionID uint `json:"sessionId" gorm:"not null"` // Unique identifier for each session/event
|
SessionID uint `json:"sessionId" gorm:"not null;default:0"` // Unique identifier for each session/event
|
||||||
}
|
}
|
||||||
154
local/model/steam_credentials.go
Normal file
154
local/model/steam_credentials.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SteamCredentials represents stored Steam login credentials
|
||||||
|
type SteamCredentials struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Username string `gorm:"not null" json:"username"`
|
||||||
|
Password string `gorm:"not null" json:"-"` // Encrypted, not exposed in JSON
|
||||||
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
|
LastUpdated time.Time `json:"lastUpdated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (SteamCredentials) TableName() string {
|
||||||
|
return "steam_credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is a GORM hook that runs before creating new credentials
|
||||||
|
func (s *SteamCredentials) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if s.DateCreated.IsZero() {
|
||||||
|
s.DateCreated = now
|
||||||
|
}
|
||||||
|
s.LastUpdated = now
|
||||||
|
|
||||||
|
// Encrypt password before saving
|
||||||
|
encrypted, err := EncryptPassword(s.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Password = encrypted
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate is a GORM hook that runs before updating credentials
|
||||||
|
func (s *SteamCredentials) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
s.LastUpdated = time.Now().UTC()
|
||||||
|
|
||||||
|
// Only encrypt if password field is being updated
|
||||||
|
if tx.Statement.Changed("Password") {
|
||||||
|
encrypted, err := EncryptPassword(s.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Password = encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterFind is a GORM hook that runs after fetching credentials
|
||||||
|
func (s *SteamCredentials) AfterFind(tx *gorm.DB) error {
|
||||||
|
// Decrypt password after fetching
|
||||||
|
if s.Password != "" {
|
||||||
|
decrypted, err := DecryptPassword(s.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Password = decrypted
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the credentials are valid
|
||||||
|
func (s *SteamCredentials) Validate() error {
|
||||||
|
if s.Username == "" {
|
||||||
|
return errors.New("username is required")
|
||||||
|
}
|
||||||
|
if s.Password == "" {
|
||||||
|
return errors.New("password is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptionKey returns the encryption key, in a real application this should be stored securely
|
||||||
|
// and potentially rotated periodically
|
||||||
|
func GetEncryptionKey() []byte {
|
||||||
|
// This is a placeholder - in production, this should be stored securely
|
||||||
|
// and potentially fetched from a key management service
|
||||||
|
return []byte("your-32-byte-encryption-key-here")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptPassword encrypts a password using AES-256
|
||||||
|
func EncryptPassword(password string) (string, error) {
|
||||||
|
key := GetEncryptionKey()
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new GCM cipher
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a nonce
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the password
|
||||||
|
ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil)
|
||||||
|
|
||||||
|
// Return base64 encoded encrypted password
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptPassword decrypts an encrypted password
|
||||||
|
func DecryptPassword(encryptedPassword string) (string, error) {
|
||||||
|
key := GetEncryptionKey()
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new GCM cipher
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 encoded password
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(ciphertext) < nonceSize {
|
||||||
|
return "", errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(plaintext), nil
|
||||||
|
}
|
||||||
@@ -15,4 +15,5 @@ func InitializeRepositories(c *dig.Container) {
|
|||||||
c.Provide(NewServerRepository)
|
c.Provide(NewServerRepository)
|
||||||
c.Provide(NewConfigRepository)
|
c.Provide(NewConfigRepository)
|
||||||
c.Provide(NewLookupRepository)
|
c.Provide(NewLookupRepository)
|
||||||
|
c.Provide(NewSteamCredentialsRepository)
|
||||||
}
|
}
|
||||||
|
|||||||
41
local/repository/steam_credentials.go
Normal file
41
local/repository/steam_credentials.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SteamCredentialsRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSteamCredentialsRepository(db *gorm.DB) *SteamCredentialsRepository {
|
||||||
|
return &SteamCredentialsRepository{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SteamCredentialsRepository) GetCurrent(ctx context.Context) (*model.SteamCredentials, error) {
|
||||||
|
var creds model.SteamCredentials
|
||||||
|
result := r.db.WithContext(ctx).Order("id desc").First(&creds)
|
||||||
|
if result.Error != nil {
|
||||||
|
if result.Error == gorm.ErrRecordNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SteamCredentialsRepository) Save(ctx context.Context, creds *model.SteamCredentials) error {
|
||||||
|
if creds.ID == 0 {
|
||||||
|
return r.db.WithContext(ctx).Create(creds).Error
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Save(creds).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SteamCredentialsRepository) Delete(ctx context.Context, id uint) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&model.SteamCredentials{}, id).Error
|
||||||
|
}
|
||||||
@@ -3,10 +3,8 @@ package service
|
|||||||
import (
|
import (
|
||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
"acc-server-manager/local/utl/common"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -17,6 +15,7 @@ type ApiService struct {
|
|||||||
serverRepository *repository.ServerRepository
|
serverRepository *repository.ServerRepository
|
||||||
serverService *ServerService
|
serverService *ServerService
|
||||||
statusCache *model.ServerStatusCache
|
statusCache *model.ServerStatusCache
|
||||||
|
windowsService *WindowsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiService(repository *repository.ApiRepository,
|
func NewApiService(repository *repository.ApiRepository,
|
||||||
@@ -29,6 +28,7 @@ func NewApiService(repository *repository.ApiRepository,
|
|||||||
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
|
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
|
||||||
DefaultStatus: model.StatusRunning, // Default to running if throttled
|
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) {
|
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) {
|
func (as *ApiService) StartServer(serviceName string) (string, error) {
|
||||||
status, err := ManageService(serviceName, "start")
|
status, err := as.windowsService.Start(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ func (as *ApiService) StartServer(serviceName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *ApiService) StopServer(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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func (as *ApiService) StopServer(serviceName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *ApiService) RestartServer(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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -166,20 +166,6 @@ func (as *ApiService) RestartServer(serviceName string) (string, error) {
|
|||||||
return status, err
|
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) {
|
func (as *ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) {
|
||||||
var server *model.Server
|
var server *model.Server
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
106
local/service/firewall_service.go
Normal file
106
local/service/firewall_service.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"acc-server-manager/local/utl/logging"
|
"acc-server-manager/local/utl/logging"
|
||||||
"acc-server-manager/local/utl/tracking"
|
"acc-server-manager/local/utl/tracking"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,12 +19,15 @@ type ServerService struct {
|
|||||||
repository *repository.ServerRepository
|
repository *repository.ServerRepository
|
||||||
stateHistoryRepo *repository.StateHistoryRepository
|
stateHistoryRepo *repository.StateHistoryRepository
|
||||||
apiService *ApiService
|
apiService *ApiService
|
||||||
instances sync.Map
|
instances sync.Map // Track instances per server
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
lastInsertTimes sync.Map // Track last insert time per server
|
lastInsertTimes sync.Map // Track last insert time per server
|
||||||
debouncers sync.Map // Track debounce timers per server
|
debouncers sync.Map // Track debounce timers per server
|
||||||
logTailers sync.Map // Track log tailers per server
|
logTailers sync.Map // Track log tailers per server
|
||||||
sessionIDs sync.Map // Track current session ID per server
|
sessionIDs sync.Map // Track current session ID per server
|
||||||
|
steamService *SteamService
|
||||||
|
windowsService *WindowsService
|
||||||
|
firewallService *FirewallService
|
||||||
}
|
}
|
||||||
|
|
||||||
type pendingState struct {
|
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{
|
service := &ServerService{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
apiService: apiService,
|
apiService: apiService,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
stateHistoryRepo: stateHistoryRepo,
|
stateHistoryRepo: stateHistoryRepo,
|
||||||
|
steamService: steamService,
|
||||||
|
windowsService: NewWindowsService(),
|
||||||
|
firewallService: NewFirewallService(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize instances for all servers
|
// Initialize instances for all servers
|
||||||
@@ -291,4 +300,141 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
return server, nil
|
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
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,6 @@ func InitializeServices(c *dig.Container) {
|
|||||||
c.Provide(NewApiService)
|
c.Provide(NewApiService)
|
||||||
c.Provide(NewConfigService)
|
c.Provide(NewConfigService)
|
||||||
c.Provide(NewLookupService)
|
c.Provide(NewLookupService)
|
||||||
|
|
||||||
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService) {
|
err := c.Invoke(func(server *ServerService, api *ApiService, config *ConfigService, lookup *LookupService) {
|
||||||
api.SetServerService(server)
|
api.SetServerService(server)
|
||||||
config.SetServerService(server)
|
config.SetServerService(server)
|
||||||
|
|||||||
61
local/service/service_manager.go
Normal file
61
local/service/service_manager.go
Normal 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)
|
||||||
|
}
|
||||||
125
local/service/steam_service.go
Normal file
125
local/service/steam_service.go
Normal 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)
|
||||||
|
}
|
||||||
123
local/service/windows_service.go
Normal file
123
local/service/windows_service.go
Normal 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)
|
||||||
|
}
|
||||||
103
local/utl/command/executor.go
Normal file
103
local/utl/command/executor.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandExecutor provides a base structure for executing commands
|
||||||
|
type CommandExecutor struct {
|
||||||
|
// Base executable path
|
||||||
|
ExePath string
|
||||||
|
// Working directory for commands
|
||||||
|
WorkDir string
|
||||||
|
// Whether to capture and log output
|
||||||
|
LogOutput bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandBuilder helps build command arguments
|
||||||
|
type CommandBuilder struct {
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandBuilder() *CommandBuilder {
|
||||||
|
return &CommandBuilder{
|
||||||
|
args: make([]string, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CommandBuilder) Add(arg string) *CommandBuilder {
|
||||||
|
b.args = append(b.args, arg)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CommandBuilder) AddPair(key, value string) *CommandBuilder {
|
||||||
|
b.args = append(b.args, key, value)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CommandBuilder) AddFlag(flag string, value interface{}) *CommandBuilder {
|
||||||
|
b.args = append(b.args, fmt.Sprintf("%s=%v", flag, value))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CommandBuilder) Build() []string {
|
||||||
|
return b.args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs a command with the given arguments
|
||||||
|
func (e *CommandExecutor) Execute(args ...string) error {
|
||||||
|
cmd := exec.Command(e.ExePath, args...)
|
||||||
|
|
||||||
|
if e.WorkDir != "" {
|
||||||
|
cmd.Dir = e.WorkDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.LogOutput {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Executing command: %s %s", e.ExePath, strings.Join(args, " "))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWithBuilder runs a command using a CommandBuilder
|
||||||
|
func (e *CommandExecutor) ExecuteWithBuilder(builder *CommandBuilder) error {
|
||||||
|
return e.Execute(builder.Build()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWithOutput runs a command and returns its output
|
||||||
|
func (e *CommandExecutor) ExecuteWithOutput(args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(e.ExePath, args...)
|
||||||
|
|
||||||
|
if e.WorkDir != "" {
|
||||||
|
cmd.Dir = e.WorkDir
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Executing command: %s %s", e.ExePath, strings.Join(args, " "))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
return string(output), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWithEnv runs a command with custom environment variables
|
||||||
|
func (e *CommandExecutor) ExecuteWithEnv(env []string, args ...string) error {
|
||||||
|
cmd := exec.Command(e.ExePath, args...)
|
||||||
|
|
||||||
|
if e.WorkDir != "" {
|
||||||
|
cmd.Dir = e.WorkDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.LogOutput {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
|
||||||
|
logging.Info("Executing command: %s %s", e.ExePath, strings.Join(args, " "))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -66,16 +65,6 @@ func Find[T any](lst *[]T, callback func(item *T) bool) *T {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunElevatedCommand(command string, service string) (string, error) {
|
|
||||||
cmd := exec.Command("powershell", "-nologo", "-noprofile", ".\\nssm", command, service)
|
|
||||||
// cmd := exec.Command("powershell", "-nologo", "-noprofile", "-File", "run_sc.ps1", command, service)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error: %v, output: %s", err, string(output))
|
|
||||||
}
|
|
||||||
return string(output), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IndentJson(body []byte) ([]byte, error) {
|
func IndentJson(body []byte) ([]byte, error) {
|
||||||
newBody := new([]byte)
|
newBody := new([]byte)
|
||||||
unmarshaledBody := bytes.NewBuffer(*newBody)
|
unmarshaledBody := bytes.NewBuffer(*newBody)
|
||||||
|
|||||||
Reference in New Issue
Block a user