diff options
| author | Bryan C. Mills <bcmills@google.com> | 2022-04-22 23:33:16 -0400 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2022-05-06 22:04:35 +0000 |
| commit | 4e79f06dac712d35d67d72777dae6df6d8c97888 (patch) | |
| tree | a549a9682bc76944e5c377270dbc27f91bcaa519 /src/os/exec | |
| parent | d7a03da8edf37ee4fa9c1200040db001c6c3e56a (diff) | |
| download | go-4e79f06dac712d35d67d72777dae6df6d8c97888.tar.xz | |
os/exec: refactor goroutine communication in Wait
This provides clearer synchronization invariants: if it occurs at all,
the call to c.Process.Kill always occurs before Wait returns. It also
allows any unexpected errors from the goroutine to be propagated back
to Wait.
For #50436.
Change-Id: I7ddadc73e6e67399596e35393f5845646f6111ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/401896
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Diffstat (limited to 'src/os/exec')
| -rw-r--r-- | src/os/exec/exec.go | 105 |
1 files changed, 78 insertions, 27 deletions
diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go index 042d7f465d..8101e718ff 100644 --- a/src/os/exec/exec.go +++ b/src/os/exec/exec.go @@ -115,6 +115,20 @@ func (e *Error) Error() string { func (e *Error) Unwrap() error { return e.Err } +// wrappedError wraps an error without relying on fmt.Errorf. +type wrappedError struct { + prefix string + err error +} + +func (w wrappedError) Error() string { + return w.prefix + ": " + w.err.Error() +} + +func (w wrappedError) Unwrap() error { + return w.err +} + // Cmd represents an external command being prepared or run. // // A Cmd cannot be reused after calling its Run, Output or CombinedOutput @@ -200,13 +214,12 @@ type Cmd struct { ctx context.Context // nil means none Err error // LookPath error, if any. - finished bool // when Wait was called childFiles []*os.File closeAfterStart []io.Closer closeAfterWait []io.Closer goroutine []func() error - errch chan error // one send per goroutine - waitDone chan struct{} + goroutineErrs <-chan error // one receive per goroutine + ctxErr <-chan error // if non nil, receives the error from watchCtx exactly once // For a security release long ago, we created x/sys/execabs, // which manipulated the unexported lookPathErr error field @@ -514,26 +527,18 @@ func (c *Cmd) Start() error { c.closeDescriptors(c.closeAfterStart) - // Don't allocate the channel unless there are goroutines to fire. + // Don't allocate the goroutineErrs channel unless there are goroutines to fire. if len(c.goroutine) > 0 { - c.errch = make(chan error, len(c.goroutine)) + errc := make(chan error, len(c.goroutine)) + c.goroutineErrs = errc for _, fn := range c.goroutine { go func(fn func() error) { - c.errch <- fn() + errc <- fn() }(fn) } } - if c.ctx != nil { - c.waitDone = make(chan struct{}) - go func() { - select { - case <-c.ctx.Done(): - c.Process.Kill() - case <-c.waitDone: - } - }() - } + c.ctxErr = c.watchCtx() return nil } @@ -580,33 +585,79 @@ func (c *Cmd) Wait() error { if c.Process == nil { return errors.New("exec: not started") } - if c.finished { + if c.ProcessState != nil { return errors.New("exec: Wait was already called") } - c.finished = true - state, err := c.Process.Wait() - if c.waitDone != nil { - close(c.waitDone) + if err == nil && !state.Success() { + err = &ExitError{ProcessState: state} } c.ProcessState = state + // Wait for the pipe-copying goroutines to complete. var copyError error for range c.goroutine { - if err := <-c.errch; err != nil && copyError == nil { + if err := <-c.goroutineErrs; err != nil && copyError == nil { copyError = err } } + c.goroutine = nil // Allow the goroutines' closures to be GC'd. + + if c.ctxErr != nil { + interruptErr := <-c.ctxErr + // If c.Process.Wait returned an error, prefer that. + // Otherwise, report any error from the interrupt goroutine. + if interruptErr != nil && err == nil { + err = interruptErr + } + } + // Report errors from the copying goroutines only if the program otherwise + // exited normally on its own. Otherwise, the copying error may be due to the + // abnormal termination. + if err == nil { + err = copyError + } c.closeDescriptors(c.closeAfterWait) + c.closeAfterWait = nil - if err != nil { - return err - } else if !state.Success() { - return &ExitError{ProcessState: state} + return err +} + +// watchCtx conditionally starts a goroutine that waits until either c.ctx is +// done or c.Process.Wait has completed (called from Wait). +// If c.ctx is done first, the goroutine terminates c.Process. +// +// If a goroutine was started, watchCtx returns a channel on which its result +// must be received. +func (c *Cmd) watchCtx() <-chan error { + if c.ctx == nil { + return nil } - return copyError + errc := make(chan error) + go func() { + select { + case errc <- nil: + return + case <-c.ctx.Done(): + } + + var err error + if killErr := c.Process.Kill(); killErr == nil { + // We appear to have successfully delivered a kill signal, so any + // program behavior from this point may be due to ctx. + err = c.ctx.Err() + } else if !errors.Is(killErr, os.ErrProcessDone) { + err = wrappedError{ + prefix: "exec: error sending signal to Cmd", + err: killErr, + } + } + errc <- err + }() + + return errc } // Output runs the command and returns its standard output. |
