Skip to content

Commit 0f64a49

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
os/exec: remove protection against a duplicate Close on StdinPipe
As of CL 438347, multiple concurrents calls to Close should be safe. This removes some indirection and may also make some programs that use type-assertions marginally more efficient. For example, if a program calls (*exec.Cmd).StdinPipe to obtain a pipe and then sets that as the Stdout of another command, that program will now allow the second command to inherit the file descriptor directly instead of copying everything through a goroutine. This will also cause calls to Close after the first to return an error wrapping os.ErrClosed instead of nil. However, it seems unlikely that programs will depend on that error behavior: if a program is calling Write in a loop followed by Close, then if a racing Close occurs it is likely that the Write would have already reported an error. (The only programs likely to notice a change are those that call Close — without Write! — after a call to Wait.) Updates #56043. Updates #9307. Updates #6270. Change-Id: Iec734b23acefcc7e7ad0c8bc720085bc45988efb Reviewed-on: https://go-review.googlesource.com/c/go/+/439195 Reviewed-by: Ian Lance Taylor <iant@google.com> Auto-Submit: Bryan Mills <bcmills@google.com> Run-TryBot: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent 1b316e3 commit 0f64a49

File tree

2 files changed

+25
-22
lines changed

2 files changed

+25
-22
lines changed

Diff for: src/os/exec/exec.go

+2-20
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ import (
102102
"runtime"
103103
"strconv"
104104
"strings"
105-
"sync"
106105
"syscall"
107106
)
108107

@@ -809,25 +808,8 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
809808
}
810809
c.Stdin = pr
811810
c.childIOFiles = append(c.childIOFiles, pr)
812-
wc := &closeOnce{File: pw}
813-
c.parentIOPipes = append(c.parentIOPipes, wc)
814-
return wc, nil
815-
}
816-
817-
type closeOnce struct {
818-
*os.File
819-
820-
once sync.Once
821-
err error
822-
}
823-
824-
func (c *closeOnce) Close() error {
825-
c.once.Do(c.close)
826-
return c.err
827-
}
828-
829-
func (c *closeOnce) close() {
830-
c.err = c.File.Close()
811+
c.parentIOPipes = append(c.parentIOPipes, pw)
812+
return pw, nil
831813
}
832814

833815
// StdoutPipe returns a pipe that will be connected to the command's

Diff for: src/os/exec/exec_test.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"bufio"
1212
"bytes"
1313
"context"
14+
"errors"
1415
"flag"
1516
"fmt"
1617
"internal/poll"
@@ -548,12 +549,22 @@ func TestStdinClose(t *testing.T) {
548549
t.Error("can't access methods of underlying *os.File")
549550
}
550551
check("Start", cmd.Start())
552+
553+
var wg sync.WaitGroup
554+
wg.Add(1)
555+
defer wg.Wait()
551556
go func() {
557+
defer wg.Done()
558+
552559
_, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString))
553560
check("Copy", err)
561+
554562
// Before the fix, this next line would race with cmd.Wait.
555-
check("Close", stdin.Close())
563+
if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
564+
t.Errorf("Close: %v", err)
565+
}
556566
}()
567+
557568
check("Wait", cmd.Wait())
558569
}
559570

@@ -573,8 +584,15 @@ func TestStdinCloseRace(t *testing.T) {
573584
}
574585
if err := cmd.Start(); err != nil {
575586
t.Fatalf("Start: %v", err)
587+
576588
}
589+
590+
var wg sync.WaitGroup
591+
wg.Add(2)
592+
defer wg.Wait()
593+
577594
go func() {
595+
defer wg.Done()
578596
// We don't check the error return of Kill. It is
579597
// possible that the process has already exited, in
580598
// which case Kill will return an error "process
@@ -583,17 +601,20 @@ func TestStdinCloseRace(t *testing.T) {
583601
// doesn't matter whether this Kill succeeds or not.
584602
cmd.Process.Kill()
585603
}()
604+
586605
go func() {
606+
defer wg.Done()
587607
// Send the wrong string, so that the child fails even
588608
// if the other goroutine doesn't manage to kill it first.
589609
// This test is to check that the race detector does not
590610
// falsely report an error, so it doesn't matter how the
591611
// child process fails.
592612
io.Copy(stdin, strings.NewReader("unexpected string"))
593-
if err := stdin.Close(); err != nil {
613+
if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
594614
t.Errorf("stdin.Close: %v", err)
595615
}
596616
}()
617+
597618
if err := cmd.Wait(); err == nil {
598619
t.Fatalf("Wait: succeeded unexpectedly")
599620
}

0 commit comments

Comments
 (0)