简体   繁体   中英

How to understand if exec.cmd was canceled

I'm trying to return specific error when the command was canceled by context. After investigating ProcessState understood that if got -1 in exitCode the process got terminate signal https://golang.org/pkg/os/#ProcessState.ExitCode but maybe we have more elegant way? Maybe I can put this error from cancel function? Maybe it isn't good enough exitCode for understanding if the command was canceled?

var (
    CmdParamsErr = errors.New("failed to get params for execution command")
    ExecutionCanceled = errors.New("command canceled")
)

func execute(m My) error {
    filePath, args, err := cmdParams(m)
    err = nil
    if err != nil {
        log.Infof("cmdParams: err: %v\n, m: %v\n", err, m)
        return CmdParamsErr
    }

    var out bytes.Buffer
    var errStd bytes.Buffer
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    cmd := exec.CommandContext(ctx, filePath, args...)
    cmd.Stdout = &out
    cmd.Stderr = &errStd
    err = cmd.Run()
    if err != nil {
        if cmd.ProcessState.ExitCode() == -1 {
            log.Warnf("execution was canceled by signal, err: %v\n", err)
            err = ExecutionCanceled
            return err
        } else {
            log.Errorf("run failed, err: %v, filePath: %v, args: %v\n", err, filePath, args)
            return err
        }
    }
    return err
}

exec.ExitError doesn't provide any reason for the exit code (there is no relevant struct field nor an Unwrap method), so you have to check the context directly:

if ctx.Err() != nil {
    log.Println("canceled")
}   

Note that this is a slight race because the context may be canceled just after the command failed for a different reason, but there is nothing you can do about that.

There is no straightforward or elegant way to figure out if a process was killed because a context was canceled. The closest you can come is this:

func run() error {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    cmd := exec.CommandContext(ctx, "bash", "-c", "exit 1")

    // Start() returns an error if the process can't be started. It will return
    // ctx.Err() if the context is expired before starting the process.

    if err := cmd.Start(); err != nil {
        return err
    }

    if err := cmd.Wait(); err != nil {
        if e, ok := err.(*exec.ExitError); ok {

            // If the process exited by itself, just return the error to the
            // caller.

            if e.Exited() {
                return e
            }

            // We know now that the process could be started, but didn't exit
            // by itself. Something must have killed it. If the context is done,
            // we can *assume* that it has been killed by the exec.Command.
            // Let's return ctx.Err() so our user knows that this *might* be
            // the case.

            select {
            case <-ctx.Done():
                return ctx.Err()
            default:
                return e
            }
        }

        return err
    }

    return nil
}

The problem here is that there might be a race condition, so returning ctx.Err() might be misleading. For example, imagine the following scenario:

  1. The process starts.
  2. The process is killed by an external actor.
  3. The context is canceled.
  4. You check the context.

At this point, the function above would return ctx.Err() , but this might be misleading because the reason why the process was killed is not because the context was canceled. If you decide to use a code similar to the function above, keep in mind this approximation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM