Skip to content

Commit 011a6d1

Browse files
authored
Merge pull request #651 from attehuhtakangas/fix-shellcheck-deadlock-macos
fix deadlock in shellcheck integration on darwin by using cmd.Stdin
2 parents 575dc06 + fd33e9f commit 011a6d1

2 files changed

Lines changed: 50 additions & 11 deletions

File tree

process.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package actionlint
33
import (
44
"context"
55
"fmt"
6-
"io"
76
"os/exec"
7+
"strings"
88
"sync"
99

1010
"github.com/mattn/go-shellwords"
@@ -24,18 +24,14 @@ type cmdExecution struct {
2424
func (e *cmdExecution) run() ([]byte, error) {
2525
cmd := exec.Command(e.cmd, e.args...)
2626
cmd.Stderr = nil
27-
28-
p, err := cmd.StdinPipe()
29-
if err != nil {
30-
return nil, fmt.Errorf("could not make stdin pipe for %s process: %w", e.cmd, err)
31-
}
32-
if _, err := io.WriteString(p, e.stdin); err != nil {
33-
p.Close()
34-
return nil, fmt.Errorf("could not write to stdin of %s process: %w", e.cmd, err)
35-
}
36-
p.Close()
27+
// Set stdin via an io.Reader so that exec.Cmd pipes the bytes to the child
28+
// after Start(). Writing to cmd.StdinPipe() before Start() relies on the
29+
// kernel pipe buffer being large enough to absorb the whole payload, which
30+
// deadlocks on darwin when multiple workers run concurrently (issue #650).
31+
cmd.Stdin = strings.NewReader(e.stdin)
3732

3833
var stdout []byte
34+
var err error
3935
if e.combineOutput {
4036
stdout, err = cmd.CombinedOutput()
4137
} else {

process_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,49 @@ func TestProcessInputStdin(t *testing.T) {
176176
}
177177
}
178178

179+
// Regression test for issue #650: concurrent runs with a stdin payload larger
180+
// than the kernel pipe buffer used to deadlock on darwin because the payload
181+
// was written to cmd.StdinPipe() before cmd.Start().
182+
func TestProcessConcurrentStdinDoesNotDeadlock(t *testing.T) {
183+
p := newConcurrentProcess(5)
184+
185+
// 64 KiB is above the default pipe buffer size on darwin and Linux so it
186+
// forces the stdin copy to happen after the child has started.
187+
payload := strings.Repeat("x", 64*1024)
188+
189+
done := make(chan struct{})
190+
go func() {
191+
defer close(done)
192+
cmds := make([]*externalCommand, 0, 5)
193+
for i := 0; i < 5; i++ {
194+
cat := testSkipIfNoCommand(t, p, "cat")
195+
cat.run(nil, payload, func(b []byte, err error) error {
196+
if err != nil {
197+
t.Errorf("cat failed: %v", err)
198+
return err
199+
}
200+
if len(b) != len(payload) {
201+
t.Errorf("cat output length %d, want %d", len(b), len(payload))
202+
}
203+
return nil
204+
})
205+
cmds = append(cmds, cat)
206+
}
207+
for _, c := range cmds {
208+
if err := c.wait(); err != nil {
209+
t.Errorf("cat wait failed: %v", err)
210+
}
211+
}
212+
p.wait()
213+
}()
214+
215+
select {
216+
case <-done:
217+
case <-time.After(10 * time.Second):
218+
t.Fatal("concurrent stdin writes deadlocked — see issue #650")
219+
}
220+
}
221+
179222
func TestProcessErrorCommandNotFound(t *testing.T) {
180223
p := newConcurrentProcess(1)
181224
c := &externalCommand{

0 commit comments

Comments
 (0)