Merge pull request #28957 from shuaiyuanxx/exec-honor-console-size

exec: honor ConsoleSize so the terminal is sized at creation
This commit is contained in:
Paul Holzinger 2026-06-22 16:30:13 +02:00 committed by GitHub
commit eb3c0f4c1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 63 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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