fix(process): remove data race on cmd.ProcessState (was read across goroutines)

The race: os/exec writes cmd.ProcessState inside cmd.Wait(), which
runs in the waitExit goroutine. Start, Status, Subscribe, and
Restart all read cmd.ProcessState from other goroutines to decide
'still running?'. That's a textbook Go data race — fixed by
TestStartRejectsDuplicateRunningSession under -race.

Fix: port the same pattern the shell package uses for its multi-shell
refactor. The manager now owns an exited map[string]struct{} guarded
by exitedMu. waitExit is the ONLY place that calls cmd.Wait(); after
Wait() returns it records exited[workspaceID]. Everyone else checks
the exited map instead of reading cmd.ProcessState.

- internal/process/manager.go: add exited + exitedMu to LocalManager;
  Start conflict check, Status, Subscribe, Restart, Stop all consult
  the exited map; Start clears the entry on new process; Stop and
  Restart delete the entry on stop; waitExit sets it after Wait().
- go.mod / go.sum: tidied (uuid v1.6.0 was already direct dep from
  the shell refactor).

go test -race -count=3 ./...  clean across the whole repo.
go test -race -count=5 ./internal/process/...  clean.
This commit is contained in:
tao.chen
2026-07-06 15:52:07 +08:00
parent 7b40b23d33
commit 568423751f
2 changed files with 41 additions and 11 deletions
+1 -1
View File
@@ -5,6 +5,7 @@ go 1.25.0
require (
github.com/creack/pty v1.1.24
github.com/gin-gonic/gin v1.12.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
gopkg.in/yaml.v3 v3.0.1
)
@@ -21,7 +22,6 @@ require (
github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect