From a17b9c542bfa1b83f89e3ea6118fea253cba9d02 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 9 May 2025 15:21:14 +0200 Subject: [PATCH] restore terminal when terminating after 3 signals When attaching to a container, hijack puts the terminal in raw mode, and local echo is disabled. In normal cases, the terminal is restored once the container detaches; https://github.com/docker/cli/blob/6f856263c2f03a8dc19cef3d6cb48d56a3fab5cc/cli/command/container/hijack.go#L40-L44 However, when the CLI is forced to exit (after 3 signals), we `os.Exit(1)`, which causes defers to not be executed, and because of this, the terminal not being restored. For example; start a container that's attached; docker run -it --rm --sig-proxy=false alpine sleep 20 In another terminal send a SIGINT 3 times to force terminate; kill -sINT $(pgrep -af docker\ run) kill -sINT $(pgrep -af docker\ run) kill -sINT $(pgrep -af docker\ run) The first terminal shows that the docker cli was terminated; got 3 SIGTERM/SIGINTs, forcefully exiting However, the terminal was not restored, so local echo is disabled, and typing any command in the terminal does not show output (a manual `stty echo` is needed to restore). With this patch, the terminal is restored before we forcefully exit the docker CLI. Restoring is a no-op if there's no previous state, so we can unconditionally execute this. Signed-off-by: Sebastiaan van Stijn --- cmd/docker/docker.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 43cffd8eca..f1f54b98f7 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "os" "os/exec" "os/signal" @@ -341,6 +340,9 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command if force { _ = plugincmd.Process.Kill() _, _ = fmt.Fprint(dockerCli.Err(), "got 3 SIGTERM/SIGINTs, forcefully exiting\n") + + // Restore terminal in case it was in raw mode. + restoreTerminal(dockerCli) os.Exit(1) } } @@ -388,7 +390,7 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command // to be caught and the context to be marked as done, then registers a new // signal handler for subsequent signals. It forces the process to exit // after 3 SIGTERM/SIGINT signals. -func forceExitAfter3TerminationSignals(ctx context.Context, w io.Writer) { +func forceExitAfter3TerminationSignals(ctx context.Context, streams command.Streams) { // wait for the first signal to be caught and the context to be marked as done <-ctx.Done() // register a new signal handler for subsequent signals @@ -399,10 +401,22 @@ func forceExitAfter3TerminationSignals(ctx context.Context, w io.Writer) { for i := 0; i < 2; i++ { <-sig } - _, _ = fmt.Fprint(w, "\ngot 3 SIGTERM/SIGINTs, forcefully exiting\n") + _, _ = fmt.Fprint(streams.Err(), "\ngot 3 SIGTERM/SIGINTs, forcefully exiting\n") + + // Restore terminal in case it was in raw mode. + restoreTerminal(streams) os.Exit(1) } +// restoreTerminal restores the terminal if it was in raw mode; this prevents +// local echo from being disabled for the current terminal after forceful +// termination. It's a no-op if there's no prior state to restore. +func restoreTerminal(streams command.Streams) { + streams.In().RestoreTerminal() + streams.Out().RestoreTerminal() + streams.Err().RestoreTerminal() +} + //nolint:gocyclo func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { tcmd := newDockerCommand(dockerCli) @@ -468,7 +482,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { // This is a fallback for the case where the command does not exit // based on context cancellation. - go forceExitAfter3TerminationSignals(ctx, dockerCli.Err()) + go forceExitAfter3TerminationSignals(ctx, dockerCli) // We've parsed global args already, so reset args to those // which remain.