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;
6f856263c2/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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-05-09 15:21:14 +02:00
parent 6f856263c2
commit a17b9c542b
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C

View File

@ -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.