fix: use readline-capable shell for interactive terminal sessions

Container terminals were echoing raw ANSI escape sequences (^[[A, ^[[B,
etc.) instead of handling arrow keys because /bin/sh (dash/ash) lacks
readline support. Two changes fix this:

1. Bridge execPTY now directly exec's bare paths (e.g. /bin/bash) instead
   of always wrapping through "/bin/sh -c", preserving readline behavior.
2. Terminal handler detects bash/zsh in the container and prefers them
   over /bin/sh for interactive PTY sessions.
This commit is contained in:
Acbox
2026-03-29 19:31:24 +08:00
parent 7825b49ff3
commit 86d83108d9
2 changed files with 40 additions and 3 deletions
+22 -1
View File
@@ -194,7 +194,12 @@ func execPTY(stream pb.ContainerService_ExecServer, firstMsg *pb.ExecInput) erro
workDir = defaultWorkDir
}
cmd := exec.CommandContext(stream.Context(), "/bin/sh", "-c", command) //nolint:gosec // G204: intentional
var cmd *exec.Cmd
if isBarePath(command) {
cmd = exec.CommandContext(stream.Context(), command) //nolint:gosec // G204: intentional
} else {
cmd = exec.CommandContext(stream.Context(), "/bin/sh", "-c", command) //nolint:gosec // G204: intentional
}
cmd.Dir = workDir
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
@@ -474,6 +479,22 @@ func (*containerServer) Rename(_ context.Context, req *pb.RenameRequest) (*pb.Re
return &pb.RenameResponse{}, nil
}
// isBarePath returns true when the command string is a plain executable path
// (no spaces, shell operators, or arguments) so it can be exec'd directly
// without wrapping in "/bin/sh -c". This matters for interactive PTY shells
// where /bin/sh -c wrapping would strip readline support.
func isBarePath(cmd string) bool {
if cmd == "" {
return false
}
for _, c := range cmd {
if c == ' ' || c == '\t' || c == '|' || c == '&' || c == ';' || c == '>' || c == '<' || c == '$' || c == '(' || c == ')' || c == '`' {
return false
}
}
return strings.HasPrefix(cmd, "/") || !strings.Contains(cmd, "/")
}
func streamPipe(stream pb.ContainerService_ExecServer, r io.Reader, st pb.ExecOutput_Stream) {
buf := make([]byte, 4096)
for {