cmd/docker: add cause to user-terminated context.Context
This patch adds a "cause" to the `context.Context` error when the user terminates the process through SIGINT/SIGTERM. This allows us to distinguish the cause of the `context.Context` cancellation. In future we would also be able to improve the UX of printed errors based on the underlying cause. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> cmd/docker: fix possible race between ctx channel and signal channel Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> test: notifyContext Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> cmd/docker: print status on SIGTERM and not SIGINT Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
parent
f97ec69122
commit
c51be77767
@ -28,16 +28,57 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type errCtxSignalTerminated struct {
|
||||||
|
signal os.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errCtxSignalTerminated) Error() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
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) {
|
if err != nil && !errdefs.IsCancelled(err) {
|
||||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(getExitCode(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 {
|
func dockerMain(ctx context.Context) error {
|
||||||
ctx, cancelNotify := signal.NotifyContext(ctx, platformsignals.TerminationSignals...)
|
ctx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
|
||||||
defer cancelNotify()
|
defer cancelNotify()
|
||||||
|
|
||||||
dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
|
dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
|
||||||
@ -57,6 +98,16 @@ func getExitCode(err error) int {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return 0
|
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
|
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.
|
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
|
return stErr.StatusCode
|
||||||
|
@ -5,10 +5,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/cli/cli/debug"
|
||||||
|
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
@ -75,3 +79,32 @@ func TestVersion(t *testing.T) {
|
|||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Check(t, is.Contains(b.String(), "Docker version"))
|
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)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user