package subprocess import ( "context" "os/exec" "time" ) type executionReason uint8 const ( executionNotStarted executionReason = iota executionNaturalExit executionCanceled executionDeadline executionAborted ) type executionOutcome struct { reason executionReason startedAt time.Time completedAt time.Time exitCode int startErr error waitErr error contextErr error abortErr error cleanupErr error } func executeOwnedCommand(ctx context.Context, cmd *exec.Cmd, abort <-chan error) (outcome executionOutcome) { outcome = executionOutcome{startedAt: time.Now().UTC(), exitCode: -1} defer func() { outcome.completedAt = time.Now().UTC() }() if outcome.contextErr = ctx.Err(); outcome.contextErr != nil { outcome.reason = executionNotStarted return outcome } tree, err := newOwnedProcessTree() if err != nil { outcome.reason, outcome.startErr = executionNotStarted, err return outcome } defer func() { outcome.cleanupErr = joinErrors(outcome.cleanupErr, tree.Dispose()) }() cmd.WaitDelay = forcefulTerminationWait if outcome.startErr = tree.Start(cmd); outcome.startErr != nil { outcome.reason = executionNotStarted return outcome } waitCh := make(chan error, 1) go func() { waitCh <- cmd.Wait() }() select { case outcome.waitErr = <-waitCh: outcome.reason = executionNaturalExit case <-ctx.Done(): outcome.contextErr = ctx.Err() if outcome.contextErr == context.DeadlineExceeded { outcome.reason = executionDeadline } else { outcome.reason = executionCanceled } outcome.cleanupErr = terminateAndReap(tree, waitCh) case outcome.abortErr = <-abort: outcome.reason = executionAborted outcome.cleanupErr = terminateAndReap(tree, waitCh) } if cmd.ProcessState != nil { outcome.exitCode = cmd.ProcessState.ExitCode() } return outcome } func terminateAndReap(tree ownedProcessTree, waitCh <-chan error) error { cleanupErr := tree.TerminateGracefully() timer := time.NewTimer(gracefulTerminationWait) defer timer.Stop() select { case <-waitCh: return joinErrors(cleanupErr, tree.TerminateForcefully()) case <-timer.C: } cleanupErr = joinErrors(cleanupErr, tree.TerminateForcefully()) forcefulTimer := time.NewTimer(forcefulTerminationWait) defer forcefulTimer.Stop() select { case <-waitCh: return cleanupErr case <-forcefulTimer.C: return joinErrors(cleanupErr, context.DeadlineExceeded) } }