From f9c71a6cf68988ed8bb5cedcc4f6754ad14b90f8 Mon Sep 17 00:00:00 2001 From: zhaji2333 Date: Sun, 16 Aug 2026 17:25:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8C=89=E5=B9=B3=E5=8F=B0=E9=80=89?= =?UTF-8?q?=E6=8B=A9=20shell=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Windows=20?= =?UTF-8?q?=E4=B8=8A=20execute=20=E5=B7=A5=E5=85=B7=20/bin/sh=20not=20foun?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execute 流式 shell 硬编码 /bin/sh -c,Windows 平台直接报 exec: "/bin/sh": executable file not found。按 runtime.GOOS 选择: Windows 走 cmd /c,类 Unix 保持 /bin/sh -c 不变;同时 Windows 下跳过 sh 专用命令预处理(export、/dev/null 等),避免 cmd 执行失败。 为依赖 sh 语法断言的流式测试补充 Windows skip(修复前后在 Windows 均无法通过)。 --- internal/security/shell_execute_stream.go | 28 ++++++++++++++++--- .../security/shell_execute_stream_test.go | 13 +++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/internal/security/shell_execute_stream.go b/internal/security/shell_execute_stream.go index 02c5cb741..080272cdb 100644 --- a/internal/security/shell_execute_stream.go +++ b/internal/security/shell_execute_stream.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os/exec" + "runtime" "sync" "github.com/cloudwego/eino/adk/filesystem" @@ -42,6 +43,15 @@ func NewEinoStreamingShell() *EinoStreamingShell { return &EinoStreamingShell{} } +// newShellCommand 按平台选择 shell:Windows 使用 cmd.exe,类 Unix 使用 /bin/sh。 +// 修复 Windows 平台 execute 工具硬编码 /bin/sh 导致 exec: "/bin/sh": executable file not found。 +func newShellCommand(ctx context.Context, command string) *exec.Cmd { + if runtime.GOOS == "windows" { + return exec.CommandContext(ctx, "cmd", "/c", command) + } + return exec.CommandContext(ctx, "/bin/sh", "-c", command) +} + // ExecuteStreaming 实现 filesystem.StreamingShell。 func (s *EinoStreamingShell) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { if input == nil || input.Command == "" { @@ -60,8 +70,8 @@ func (s *EinoStreamingShell) ExecuteStreaming(ctx context.Context, input *filesy func runShellInBackground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) { defer w.Close() - command = PrepareShellCommandForExecute(command) - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) + command = prepareShellCommandForPlatform(command) + cmd := newShellCommand(ctx, command) applyDefaultTerminalEnv(cmd) attachNonInteractiveStdin(cmd) stdout, err := cmd.StdoutPipe() @@ -120,8 +130,8 @@ func drainShellPipes(stdout, stderr io.Reader) { func streamShellForeground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) { defer w.Close() - command = PrepareShellCommandForExecute(command) - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) + command = prepareShellCommandForPlatform(command) + cmd := newShellCommand(ctx, command) applyDefaultTerminalEnv(cmd) attachNonInteractiveStdin(cmd) @@ -209,3 +219,13 @@ func streamShellForeground(ctx context.Context, command string, w *schema.Stream } _ = w.Send(nil, fmt.Errorf("command failed: %w", waitErr)) } + +// prepareShellCommandForPlatform 按平台预处理命令: +// - Windows(cmd.exe):不注入 sh 专用指令,避免 /dev/null、export 等导致命令失败; +// - 类 Unix(/bin/sh):保持原有非交互 + 后台 IO 重定向包装,行为不变。 +func prepareShellCommandForPlatform(command string) string { + if runtime.GOOS == "windows" { + return command + } + return PrepareShellCommandForExecute(command) +} diff --git a/internal/security/shell_execute_stream_test.go b/internal/security/shell_execute_stream_test.go index 938f29942..4927a9f2b 100644 --- a/internal/security/shell_execute_stream_test.go +++ b/internal/security/shell_execute_stream_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "runtime" "strings" "testing" "time" @@ -12,6 +13,9 @@ import ( ) func TestEinoStreamingShell_StreamsStderrBeforeStdoutEOF(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } shell := NewEinoStreamingShell() cmd := PrepareNonInteractiveShellCommand("echo err-only >&2; exit 1") sr, err := shell.ExecuteStreaming(context.Background(), &filesystem.ExecuteRequest{Command: cmd}) @@ -43,6 +47,9 @@ func TestEinoStreamingShell_StreamsStderrBeforeStdoutEOF(t *testing.T) { } func TestEinoStreamingShell_SudoFailsFast(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sudo is unix-only") + } shell := NewEinoStreamingShell() cmd := PrepareNonInteractiveShellCommand("sudo whoami && sudo cat /etc/os-release") sr, err := shell.ExecuteStreaming(context.Background(), &filesystem.ExecuteRequest{Command: cmd}) @@ -79,6 +86,9 @@ func TestEinoStreamingShell_SudoFailsFast(t *testing.T) { } func TestEinoStreamingShell_StderrWhileStdoutBlocks(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } shell := NewEinoStreamingShell() // 模拟 sudo:stderr 先有输出,stdout 侧进程仍挂起;旧 eino local 在首包 stderr 前不会向流写任何内容。 cmd := PrepareNonInteractiveShellCommand(`echo "password prompt" >&2; sleep 30`) @@ -118,6 +128,9 @@ func TestEinoStreamingShell_StderrWhileStdoutBlocks(t *testing.T) { // TestEinoStreamingShell_BackgroundJobDoesNotHoldPipe 模拟 cmd & 后继续前台逻辑:重定向后应快速结束。 func TestEinoStreamingShell_BackgroundJobDoesNotHoldPipe(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } if testing.Short() { t.Skip("skipping shell integration in -short") }