diff --git a/cmd/podman/containers/exec.go b/cmd/podman/containers/exec.go index a022739b02..b349e77244 100644 --- a/cmd/podman/containers/exec.go +++ b/cmd/podman/containers/exec.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "go.podman.io/common/pkg/completion" + "go.podman.io/common/pkg/resize" "go.podman.io/podman/v6/cmd/podman/common" "go.podman.io/podman/v6/cmd/podman/registry" "go.podman.io/podman/v6/cmd/podman/validate" @@ -18,6 +19,7 @@ import ( "go.podman.io/podman/v6/pkg/domain/entities" envLib "go.podman.io/podman/v6/pkg/env" "go.podman.io/podman/v6/pkg/rootless" + "golang.org/x/term" ) var ( @@ -189,6 +191,16 @@ func exec(cmd *cobra.Command, args []string) error { streams.AttachOutput = true streams.AttachError = true + // When allocating a TTY, capture the current terminal size up front so the + // exec pseudo-terminal is created at the right size, rather than relying on + // the asynchronous resize that follows attach. This matters for short-lived + // commands that read their window size at startup (e.g. `stty size`). + if execOpts.Tty { + if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil { + execOpts.ConsoleSize = &resize.TerminalSize{Width: uint16(w), Height: uint16(h)} + } + } + if execNoSession { exitCode, err := registry.ContainerEngine().ContainerExecNoSession(registry.Context(), nameOrID, execOpts, streams) registry.SetExitCode(exitCode) diff --git a/libpod/container_exec.go b/libpod/container_exec.go index 52169565cb..7dca91e524 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -29,6 +29,12 @@ type ExecConfig struct { Command []string `json:"command"` // Terminal is whether the exec session will allocate a pseudoterminal. Terminal bool `json:"terminal,omitempty"` + // ConsoleSize is an optional initial size for the exec session's + // pseudoterminal, applied at creation when Terminal is true. This lets a + // one-shot exec read the correct window size immediately instead of + // relying on an asynchronous resize that may arrive after the process has + // already started. + ConsoleSize *resize.TerminalSize `json:"consoleSize,omitempty"` // AttachStdin is whether the STDIN stream will be forwarded to the exec // session's first process when attaching. Only available if Terminal is // false. @@ -1180,6 +1186,7 @@ func prepareForExec(c *Container, session *ExecSession) (*ExecOptions, error) { opts.ExitCommand = session.Config.ExitCommand opts.ExitCommandDelay = session.Config.ExitCommandDelay opts.Privileged = session.Config.Privileged + opts.ConsoleSize = session.Config.ConsoleSize return opts, nil } diff --git a/libpod/oci.go b/libpod/oci.go index 3aedcad0b2..fa7b10dc35 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -201,6 +201,9 @@ type ExecOptions struct { Env map[string]string // Terminal is whether to create a new TTY for the exec session. Terminal bool + // ConsoleSize is an optional initial size for the exec session's TTY, + // applied at creation when Terminal is true. + ConsoleSize *resize.TerminalSize // Cwd is the working directory for the executed command. If unset, the // working directory of the container will be used. Cwd string diff --git a/libpod/oci_conmon_exec_common.go b/libpod/oci_conmon_exec_common.go index 8037e07be0..c002e04f2f 100644 --- a/libpod/oci_conmon_exec_common.go +++ b/libpod/oci_conmon_exec_common.go @@ -704,6 +704,16 @@ func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessi pspec.Terminal = false if options.Terminal { pspec.Terminal = true + // Size the exec PTY at creation so the process reads the correct + // window size immediately. Without this the PTY starts at 0x0 and is + // only corrected by an asynchronous resize, which can arrive after a + // one-shot process has already read its size. + if options.ConsoleSize != nil { + pspec.ConsoleSize = &spec.Box{ + Height: uint(options.ConsoleSize.Height), + Width: uint(options.ConsoleSize.Width), + } + } } if len(env) > 0 { pspec.Env = append(pspec.Env, env...) diff --git a/pkg/api/handlers/compat/exec.go b/pkg/api/handlers/compat/exec.go index a3b26e362a..b7276a17c3 100644 --- a/pkg/api/handlers/compat/exec.go +++ b/pkg/api/handlers/compat/exec.go @@ -42,6 +42,14 @@ func ExecCreateHandler(w http.ResponseWriter, r *http.Request) { libpodConfig := new(libpod.ExecConfig) libpodConfig.Command = input.Cmd libpodConfig.Terminal = input.Tty + // Preserve the requested initial terminal size so the exec PTY can be sized + // at creation (docker sends ConsoleSize as [height, width]). + if input.ConsoleSize != nil { + libpodConfig.ConsoleSize = &resize.TerminalSize{ + Height: uint16(input.ConsoleSize[0]), + Width: uint16(input.ConsoleSize[1]), + } + } libpodConfig.AttachStdin = input.AttachStdin libpodConfig.AttachStderr = input.AttachStderr libpodConfig.AttachStdout = input.AttachStdout diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 6e9e200077..6b13013fbc 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -7,6 +7,7 @@ import ( "time" nettypes "go.podman.io/common/libnetwork/types" + "go.podman.io/common/pkg/resize" imageTypes "go.podman.io/image/v5/types" "go.podman.io/podman/v6/libpod/define" "go.podman.io/podman/v6/pkg/domain/entities/types" @@ -283,6 +284,7 @@ type ContainerLogsOptions struct { // a container type ExecOptions struct { Cmd []string + ConsoleSize *resize.TerminalSize DetachKeys string Envs map[string]string Interactive bool diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 4f4b3940fa..6f8025ccfb 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -890,6 +890,13 @@ func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime, noSession execConfig.PreserveFD = options.PreserveFD execConfig.AttachStdin = options.Interactive + // Size the exec terminal at creation so a process that reads its window size + // at startup (e.g. `stty size`) observes the right value, rather than relying + // on the asynchronous resize that follows attach. + if options.Tty { + execConfig.ConsoleSize = options.ConsoleSize + } + // Only set up exit command for regular exec sessions, not no-session mode if !noSession { // Make an exit command diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 66df7420ec..6e7be5b1f2 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -618,6 +618,12 @@ func makeExecConfig(options entities.ExecOptions) *handlers.ExecCreateConfig { createConfig.Env = env createConfig.WorkingDir = options.WorkDir createConfig.Cmd = options.Cmd + // Size the exec terminal at creation, mirroring the local path, so the + // service does not have to rely on the asynchronous resize that follows + // attach. ConsoleSize is ordered [height, width]. + if options.Tty && options.ConsoleSize != nil { + createConfig.ConsoleSize = &[2]uint{uint(options.ConsoleSize.Height), uint(options.ConsoleSize.Width)} + } return createConfig } diff --git a/test/system/450-interactive.bats b/test/system/450-interactive.bats index 31f01f6424..6b800e4028 100644 --- a/test/system/450-interactive.bats +++ b/test/system/450-interactive.bats @@ -70,14 +70,15 @@ function teardown() { run_podman rm -t 0 -f mystty - # FIXME: the checks below are flaking a lot (see #10710). + # The same must hold for podman exec. The exec pseudo-terminal is now sized + # at creation (honoring the requested ConsoleSize), so stty reads the right + # dimensions immediately instead of racing the asynchronous resize that + # previously followed attach (see #10710). + run_podman run -d --name mystty $IMAGE top + run_podman exec -it mystty stty size <$PODMAN_TEST_PTY + is "$output" "$rows $cols$CR" "stty under podman exec reads the correct dimensions" - # check that the same works for podman exec -# run_podman run -d --name mystty $IMAGE top -# run_podman exec -it mystty stty size <$PODMAN_TEST_PTY -# is "$output" "$rows $cols" "stty under podman exec reads the correct dimensions" -# -# run_podman rm -t 0 -f mystty + run_podman rm -t 0 -f mystty }