Skip to content

Commit 28f7ee7

Browse files
committed
refactor(exec): enhance concurrent output handling with syncWriter for OSRunner
1 parent f1a1d1b commit 28f7ee7

1 file changed

Lines changed: 23 additions & 2 deletions

File tree

‎internal/exec/exec.go‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
osexec "os/exec"
11+
"sync"
1112
)
1213

1314
// Runner runs an external command to completion, capturing its stdout and
@@ -37,8 +38,15 @@ func (r OSRunner) Run(ctx context.Context, stream io.Writer, name string, args .
3738
}
3839

3940
var stdout, stderr bytes.Buffer
40-
cmd.Stdout = tee(&stdout, stream)
41-
cmd.Stderr = tee(&stderr, stream)
41+
// os/exec copies stdout and stderr on two separate goroutines. Each has its
42+
// own capture buffer (no sharing there), but both tee to the single `stream`
43+
// writer, so that shared writer must be serialized or the two goroutines race.
44+
live := stream
45+
if stream != nil {
46+
live = &syncWriter{w: stream}
47+
}
48+
cmd.Stdout = tee(&stdout, live)
49+
cmd.Stderr = tee(&stderr, live)
4250

4351
err := cmd.Run()
4452
if err != nil {
@@ -66,3 +74,16 @@ func tee(buf *bytes.Buffer, live io.Writer) io.Writer {
6674
}
6775
return io.MultiWriter(buf, live)
6876
}
77+
78+
// syncWriter serializes concurrent writes to an underlying writer, so the same
79+
// stream target can back both the stdout and stderr copy goroutines safely.
80+
type syncWriter struct {
81+
mu sync.Mutex
82+
w io.Writer
83+
}
84+
85+
func (s *syncWriter) Write(p []byte) (int, error) {
86+
s.mu.Lock()
87+
defer s.mu.Unlock()
88+
return s.w.Write(p)
89+
}

0 commit comments

Comments
 (0)