Fix shelltool readExitCode to wait on the live stdout signal so it is not gated on the 100ms poll#702
Conversation
6f5d1dc to
1d982b6
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes a subtle producer/consumer signaling bug in the persistent shell session that caused readExitCode to miss the live stdoutSignal close and fall back to the 100ms polling path, adding avoidable latency to each command.
Changes:
- Stop resetting
s.stdoutSignalinsidereadExitCode, so it always waits on the exact signal thatreadLoopwill close for the next stdout chunk. - Add a regression test that splits the sentinel and exit-code suffix across two stdout reads and asserts the exit code is observed promptly (not gated by the 100ms poll).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tool/shelltool/session.go | Fixes readExitCode to wait on the live stdoutSignal channel closed by readLoop, avoiding the 100ms fallback poll delay. |
| tool/shelltool/localshell_internal_test.go | Adds a regression test covering the “sentinel and exit code arrive in separate reads” case to ensure prompt wake-up behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
readExitCode captured signal := s.stdoutSignal and then immediately reassigned s.stdoutSignal = newSignal(). readLoop only closes the current s.stdoutSignal, so it closed the replacement and never the channel readExitCode was selecting on. That made the <-signal case dead: readExitCode always waited out the 100ms fallback poll to notice the exit-code line when it arrived in a separate read from the sentinel, stalling each command by up to ~100ms. Drop the reassignment so readExitCode observes the same channel readLoop closes, mirroring waitForSentinel. The pre-command fresh signal is already installed in snapshotOffsets, so no reset is lost.
1d982b6 to
9bbca96
Compare
Parity Review — No Issues FoundThis PR fixes a channel-signalling race in Cross-repo parity: Not applicable — this is a Go-internal concurrency fix with no semantic equivalent in the upstream .NET or Python implementations. Label status: Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
What
In
tool/shelltool/session.go,readExitCodecapturedsignal := s.stdoutSignaland then immediately reassigneds.stdoutSignal = newSignal(). The producerreadLooponly closes the currents.stdoutSignal(prev := s.stdoutSignal; s.stdoutSignal = newSignal(); close(prev)). BecausereadExitCodehad already swapped in a replacement,readLoopclosed that replacement on the next stdout chunk and never the old channel valuereadExitCodewas selecting on.That made the
<-signalcase dead:readExitCodealways waited out thetime.After(100ms)fallback poll to notice the exit-code line whenever it arrived in a separate read from the sentinel — stalling each command by up to ~100ms.The fix drops the reassignment so
readExitCodeselects on the exact channelreadLoopcloses, mirroringwaitForSentinel(which grabssignal := s.stdoutSignalwithout reassigning and is therefore woken immediately). The pre-command fresh signal is already installed bysnapshotOffsets, so no reset is lost.Why
This matches the intended producer/consumer signalling contract already used by
waitForSentinel, and keeps command latency tight against the .NETShellSessionprotocol semantics the Go port follows — the exit-code line delimiting each command should be observed as soon as it is read, not on a coarse poll.Testing
Added
TestReadExitCodeWakesOnLiveStdoutSignal(white-box, in the package's existing internal test file) that drives apersistentSessionreadLoopwith a scripted stdout reader delivering the sentinel and the exit-code line in two separate reads ~5ms apart, then assertswaitForSentinel/readExitCodereturns the correct rc in well under 100ms.go build ./...,go vet ./tool/shelltool/..., andgo test -race ./tool/shelltool/...all pass.