refactor(shell): sync.RWMutex for sessions + sync.Map for exited (mirror process pkg)

Same shape as the process package refactor:
- LocalManager.mu (sync.Mutex) -> sessionsMu (sync.RWMutex).
  Read paths (Status, Subscribe, Stdin, ExitStatus, Resize) take
  RLock; write paths (Start, Stop, Restart, List) take Lock.
- LocalManager.exited: was a hand-rolled map[workspaceID]map[shellID]struct{}
  guarded by exitedMu; now a sync.Map keyed by shellID only (UUID
  is globally unique, no need for the nested map). Helpers
  IsExited / MarkAsExited / ClearExited.
- shellOrder stays a plain map; read+written under sessionsMu.
- waitExit remains the sole caller of MarkAsExited; Start /
  Stop / Restart call ClearExited.
- New TestShellIsExitedHelpers covers the helper semantics.

go test -race -count=2 ./... clean. Same caveat as the process
package: at this app's concurrency level, neither sync.RWMutex nor
sync.Map measurably beats the previous pair — the change is mostly
stylistic (one fewer lock, no nested maps, more idiomatic Go).

Conversation: 019f3673-d2d5-78f0-a7a9-5e3e91b65933
This commit is contained in:
tao.chen
2026-07-06 16:14:55 +08:00
parent 5e694aec12
commit 3d34de96bf
2 changed files with 104 additions and 71 deletions
+47
View File
@@ -201,3 +201,50 @@ func TestShellMultiInstance(t *testing.T) {
t.Fatalf("List returned shell %q, want %q", list[0].ShellID, shellID2)
}
}
func TestShellIsExitedHelpers(t *testing.T) {
mgr := NewManager("bash", []string{"-i"})
root := t.TempDir()
shellIDA, err := mgr.Start("test-ws", root)
if err != nil {
t.Fatalf("Start shell A failed: %v", err)
}
shellIDB, err := mgr.Start("test-ws", root)
if err != nil {
t.Fatalf("Start shell B failed: %v", err)
}
t.Cleanup(func() {
_ = mgr.Stop("test-ws", shellIDA)
_ = mgr.Stop("test-ws", shellIDB)
})
if mgr.IsExited(shellIDA) {
t.Errorf("IsExited(A) = true, want false")
}
if mgr.IsExited(shellIDB) {
t.Errorf("IsExited(B) = true, want false")
}
if err := mgr.Stop("test-ws", shellIDA); err != nil {
t.Fatalf("Stop shell A failed: %v", err)
}
if mgr.IsExited(shellIDA) {
t.Errorf("IsExited(A) = true after Stop, want false")
}
if mgr.IsExited(shellIDB) {
t.Errorf("IsExited(B) = true after stopping A, want false")
}
mgr.MarkAsExited(shellIDB)
if !mgr.IsExited(shellIDB) {
t.Errorf("IsExited(B) = false after MarkAsExited, want true")
}
mgr.ClearExited(shellIDB)
if mgr.IsExited(shellIDB) {
t.Errorf("IsExited(B) = true after ClearExited, want false")
}
}