Skip to content

Commit

Permalink
Merge pull request #5760 from Benehiko/user-terminated-ctx-err
Browse files Browse the repository at this point in the history
cmd/docker: add cause to user-terminated `context.Context`
  • Loading branch information
Benehiko authored Feb 5, 2025
2 parents dff0dc8 + c51be77 commit 9005f36
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 2 deletions.
55 changes: 53 additions & 2 deletions cmd/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,57 @@ import (
"go.opentelemetry.io/otel"
)

type errCtxSignalTerminated struct {
signal os.Signal
}

func (e errCtxSignalTerminated) Error() string {
return ""
}

func main() {
err := dockerMain(context.Background())
ctx := context.Background()
err := dockerMain(ctx)

if errors.As(err, &errCtxSignalTerminated{}) {
os.Exit(getExitCode(err))
return
}

if err != nil && !errdefs.IsCancelled(err) {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(getExitCode(err))
}
}

func notifyContext(ctx context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, signals...)

ctxCause, cancel := context.WithCancelCause(ctx)

go func() {
select {
case <-ctx.Done():
signal.Stop(ch)
return
case sig := <-ch:
cancel(errCtxSignalTerminated{
signal: sig,
})
signal.Stop(ch)
return
}
}()

return ctxCause, func() {
signal.Stop(ch)
cancel(nil)
}
}

func dockerMain(ctx context.Context) error {
ctx, cancelNotify := signal.NotifyContext(ctx, platformsignals.TerminationSignals...)
ctx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
defer cancelNotify()

dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
Expand All @@ -57,6 +98,16 @@ func getExitCode(err error) int {
if err == nil {
return 0
}

var userTerminatedErr errCtxSignalTerminated
if errors.As(err, &userTerminatedErr) {
s, ok := userTerminatedErr.signal.(syscall.Signal)
if !ok {
return 1
}
return 128 + int(s)
}

var stErr cli.StatusError
if errors.As(err, &stErr) && stErr.StatusCode != 0 { // FIXME(thaJeztah): StatusCode should never be used with a zero status-code. Check if we do this anywhere.
return stErr.StatusCode
Expand Down
33 changes: 33 additions & 0 deletions cmd/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"context"
"io"
"os"
"syscall"
"testing"
"time"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
Expand Down Expand Up @@ -75,3 +79,32 @@ func TestVersion(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(b.String(), "Docker version"))
}

func TestUserTerminatedError(t *testing.T) {
ctx, cancel := context.WithTimeoutCause(context.Background(), time.Second*1, errors.New("test timeout"))
t.Cleanup(cancel)

notifyCtx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
t.Cleanup(cancelNotify)

syscall.Kill(syscall.Getpid(), syscall.SIGINT)

<-notifyCtx.Done()
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
signal: syscall.SIGINT,
})

assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 130)

notifyCtx, cancelNotify = notifyContext(ctx, platformsignals.TerminationSignals...)
t.Cleanup(cancelNotify)

syscall.Kill(syscall.Getpid(), syscall.SIGTERM)

<-notifyCtx.Done()
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
signal: syscall.SIGTERM,
})

assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 143)
}

0 comments on commit 9005f36

Please sign in to comment.