Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to understand if exec.cmd was canceled

Tags:

go

exec

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
}
like image 534
Alina Koren Avatar asked Oct 15 '25 07:10

Alina Koren


1 Answers

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.

like image 80
frm Avatar answered Oct 19 '25 14:10

frm



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!