Skip to content

Commit 9005f36

Browse files
authored
Merge pull request #5760 from Benehiko/user-terminated-ctx-err
cmd/docker: add cause to user-terminated `context.Context`
2 parents dff0dc8 + c51be77 commit 9005f36

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

cmd/docker/docker.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,57 @@ import (
2828
"go.opentelemetry.io/otel"
2929
)
3030

31+
type errCtxSignalTerminated struct {
32+
signal os.Signal
33+
}
34+
35+
func (e errCtxSignalTerminated) Error() string {
36+
return ""
37+
}
38+
3139
func main() {
32-
err := dockerMain(context.Background())
40+
ctx := context.Background()
41+
err := dockerMain(ctx)
42+
43+
if errors.As(err, &errCtxSignalTerminated{}) {
44+
os.Exit(getExitCode(err))
45+
return
46+
}
47+
3348
if err != nil && !errdefs.IsCancelled(err) {
3449
_, _ = fmt.Fprintln(os.Stderr, err)
3550
os.Exit(getExitCode(err))
3651
}
3752
}
3853

54+
func notifyContext(ctx context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) {
55+
ch := make(chan os.Signal, 1)
56+
signal.Notify(ch, signals...)
57+
58+
ctxCause, cancel := context.WithCancelCause(ctx)
59+
60+
go func() {
61+
select {
62+
case <-ctx.Done():
63+
signal.Stop(ch)
64+
return
65+
case sig := <-ch:
66+
cancel(errCtxSignalTerminated{
67+
signal: sig,
68+
})
69+
signal.Stop(ch)
70+
return
71+
}
72+
}()
73+
74+
return ctxCause, func() {
75+
signal.Stop(ch)
76+
cancel(nil)
77+
}
78+
}
79+
3980
func dockerMain(ctx context.Context) error {
40-
ctx, cancelNotify := signal.NotifyContext(ctx, platformsignals.TerminationSignals...)
81+
ctx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
4182
defer cancelNotify()
4283

4384
dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
@@ -57,6 +98,16 @@ func getExitCode(err error) int {
5798
if err == nil {
5899
return 0
59100
}
101+
102+
var userTerminatedErr errCtxSignalTerminated
103+
if errors.As(err, &userTerminatedErr) {
104+
s, ok := userTerminatedErr.signal.(syscall.Signal)
105+
if !ok {
106+
return 1
107+
}
108+
return 128 + int(s)
109+
}
110+
60111
var stErr cli.StatusError
61112
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.
62113
return stErr.StatusCode

cmd/docker/docker_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import (
55
"context"
66
"io"
77
"os"
8+
"syscall"
89
"testing"
10+
"time"
911

1012
"github.com/docker/cli/cli/command"
1113
"github.com/docker/cli/cli/debug"
14+
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
15+
"github.com/pkg/errors"
1216
"github.com/sirupsen/logrus"
1317
"gotest.tools/v3/assert"
1418
is "gotest.tools/v3/assert/cmp"
@@ -75,3 +79,32 @@ func TestVersion(t *testing.T) {
7579
assert.NilError(t, err)
7680
assert.Check(t, is.Contains(b.String(), "Docker version"))
7781
}
82+
83+
func TestUserTerminatedError(t *testing.T) {
84+
ctx, cancel := context.WithTimeoutCause(context.Background(), time.Second*1, errors.New("test timeout"))
85+
t.Cleanup(cancel)
86+
87+
notifyCtx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
88+
t.Cleanup(cancelNotify)
89+
90+
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
91+
92+
<-notifyCtx.Done()
93+
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
94+
signal: syscall.SIGINT,
95+
})
96+
97+
assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 130)
98+
99+
notifyCtx, cancelNotify = notifyContext(ctx, platformsignals.TerminationSignals...)
100+
t.Cleanup(cancelNotify)
101+
102+
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
103+
104+
<-notifyCtx.Done()
105+
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
106+
signal: syscall.SIGTERM,
107+
})
108+
109+
assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 143)
110+
}

0 commit comments

Comments
 (0)