diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 2b4bcb638e..e81bc21eb9 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -27,6 +27,7 @@ import ( "path/filepath" "strconv" "strings" + "sync/atomic" "syscall" "github.com/compose-spec/compose-go/v2/cli" @@ -108,10 +109,12 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithCancel(cmd.Context()) + var caughtSignal atomic.Value s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGTERM, syscall.SIGINT) go func() { - <-s + sig := <-s + caughtSignal.Store(sig) cancel() signal.Stop(s) close(s) @@ -119,6 +122,11 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error { err := fn(ctx, cmd, args) if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) { + if sig, ok := caughtSignal.Load().(os.Signal); ok { + reraiseSignal(sig) + // On Unix, process dies here from signal. + // On Windows (or fallback), continues below. + } err = dockercli.StatusError{ StatusCode: 130, } diff --git a/cmd/compose/signal_unix.go b/cmd/compose/signal_unix.go new file mode 100644 index 0000000000..8c4b67628f --- /dev/null +++ b/cmd/compose/signal_unix.go @@ -0,0 +1,32 @@ +//go:build !windows + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import ( + "os" + "os/signal" + "syscall" +) + +func reraiseSignal(sig os.Signal) { + if s, ok := sig.(syscall.Signal); ok { + signal.Reset(s) + _ = syscall.Kill(syscall.Getpid(), s) + } +} diff --git a/cmd/compose/signal_windows.go b/cmd/compose/signal_windows.go new file mode 100644 index 0000000000..8e19e57b63 --- /dev/null +++ b/cmd/compose/signal_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +import "os" + +// reraiseSignal is a no-op on Windows as signal re-raising for parent +// process detection is not supported. Falls through to os.Exit(130). +func reraiseSignal(_ os.Signal) {} diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 8a02dc719b..1bcc5547ae 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -211,7 +211,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser Status: api.Warning, Text: "Interrupted", }) - return "", nil + return "", ctx.Err() } // check if has error and the service has a build section diff --git a/pkg/e2e/cancel_test.go b/pkg/e2e/cancel_test.go index 64f3ff609a..6321c0f815 100644 --- a/pkg/e2e/cancel_test.go +++ b/pkg/e2e/cancel_test.go @@ -74,9 +74,8 @@ func TestComposeCancel(t *testing.T) { case <-ctx.Done(): t.Fatal("test context canceled") case err := <-processDone: - // TODO(milas): Compose should really not return exit code 130 here, - // this is an old hack for the compose-cli wrapper - assert.Error(t, err, "exit status 130", + // Process should be killed by re-raised SIGINT signal + assert.ErrorContains(t, err, "signal: interrupt", "STDOUT:\n%s\nSTDERR:\n%s\n", stdout.String(), stderr.String()) case <-time.After(10 * time.Second): t.Fatal("timeout waiting for Compose exit") diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index d34f2061e2..d8d785d7b6 100644 --- a/pkg/e2e/up_test.go +++ b/pkg/e2e/up_test.go @@ -94,11 +94,13 @@ func TestUpDependenciesNotStopped(t *testing.T) { err = cmd.Wait() if err != nil { var exitErr *exec.ExitError - errors.As(err, &exitErr) - if exitErr.ExitCode() == -1 { - t.Fatalf("`compose up` was killed: %v", err) + if !errors.As(err, &exitErr) { + t.Fatalf("`compose up` failed with non-exit error: %v", err) } - require.Equal(t, 130, exitErr.ExitCode()) + // Process is expected to die from re-raised SIGINT signal (exit code -1). + // If signal re-raise doesn't terminate the process, the fallback path exits with code 130. + assert.Assert(t, exitErr.ExitCode() == -1 || exitErr.ExitCode() == 130, + "`compose up` exited with unexpected code: %d (%v)", exitErr.ExitCode(), err) } RequireServiceState(t, c, "app", "exited")