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"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"arma3-web-server/internal/models"
|
||||
)
|
||||
|
||||
type procState int32
|
||||
|
||||
const (
|
||||
procIdle procState = 0
|
||||
procStarting procState = 1
|
||||
procRunning procState = 2
|
||||
procStopping procState = 3
|
||||
)
|
||||
|
||||
type ProcessManager struct {
|
||||
serverfileDir string
|
||||
modsDir string
|
||||
@@ -23,7 +33,8 @@ type ProcessManager struct {
|
||||
configs *ConfigManager
|
||||
streamer *LogStreamer
|
||||
proc *runningProcess
|
||||
mu sync.RWMutex
|
||||
state atomic.Int32
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type runningProcess struct {
|
||||
@@ -54,26 +65,23 @@ func (pm *ProcessManager) ProfilesDir() string {
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) IsRunning() bool {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
return pm.proc != nil
|
||||
return pm.state.Load() == int32(procRunning)
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) Start() error {
|
||||
pm.mu.Lock()
|
||||
if pm.proc != nil {
|
||||
pm.mu.Unlock()
|
||||
return fmt.Errorf("server already running")
|
||||
if !pm.state.CompareAndSwap(int32(procIdle), int32(procStarting)) {
|
||||
return fmt.Errorf("server already running or changing state")
|
||||
}
|
||||
pm.mu.Unlock()
|
||||
|
||||
s, err := pm.settings.Load()
|
||||
if err != nil {
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("load settings: %w", err)
|
||||
}
|
||||
|
||||
s.WasRunning = true
|
||||
if err := pm.settings.Save(s); err != nil {
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("save was_running: %w", err)
|
||||
}
|
||||
|
||||
@@ -90,6 +98,7 @@ func (pm *ProcessManager) Start() error {
|
||||
parts := splitArgs(full)
|
||||
if len(parts) == 0 {
|
||||
cancel()
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("empty command after %%command%% substitution")
|
||||
}
|
||||
autoArgs := pm.buildAutoArgs(s)
|
||||
@@ -102,16 +111,19 @@ func (pm *ProcessManager) Start() error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
cancel()
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
cancel()
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
cancel()
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
@@ -119,6 +131,7 @@ func (pm *ProcessManager) Start() error {
|
||||
pm.mu.Lock()
|
||||
pm.proc = &runningProcess{cmd: cmd, cancel: cancel, exited: exited}
|
||||
pm.mu.Unlock()
|
||||
pm.state.Store(int32(procRunning))
|
||||
|
||||
go pm.streamer.Stream("server", stdout, "")
|
||||
go pm.streamer.Stream("server", stderr, "")
|
||||
@@ -128,6 +141,7 @@ func (pm *ProcessManager) Start() error {
|
||||
pm.mu.Lock()
|
||||
pm.proc = nil
|
||||
pm.mu.Unlock()
|
||||
pm.state.Store(int32(procIdle))
|
||||
close(exited)
|
||||
pm.streamer.Broadcast("server", "[SERVER_PROCESS_EXITED]")
|
||||
}()
|
||||
@@ -136,11 +150,16 @@ func (pm *ProcessManager) Start() error {
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) Stop() error {
|
||||
if !pm.state.CompareAndSwap(int32(procRunning), int32(procStopping)) {
|
||||
return fmt.Errorf("server not running")
|
||||
}
|
||||
|
||||
pm.mu.Lock()
|
||||
rp := pm.proc
|
||||
pm.mu.Unlock()
|
||||
|
||||
if rp == nil {
|
||||
pm.state.Store(int32(procIdle))
|
||||
return fmt.Errorf("server not running")
|
||||
}
|
||||
|
||||
@@ -157,7 +176,9 @@ func (pm *ProcessManager) Stop() error {
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) Restart() error {
|
||||
_ = pm.Stop()
|
||||
if err := pm.Stop(); err != nil {
|
||||
return fmt.Errorf("stop: %w", err)
|
||||
}
|
||||
return pm.Start()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user