diff options
Diffstat (limited to 'src/os/exec/exec.go')
| -rw-r--r-- | src/os/exec/exec.go | 274 |
1 files changed, 244 insertions, 30 deletions
diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go index 0d7a86bad4..31395c13df 100644 --- a/src/os/exec/exec.go +++ b/src/os/exec/exec.go @@ -103,6 +103,7 @@ import ( "strconv" "strings" "syscall" + "time" ) // Error is returned by LookPath when it fails to classify a file as an @@ -120,6 +121,11 @@ func (e *Error) Error() string { func (e *Error) Unwrap() error { return e.Err } +// ErrWaitDelay is returned by (*Cmd).Wait if the process exits with a +// successful status code but its output pipes are not closed before the +// command's WaitDelay expires. +var ErrWaitDelay = errors.New("exec: WaitDelay expired before I/O complete") + // wrappedError wraps an error without relying on fmt.Errorf. type wrappedError struct { prefix string @@ -178,7 +184,8 @@ type Cmd struct { // goroutine reads from Stdin and delivers that data to the command // over a pipe. In this case, Wait does not complete until the goroutine // stops copying, either because it has reached the end of Stdin - // (EOF or a read error) or because writing to the pipe returned an error. + // (EOF or a read error), or because writing to the pipe returned an error, + // or because a nonzero WaitDelay was set and expired. Stdin io.Reader // Stdout and Stderr specify the process's standard output and error. @@ -192,7 +199,8 @@ type Cmd struct { // Otherwise, during the execution of the command a separate goroutine // reads from the process over a pipe and delivers that data to the // corresponding Writer. In this case, Wait does not complete until the - // goroutine reaches EOF or encounters an error. + // goroutine reaches EOF or encounters an error or a nonzero WaitDelay + // expires. // // If Stdout and Stderr are the same writer, and have a type that can // be compared with ==, at most one goroutine at a time will call Write. @@ -218,8 +226,64 @@ type Cmd struct { // populate its ProcessState when the command completes. ProcessState *os.ProcessState - ctx context.Context // nil means none - Err error // LookPath error, if any. + // ctx is the context passed to CommandContext, if any. + ctx context.Context + + Err error // LookPath error, if any. + + // If Cancel is non-nil, the command must have been created with + // CommandContext and Cancel will be called when the command's + // Context is done. By default, CommandContext sets Cancel to + // call the Kill method on the command's Process. + // + // Typically a custom Cancel will send a signal to the command's + // Process, but it may instead take other actions to initiate cancellation, + // such as closing a stdin or stdout pipe or sending a shutdown request on a + // network socket. + // + // If the command exits with a success status after Cancel is + // called, and Cancel does not return an error equivalent to + // os.ErrProcessDone, then Wait and similar methods will return a non-nil + // error: either an error wrapping the one returned by Cancel, + // or the error from the Context. + // (If the command exits with a non-success status, or Cancel + // returns an error that wraps os.ErrProcessDone, Wait and similar methods + // continue to return the command's usual exit status.) + // + // If Cancel is set to nil, nothing will happen immediately when the command's + // Context is done, but a nonzero WaitDelay will still take effect. That may + // be useful, for example, to work around deadlocks in commands that do not + // support shutdown signals but are expected to always finish quickly. + // + // Cancel will not be called if Start returns a non-nil error. + Cancel func() error + + // If WaitDelay is non-zero, it bounds the time spent waiting on two sources + // of unexpected delay in Wait: a child process that fails to exit after the + // associated Context is canceled, and a child process that exits but leaves + // its I/O pipes unclosed. + // + // The WaitDelay timer starts when either the associated Context is done or a + // call to Wait observes that the child process has exited, whichever occurs + // first. When the delay has elapsed, the command shuts down the child process + // and/or its I/O pipes. + // + // If the child process has failed to exit — perhaps because it ignored or + // failed to receive a shutdown signal from a Cancel function, or because no + // Cancel function was set — then it will be terminated using os.Process.Kill. + // + // Then, if the I/O pipes communicating with the child process are still open, + // those pipes are closed in order to unblock any goroutines currently blocked + // on Read or Write calls. + // + // If pipes are closed due to WaitDelay, no Cancel call has occurred, + // and the command has otherwise exited with a successful status, Wait and + // similar methods will return ErrWaitDelay instead of nil. + // + // If WaitDelay is zero (the default), I/O pipes will be read until EOF, + // which might not occur until orphaned subprocesses of the command have + // also closed their descriptors for the pipes. + WaitDelay time.Duration // childIOFiles holds closers for any of the child process's // stdin, stdout, and/or stderr files that were opened by the Cmd itself @@ -230,7 +294,8 @@ type Cmd struct { // parentIOPipes holds closers for the parent's end of any pipes // connected to the child's stdin, stdout, and/or stderr streams // that were opened by the Cmd itself (not supplied by the caller). - // These should be closed after Wait sees the command exit. + // These should be closed after Wait sees the command and copying + // goroutines exit, or after WaitDelay has expired. parentIOPipes []io.Closer // goroutine holds a set of closures to execute to copy data @@ -242,7 +307,8 @@ type Cmd struct { // goroutineErr is set to nil once its error has been received. goroutineErr <-chan error - ctxErr <-chan error // if non nil, receives the error from watchCtx exactly once + // If ctxResult is non-nil, it receives the result of watchCtx exactly once. + ctxResult <-chan ctxResult // The stack saved when the Command was created, if GODEBUG contains // execwait=2. Used for debugging leaks. @@ -268,6 +334,20 @@ type Cmd struct { lookPathErr error } +// A ctxResult reports the result of watching the Context associated with a +// running command (and sending corresponding signals if needed). +type ctxResult struct { + err error + + // If timer is non-nil, it expires after WaitDelay has elapsed after + // the Context is done. + // + // (If timer is nil, that means that the Context was not done before the + // command completed, or no WaitDelay was set, or the WaitDelay already + // expired and its effect was already applied.) + timer *time.Timer +} + // Command returns the Cmd struct to execute the named program with // the given arguments. // @@ -349,15 +429,22 @@ func Command(name string, arg ...string) *Cmd { // CommandContext is like Command but includes a context. // -// The provided context is used to kill the process (by calling -// os.Process.Kill) if the context becomes done before the command -// completes on its own. +// The provided context is used to interrupt the process +// (by calling cmd.Cancel or os.Process.Kill) +// if the context becomes done before the command completes on its own. +// +// CommandContext sets the command's Cancel function to invoke the Kill method +// on its Process, and leaves its WaitDelay unset. The caller may change the +// cancellation behavior by modifying those fields before starting the command. func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { if ctx == nil { panic("nil Context") } cmd := Command(name, arg...) cmd.ctx = ctx + cmd.Cancel = func() error { + return cmd.Process.Kill() + } return cmd } @@ -566,6 +653,9 @@ func (c *Cmd) Start() error { } c.Path = lp } + if c.Cancel != nil && c.ctx == nil { + return errors.New("exec: command with a non-nil Cancel was not created with CommandContext") + } if c.ctx != nil { select { case <-c.ctx.Done(): @@ -638,38 +728,114 @@ func (c *Cmd) Start() error { c.goroutine = nil // Allow the goroutines' closures to be GC'd when they complete. } - if c.ctx != nil && c.ctx.Done() != nil { - errc := make(chan error) - c.ctxErr = errc - go c.watchCtx(errc) + // If we have anything to do when the command's Context expires, + // start a goroutine to watch for cancellation. + // + // (Even if the command was created by CommandContext, a helper library may + // have explicitly set its Cancel field back to nil, indicating that it should + // be allowed to continue running after cancellation after all.) + if (c.Cancel != nil || c.WaitDelay != 0) && c.ctx != nil && c.ctx.Done() != nil { + resultc := make(chan ctxResult) + c.ctxResult = resultc + go c.watchCtx(resultc) } return nil } -// watchCtx watches c.ctx until it is able to send a result to errc. +// watchCtx watches c.ctx until it is able to send a result to resultc. +// +// If c.ctx is done before a result can be sent, watchCtx calls c.Cancel, +// and/or kills cmd.Process it after c.WaitDelay has elapsed. // -// If c.ctx is done before a result can be sent, watchCtx terminates c.Process. -func (c *Cmd) watchCtx(errc chan<- error) { +// watchCtx manipulates c.goroutineErr, so its result must be received before +// c.awaitGoroutines is called. +func (c *Cmd) watchCtx(resultc chan<- ctxResult) { select { - case errc <- nil: + case resultc <- ctxResult{}: return case <-c.ctx.Done(): } var err error + if c.Cancel != nil { + if interruptErr := c.Cancel(); interruptErr == nil { + // We appear to have successfully interrupted the command, so any + // program behavior from this point may be due to ctx even if the + // command exits with code 0. + err = c.ctx.Err() + } else if errors.Is(interruptErr, os.ErrProcessDone) { + // The process already finished: we just didn't notice it yet. + // (Perhaps c.Wait hadn't been called, or perhaps it happened to race with + // c.ctx being cancelled.) Don't inject a needless error. + } else { + err = wrappedError{ + prefix: "exec: canceling Cmd", + err: interruptErr, + } + } + } + if c.WaitDelay == 0 { + resultc <- ctxResult{err: err} + return + } + + timer := time.NewTimer(c.WaitDelay) + select { + case resultc <- ctxResult{err: err, timer: timer}: + // c.Process.Wait returned and we've handed the timer off to c.Wait. + // It will take care of goroutine shutdown from here. + return + case <-timer.C: + } + + killed := false if killErr := c.Process.Kill(); killErr == nil { // We appear to have killed the process. c.Process.Wait should return a // non-nil error to c.Wait unless the Kill signal races with a successful // exit, and if that does happen we shouldn't report a spurious error, // so don't set err to anything here. + killed = true } else if !errors.Is(killErr, os.ErrProcessDone) { err = wrappedError{ - prefix: "exec: error sending signal to Cmd", + prefix: "exec: killing Cmd", err: killErr, } } - errc <- err + + if c.goroutineErr != nil { + select { + case goroutineErr := <-c.goroutineErr: + // Forward goroutineErr only if we don't have reason to believe it was + // caused by a call to Cancel or Kill above. + if err == nil && !killed { + err = goroutineErr + } + default: + // Close the child process's I/O pipes, in case it abandoned some + // subprocess that inherited them and is still holding them open + // (see https://go.dev/issue/23019). + // + // We close the goroutine pipes only after we have sent any signals we're + // going to send to the process (via Signal or Kill above): if we send + // SIGKILL to the process, we would prefer for it to die of SIGKILL, not + // SIGPIPE. (However, this may still cause any orphaned subprocesses to + // terminate with SIGPIPE.) + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but report ErrWaitDelay for + // the error: any other error here could result from closing the pipes. + _ = <-c.goroutineErr + if err == nil { + err = ErrWaitDelay + } + } + + // Since we have already received the only result from c.goroutineErr, + // set it to nil to prevent awaitGoroutines from blocking on it. + c.goroutineErr = nil + } + + resultc <- ctxResult{err: err} } // An ExitError reports an unsuccessful exit by a command. @@ -724,24 +890,23 @@ func (c *Cmd) Wait() error { } c.ProcessState = state - if c.ctxErr != nil { - interruptErr := <-c.ctxErr + var timer *time.Timer + if c.ctxResult != nil { + watch := <-c.ctxResult + timer = watch.timer // If c.Process.Wait returned an error, prefer that. - // Otherwise, report any error from the interrupt goroutine. - if err == nil { - err = interruptErr + // Otherwise, report any error from the watchCtx goroutine, + // such as a Context cancellation or a WaitDelay overrun. + if err == nil && watch.err != nil { + err = watch.err } } - // Wait for the pipe-copying goroutines to complete. - if c.goroutineErr != nil { + if goroutineErr := c.awaitGoroutines(timer); err == nil { // Report an error 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. - copyErr := <-c.goroutineErr - if err == nil { - err = copyErr - } + err = goroutineErr } closeDescriptors(c.parentIOPipes) c.parentIOPipes = nil @@ -749,6 +914,55 @@ func (c *Cmd) Wait() error { return err } +// awaitGoroutines waits for the results of the goroutines copying data to or +// from the command's I/O pipes. +// +// If c.WaitDelay elapses before the goroutines complete, awaitGoroutines +// forcibly closes their pipes and returns ErrWaitDelay. +// +// If timer is non-nil, it must send to timer.C at the end of c.WaitDelay. +func (c *Cmd) awaitGoroutines(timer *time.Timer) error { + defer func() { + if timer != nil { + timer.Stop() + } + c.goroutineErr = nil + }() + + if c.goroutineErr == nil { + return nil // No running goroutines to await. + } + + if timer == nil { + if c.WaitDelay == 0 { + return <-c.goroutineErr + } + + select { + case err := <-c.goroutineErr: + // Avoid the overhead of starting a timer. + return err + default: + } + + // No existing timer was started: either there is no Context associated with + // the command, or c.Process.Wait completed before the Context was done. + timer = time.NewTimer(c.WaitDelay) + } + + select { + case <-timer.C: + closeDescriptors(c.parentIOPipes) + // Wait for the copying goroutines to finish, but ignore any error + // (since it was probably caused by closing the pipes). + _ = <-c.goroutineErr + return ErrWaitDelay + + case err := <-c.goroutineErr: + return err + } +} + // Output runs the command and returns its standard output. // Any returned error will usually be of type *ExitError. // If c.Stderr was nil, Output populates ExitError.Stderr. |
