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
|
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.Dir = workDir
|
||||||
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
cmd.Env = append(os.Environ(), firstMsg.GetEnv()...)
|
||||||
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
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
|
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) {
|
func streamPipe(stream pb.ContainerService_ExecServer, r io.Reader, st pb.ExecOutput_Stream) {
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
|
||||||
|
"github.com/memohai/memoh/internal/workspace/bridge"
|
||||||
pb "github.com/memohai/memoh/internal/workspace/bridgepb"
|
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})
|
return c.JSON(http.StatusOK, terminalInfoResponse{Available: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shell := detectShell(ctx, client)
|
||||||
return c.JSON(http.StatusOK, terminalInfoResponse{
|
return c.JSON(http.StatusOK, terminalInfoResponse{
|
||||||
Available: true,
|
Available: true,
|
||||||
Shell: "/bin/sh",
|
Shell: shell,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +95,8 @@ func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
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 {
|
if err != nil {
|
||||||
_ = conn.WriteMessage(websocket.CloseMessage,
|
_ = conn.WriteMessage(websocket.CloseMessage,
|
||||||
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "exec failed"))
|
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, "exec failed"))
|
||||||
@@ -154,6 +158,18 @@ func (h *ContainerdHandler) HandleTerminalWS(c echo.Context) error {
|
|||||||
return nil
|
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 {
|
func parseUint32Query(c echo.Context, name string, fallback uint32) uint32 {
|
||||||
raw := c.QueryParam(name)
|
raw := c.QueryParam(name)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user