Serialize concurrent writes to the streamed command's output writer

runAndStreamAux funnels a command's stdout and stderr into a single
cmdWriter (the command-log panel, or a buffer when output is suppressed)
from two separate goroutines: stderr through the MultiWriter set on
cmd.Stderr, and stdout through the onRun callback. Those goroutines
wrote the shared writer without any synchronization, racing on the
prefixWriter's prefixWritten flag and interleaving the two streams.
Wrap the writer so its writes are serialized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-08 19:13:33 +02:00
parent 4b97c2ba61
commit d097519c05

View file

@ -244,6 +244,10 @@ func (self *cmdObjRunner) runAndStreamAux(
} else {
cmdWriter = self.guiIO.newCmdWriterFn()
}
// The command's stdout and stderr are streamed to cmdWriter concurrently
// from separate goroutines (stderr via the MultiWriter below, stdout via
// onRun), so it must be safe for concurrent writes.
cmdWriter = &synchronizedWriter{writer: cmdWriter}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
@ -451,6 +455,20 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
}
}
// synchronizedWriter serializes writes to its underlying writer so that it can
// be written from multiple goroutines at once (see runAndStreamAux, which
// streams a command's stdout and stderr to one writer from two goroutines).
type synchronizedWriter struct {
mutex deadlock.Mutex
writer io.Writer
}
func (self *synchronizedWriter) Write(p []byte) (int, error) {
self.mutex.Lock()
defer self.mutex.Unlock()
return self.writer.Write(p)
}
type Buffer struct {
b bytes.Buffer
m deadlock.Mutex