@@ -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+
179222func TestProcessErrorCommandNotFound (t * testing.T ) {
180223 p := newConcurrentProcess (1 )
181224 c := & externalCommand {
0 commit comments