Files
3x-ui/internal/xray/process_race_test.go
T
MHSanaei f2f11bc042 fix(xray): synchronize the process version and apiPort fields
Start writes p.version and p.apiPort (via refreshVersion/refreshAPIPort) after
flipping the process to running, while GetXrayVersion and GetAPIPort read them
lock-free from the status and traffic poll goroutines. The struct mutex
deliberately excluded these fields, so a restart racing a poll was a real data
race — a torn read of the version string header can crash. Extend the mutex to
cover version and apiPort, doing the blocking version probe before taking the
lock.
2026-07-15 03:33:38 +02:00

103 lines
1.9 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.Go(func() {
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 range 4 {
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
_ = p.IsRunning()
_ = p.GetErr()
_ = p.GetResult()
}
})
}
time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}
// TestProcessVersionAPIPortRaceSafe writes version/apiPort the way Start's
// refresh helpers do while GetXrayVersion/GetAPIPort read them concurrently.
// Run with -race: an unsynchronized access to either field is reported.
func TestProcessVersionAPIPortRaceSafe(t *testing.T) {
inner := &process{
logWriter: NewLogWriter(),
config: &Config{InboundConfigs: []InboundConfig{{Tag: "api", Port: 12345}}},
}
p := &Process{inner}
var wg sync.WaitGroup
stop := make(chan struct{})
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
p.refreshAPIPort()
inner.mu.Lock()
inner.version = "v1.2.3"
inner.mu.Unlock()
}
})
for range 4 {
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
_ = p.GetAPIPort()
_ = p.GetXrayVersion()
}
})
}
time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}