Skip to content

Commit

Permalink
os/exec: remove protection against a duplicate Close on StdinPipe
Browse files Browse the repository at this point in the history
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 <[email protected]>
Auto-Submit: Bryan Mills <[email protected]>
Run-TryBot: Bryan Mills <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
  • Loading branch information
Bryan C. Mills authored and gopherbot committed Oct 11, 2022
1 parent 1b316e3 commit 0f64a49
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 22 deletions.
22 changes: 2 additions & 20 deletions src/os/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"syscall"
)

Expand Down Expand Up @@ -809,25 +808,8 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
}
c.Stdin = pr
c.childIOFiles = append(c.childIOFiles, pr)
wc := &closeOnce{File: pw}
c.parentIOPipes = append(c.parentIOPipes, wc)
return wc, nil
}

type closeOnce struct {
*os.File

once sync.Once
err error
}

func (c *closeOnce) Close() error {
c.once.Do(c.close)
return c.err
}

func (c *closeOnce) close() {
c.err = c.File.Close()
c.parentIOPipes = append(c.parentIOPipes, pw)
return pw, nil
}

// StdoutPipe returns a pipe that will be connected to the command's
Expand Down
25 changes: 23 additions & 2 deletions src/os/exec/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"flag"
"fmt"
"internal/poll"
Expand Down Expand Up @@ -548,12 +549,22 @@ func TestStdinClose(t *testing.T) {
t.Error("can't access methods of underlying *os.File")
}
check("Start", cmd.Start())

var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
defer wg.Done()

_, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString))
check("Copy", err)

// Before the fix, this next line would race with cmd.Wait.
check("Close", stdin.Close())
if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
t.Errorf("Close: %v", err)
}
}()

check("Wait", cmd.Wait())
}

Expand All @@ -573,8 +584,15 @@ func TestStdinCloseRace(t *testing.T) {
}
if err := cmd.Start(); err != nil {
t.Fatalf("Start: %v", err)

}

var wg sync.WaitGroup
wg.Add(2)
defer wg.Wait()

go func() {
defer wg.Done()
// We don't check the error return of Kill. It is
// possible that the process has already exited, in
// which case Kill will return an error "process
Expand All @@ -583,17 +601,20 @@ func TestStdinCloseRace(t *testing.T) {
// doesn't matter whether this Kill succeeds or not.
cmd.Process.Kill()
}()

go func() {
defer wg.Done()
// Send the wrong string, so that the child fails even
// if the other goroutine doesn't manage to kill it first.
// This test is to check that the race detector does not
// falsely report an error, so it doesn't matter how the
// child process fails.
io.Copy(stdin, strings.NewReader("unexpected string"))
if err := stdin.Close(); err != nil {
if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
t.Errorf("stdin.Close: %v", err)
}
}()

if err := cmd.Wait(); err == nil {
t.Fatalf("Wait: succeeded unexpectedly")
}
Expand Down

0 comments on commit 0f64a49

Please sign in to comment.