Files
3x-ui/internal/xray/process_race_test.go
T
n0ctal abffa8f6c9 fix(xray): guard process lifecycle fields against concurrent access (#5395)
The process cmd, done and exitErr fields were written by Start/startCommand and
the waitForCommand goroutine while IsRunning/GetErr/GetResult/Stop read them
concurrently from other goroutines (the status endpoint and the check-xray
job) — a data race. Guard them with a RWMutex: writers take the write lock;
readers snapshot under the read lock and run any blocking syscall
(Wait/Signal/Kill) on the local copy without holding it. IsRunning now uses the
done channel as the exit signal instead of reading cmd.ProcessState, which
races with cmd.Wait. Adds a -race regression test.
2026-06-20 00:37:03 +02:00

61 lines
1.1 KiB
Go

package xray
import (
"errors"
"os/exec"
"sync"
"testing"
"time"
)
// TestProcessLifecycleFieldsRaceSafe drives the lifecycle fields (cmd, done,
// exitErr) the way Start/startCommand and the waitForCommand goroutine do, while
// the status getters read them concurrently. Run with -race: any unsynchronized
// access to those fields is reported as a data race.
func TestProcessLifecycleFieldsRaceSafe(t *testing.T) {
p := &process{logWriter: NewLogWriter()}
var wg sync.WaitGroup
stop := make(chan struct{})
// Writer: churn cmd/done/exitErr like Start + waitForCommand.
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
}
p.mu.Lock()
p.cmd = &exec.Cmd{}
p.done = make(chan struct{})
p.mu.Unlock()
p.setExitErr(errors.New("boom"))
}
}()
// Readers: the concurrent status getters.
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
}
_ = p.IsRunning()
_ = p.GetErr()
_ = p.GetResult()
}
}()
}
time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}