mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
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:
+22
-1
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/memohai/memoh/internal/workspace/bridge"
|
||||
pb "github.com/memohai/memoh/internal/workspace/bridgepb"
|
||||
)
|
||||
|
||||
@@ -50,9 +52,10 @@ func (h *ContainerdHandler) GetTerminalInfo(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, terminalInfoResponse{Available: false})
|
||||
}
|
||||
|
||||
shell := detectShell(ctx, client)
|
||||
return c.JSON(http.StatusOK, terminalInfoResponse{
|
||||
Available: true,
|
||||
Shell: "/bin/sh",
|
||||
Shell: shell,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,7 +95,8 @@ func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error {
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
execStream, err := client.ExecStreamPTY(ctx, "/bin/sh", "/data", cols, rows)
|
||||
shell := detectShell(ctx, client)
|
||||
execStream, err := client.ExecStreamPTY(ctx, shell, "/data", cols, rows)
|
||||
if err != nil {
|
||||
_ = conn.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "exec failed"))
|
||||
@@ -154,6 +158,18 @@ func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectShell probes the container for an interactive shell with readline support.
|
||||
// Prefers bash > zsh > /bin/sh.
|
||||
func detectShell(ctx context.Context, client *bridge.Client) string {
|
||||
for _, sh := range []string{"/bin/bash", "/usr/bin/bash", "/bin/zsh", "/usr/bin/zsh"} {
|
||||
result, err := client.Exec(ctx, "test -x "+sh, "/", 5)
|
||||
if err == nil && result.ExitCode == 0 {
|
||||
return sh
|
||||
}
|
||||
}
|
||||
return "/bin/sh"
|
||||
}
|
||||
|
||||
func parseUint32Query(c echo.Context, name string, fallback uint32) uint32 {
|
||||
raw := c.QueryParam(name)
|
||||
if raw == "" {
|
||||
|
||||
Reference in New Issue
Block a user