Skip to content

Commit 10e2ffd

Browse files
committed
os/exec: return idempotent Closer from StdinPipe
Before this fix, it was always an error to use the Close method on the io.WriteCloser obtained from Cmd.StdinPipe, as it would race with the Close performed by Cmd.Wait. Fixes #6270. R=golang-dev, r, remyoudompheng, bradfitz, dsymonds CC=golang-dev https://golang.org/cl/13329043
1 parent 466001d commit 10e2ffd

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

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

+20-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"io"
1414
"os"
1515
"strconv"
16+
"sync"
1617
"syscall"
1718
)
1819

@@ -357,6 +358,8 @@ func (c *Cmd) CombinedOutput() ([]byte, error) {
357358

358359
// StdinPipe returns a pipe that will be connected to the command's
359360
// standard input when the command starts.
361+
// If the returned WriteCloser is not closed before Wait is called,
362+
// Wait will close it.
360363
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
361364
if c.Stdin != nil {
362365
return nil, errors.New("exec: Stdin already set")
@@ -370,8 +373,23 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
370373
}
371374
c.Stdin = pr
372375
c.closeAfterStart = append(c.closeAfterStart, pr)
373-
c.closeAfterWait = append(c.closeAfterWait, pw)
374-
return pw, nil
376+
wc := &closeOnce{File: pw}
377+
c.closeAfterWait = append(c.closeAfterWait, wc)
378+
return wc, nil
379+
}
380+
381+
type closeOnce struct {
382+
*os.File
383+
384+
close sync.Once
385+
closeErr error
386+
}
387+
388+
func (c *closeOnce) Close() error {
389+
c.close.Do(func() {
390+
c.closeErr = c.File.Close()
391+
})
392+
return c.closeErr
375393
}
376394

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

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

+39
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,34 @@ func TestPipes(t *testing.T) {
152152
check("Wait", err)
153153
}
154154

155+
const stdinCloseTestString = "Some test string."
156+
157+
// Issue 6270.
158+
func TestStdinClose(t *testing.T) {
159+
check := func(what string, err error) {
160+
if err != nil {
161+
t.Fatalf("%s: %v", what, err)
162+
}
163+
}
164+
cmd := helperCommand("stdinClose")
165+
stdin, err := cmd.StdinPipe()
166+
check("StdinPipe", err)
167+
// Check that we can access methods of the underlying os.File.`
168+
if _, ok := stdin.(interface {
169+
Fd() uintptr
170+
}); !ok {
171+
t.Error("can't access methods of underlying *os.File")
172+
}
173+
check("Start", cmd.Start())
174+
go func() {
175+
_, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString))
176+
check("Copy", err)
177+
// Before the fix, this next line would race with cmd.Wait.
178+
check("Close", stdin.Close())
179+
}()
180+
check("Wait", cmd.Wait())
181+
}
182+
155183
// Issue 5071
156184
func TestPipeLookPathLeak(t *testing.T) {
157185
fd0 := numOpenFDS(t)
@@ -507,6 +535,17 @@ func TestHelperProcess(*testing.T) {
507535
os.Exit(1)
508536
}
509537
}
538+
case "stdinClose":
539+
b, err := ioutil.ReadAll(os.Stdin)
540+
if err != nil {
541+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
542+
os.Exit(1)
543+
}
544+
if s := string(b); s != stdinCloseTestString {
545+
fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString)
546+
os.Exit(1)
547+
}
548+
os.Exit(0)
510549
case "read3": // read fd 3
511550
fd3 := os.NewFile(3, "fd3")
512551
bs, err := ioutil.ReadAll(fd3)

0 commit comments

Comments
 (0)