fix(backend): replace mutex with atomic state machine in ProcessManager and SteamCmdManager
- ProcessManager: atomic.Int32 state enum (idle/starting/running/stopping) eliminates race between IsRunning check and Start/Stop - SteamCmdManager: atomic.Bool with CompareAndSwap for same protection - Restart() now propagates Stop() errors instead of swallowing them
This commit is contained in:
@@ -9,10 +9,20 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"arma3-web-server/internal/models"
|
"arma3-web-server/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type procState int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
procIdle procState = 0
|
||||||
|
procStarting procState = 1
|
||||||
|
procRunning procState = 2
|
||||||
|
procStopping procState = 3
|
||||||
|
)
|
||||||
|
|
||||||
type ProcessManager struct {
|
type ProcessManager struct {
|
||||||
serverfileDir string
|
serverfileDir string
|
||||||
modsDir string
|
modsDir string
|
||||||
@@ -23,7 +33,8 @@ type ProcessManager struct {
|
|||||||
configs *ConfigManager
|
configs *ConfigManager
|
||||||
streamer *LogStreamer
|
streamer *LogStreamer
|
||||||
proc *runningProcess
|
proc *runningProcess
|
||||||
mu sync.RWMutex
|
state atomic.Int32
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type runningProcess struct {
|
type runningProcess struct {
|
||||||
@@ -54,26 +65,23 @@ func (pm *ProcessManager) ProfilesDir() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProcessManager) IsRunning() bool {
|
func (pm *ProcessManager) IsRunning() bool {
|
||||||
pm.mu.RLock()
|
return pm.state.Load() == int32(procRunning)
|
||||||
defer pm.mu.RUnlock()
|
|
||||||
return pm.proc != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProcessManager) Start() error {
|
func (pm *ProcessManager) Start() error {
|
||||||
pm.mu.Lock()
|
if !pm.state.CompareAndSwap(int32(procIdle), int32(procStarting)) {
|
||||||
if pm.proc != nil {
|
return fmt.Errorf("server already running or changing state")
|
||||||
pm.mu.Unlock()
|
|
||||||
return fmt.Errorf("server already running")
|
|
||||||
}
|
}
|
||||||
pm.mu.Unlock()
|
|
||||||
|
|
||||||
s, err := pm.settings.Load()
|
s, err := pm.settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("load settings: %w", err)
|
return fmt.Errorf("load settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WasRunning = true
|
s.WasRunning = true
|
||||||
if err := pm.settings.Save(s); err != nil {
|
if err := pm.settings.Save(s); err != nil {
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("save was_running: %w", err)
|
return fmt.Errorf("save was_running: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +98,7 @@ func (pm *ProcessManager) Start() error {
|
|||||||
parts := splitArgs(full)
|
parts := splitArgs(full)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
cancel()
|
cancel()
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("empty command after %%command%% substitution")
|
return fmt.Errorf("empty command after %%command%% substitution")
|
||||||
}
|
}
|
||||||
autoArgs := pm.buildAutoArgs(s)
|
autoArgs := pm.buildAutoArgs(s)
|
||||||
@@ -102,16 +111,19 @@ func (pm *ProcessManager) Start() error {
|
|||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("stderr pipe: %w", err)
|
return fmt.Errorf("stderr pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +131,7 @@ func (pm *ProcessManager) Start() error {
|
|||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
pm.proc = &runningProcess{cmd: cmd, cancel: cancel, exited: exited}
|
pm.proc = &runningProcess{cmd: cmd, cancel: cancel, exited: exited}
|
||||||
pm.mu.Unlock()
|
pm.mu.Unlock()
|
||||||
|
pm.state.Store(int32(procRunning))
|
||||||
|
|
||||||
go pm.streamer.Stream("server", stdout, "")
|
go pm.streamer.Stream("server", stdout, "")
|
||||||
go pm.streamer.Stream("server", stderr, "")
|
go pm.streamer.Stream("server", stderr, "")
|
||||||
@@ -128,6 +141,7 @@ func (pm *ProcessManager) Start() error {
|
|||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
pm.proc = nil
|
pm.proc = nil
|
||||||
pm.mu.Unlock()
|
pm.mu.Unlock()
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
close(exited)
|
close(exited)
|
||||||
pm.streamer.Broadcast("server", "[SERVER_PROCESS_EXITED]")
|
pm.streamer.Broadcast("server", "[SERVER_PROCESS_EXITED]")
|
||||||
}()
|
}()
|
||||||
@@ -136,11 +150,16 @@ func (pm *ProcessManager) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProcessManager) Stop() error {
|
func (pm *ProcessManager) Stop() error {
|
||||||
|
if !pm.state.CompareAndSwap(int32(procRunning), int32(procStopping)) {
|
||||||
|
return fmt.Errorf("server not running")
|
||||||
|
}
|
||||||
|
|
||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
rp := pm.proc
|
rp := pm.proc
|
||||||
pm.mu.Unlock()
|
pm.mu.Unlock()
|
||||||
|
|
||||||
if rp == nil {
|
if rp == nil {
|
||||||
|
pm.state.Store(int32(procIdle))
|
||||||
return fmt.Errorf("server not running")
|
return fmt.Errorf("server not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +176,9 @@ func (pm *ProcessManager) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProcessManager) Restart() error {
|
func (pm *ProcessManager) Restart() error {
|
||||||
_ = pm.Stop()
|
if err := pm.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("stop: %w", err)
|
||||||
|
}
|
||||||
return pm.Start()
|
return pm.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,9 +24,8 @@ const arma3AppID = "233780"
|
|||||||
type SteamCmdManager struct {
|
type SteamCmdManager struct {
|
||||||
serverfileDir string
|
serverfileDir string
|
||||||
streamer *LogStreamer
|
streamer *LogStreamer
|
||||||
mu sync.Mutex
|
running atomic.Bool
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
running bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSteamCmdManager(serverfileDir string, streamer *LogStreamer) *SteamCmdManager {
|
func NewSteamCmdManager(serverfileDir string, streamer *LogStreamer) *SteamCmdManager {
|
||||||
@@ -37,18 +36,13 @@ func NewSteamCmdManager(serverfileDir string, streamer *LogStreamer) *SteamCmdMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SteamCmdManager) IsRunning() bool {
|
func (s *SteamCmdManager) IsRunning() bool {
|
||||||
s.mu.Lock()
|
return s.running.Load()
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.running
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SteamCmdManager) UpdateGame(branch, user string) error {
|
func (s *SteamCmdManager) UpdateGame(branch, user string) error {
|
||||||
s.mu.Lock()
|
if !s.running.CompareAndSwap(false, true) {
|
||||||
if s.running {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return fmt.Errorf("steamcmd already running")
|
return fmt.Errorf("steamcmd already running")
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"+force_install_dir", s.serverfileDir,
|
"+force_install_dir", s.serverfileDir,
|
||||||
@@ -68,12 +62,9 @@ func (s *SteamCmdManager) DownloadMod(modID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SteamCmdManager) DownloadMods(modIDs []string) error {
|
func (s *SteamCmdManager) DownloadMods(modIDs []string) error {
|
||||||
s.mu.Lock()
|
if !s.running.CompareAndSwap(false, true) {
|
||||||
if s.running {
|
|
||||||
s.mu.Unlock()
|
|
||||||
return fmt.Errorf("steamcmd already running")
|
return fmt.Errorf("steamcmd already running")
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
if len(modIDs) == 0 {
|
if len(modIDs) == 0 {
|
||||||
return fmt.Errorf("no mod ids provided")
|
return fmt.Errorf("no mod ids provided")
|
||||||
@@ -104,33 +95,31 @@ func (s *SteamCmdManager) run(label string, args []string) error {
|
|||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
s.running.Store(false)
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
s.running.Store(false)
|
||||||
return fmt.Errorf("stderr pipe: %w", err)
|
return fmt.Errorf("stderr pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
|
s.running.Store(false)
|
||||||
return fmt.Errorf("start steamcmd: %w", err)
|
return fmt.Errorf("start steamcmd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.running = true
|
|
||||||
s.cancel = cancel
|
s.cancel = cancel
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
go s.streamer.Stream("steamcmd", stdout, "")
|
go s.streamer.Stream("steamcmd", stdout, "")
|
||||||
go s.streamer.Stream("steamcmd", stderr, "")
|
go s.streamer.Stream("steamcmd", stderr, "")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
s.mu.Lock()
|
s.running.Store(false)
|
||||||
s.running = false
|
|
||||||
s.cancel = nil
|
s.cancel = nil
|
||||||
s.mu.Unlock()
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.streamer.Broadcast("steamcmd", "[STEAMCMD] SUCCESS: "+label+" finished")
|
s.streamer.Broadcast("steamcmd", "[STEAMCMD] SUCCESS: "+label+" finished")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user