|
1 | 1 | package builtin |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
5 | 6 | "encoding/json" |
6 | 7 | "fmt" |
7 | 8 | "os" |
8 | 9 | "os/exec" |
9 | 10 | "runtime" |
| 11 | + "strings" |
10 | 12 |
|
11 | 13 | "github.com/docker/cagent/pkg/tools" |
12 | 14 | ) |
@@ -36,37 +38,67 @@ func (h *shellHandler) RunShell(ctx context.Context, toolCall tools.ToolCall) (* |
36 | 38 | return nil, fmt.Errorf("invalid arguments: %w", err) |
37 | 39 | } |
38 | 40 |
|
39 | | - cmd := exec.CommandContext(ctx, h.shell, append(h.shellArgsPrefix, params.Cmd)...) |
| 41 | + cmd := exec.Command(h.shell, append(h.shellArgsPrefix, params.Cmd)...) |
40 | 42 | cmd.Env = h.env |
41 | 43 | if params.Cwd != "" { |
42 | 44 | cmd.Dir = params.Cwd |
43 | 45 | } else { |
44 | | - // Use the current working directory; avoid PWD on Windows (may be MSYS-style like /c/...) |
45 | 46 | if wd, err := os.Getwd(); err == nil { |
46 | 47 | cmd.Dir = wd |
47 | 48 | } |
48 | 49 | } |
49 | 50 |
|
50 | | - // Set up process group for proper cleanup |
51 | | - // On Unix: create new process group so we can kill the entire tree |
52 | 51 | cmd.SysProcAttr = platformSpecificSysProcAttr() |
53 | 52 |
|
54 | | - output, err := cmd.CombinedOutput() |
55 | | - if err != nil { |
| 53 | + var outBuf bytes.Buffer |
| 54 | + cmd.Stdout = &outBuf |
| 55 | + cmd.Stderr = &outBuf |
| 56 | + |
| 57 | + if err := cmd.Start(); err != nil { |
56 | 58 | return &tools.ToolCallResult{ |
57 | | - Output: fmt.Sprintf("Error executing command: %s\nOutput: %s", err, string(output)), |
| 59 | + Output: fmt.Sprintf("Error starting command: %s", err), |
58 | 60 | }, nil |
59 | 61 | } |
60 | 62 |
|
61 | | - if len(output) == 0 { |
| 63 | + pg, err := createProcessGroup(cmd.Process) |
| 64 | + if err != nil { |
62 | 65 | return &tools.ToolCallResult{ |
63 | | - Output: "<no output>", |
| 66 | + Output: fmt.Sprintf("Error creating process group: %s", err), |
64 | 67 | }, nil |
65 | 68 | } |
66 | 69 |
|
67 | | - return &tools.ToolCallResult{ |
68 | | - Output: fmt.Sprintf("Output: %s", string(output)), |
69 | | - }, nil |
| 70 | + done := make(chan error, 1) |
| 71 | + go func() { |
| 72 | + done <- cmd.Wait() |
| 73 | + }() |
| 74 | + |
| 75 | + select { |
| 76 | + case <-ctx.Done(): |
| 77 | + if cmd.Process != nil { |
| 78 | + _ = kill(cmd.Process, pg) |
| 79 | + } |
| 80 | + return &tools.ToolCallResult{ |
| 81 | + Output: "Command cancelled", |
| 82 | + }, nil |
| 83 | + case err := <-done: |
| 84 | + output := outBuf.String() |
| 85 | + |
| 86 | + if err != nil { |
| 87 | + return &tools.ToolCallResult{ |
| 88 | + Output: fmt.Sprintf("Error executing command: %s\nOutput: %s", err, output), |
| 89 | + }, nil |
| 90 | + } |
| 91 | + |
| 92 | + if strings.TrimSpace(output) == "" { |
| 93 | + return &tools.ToolCallResult{ |
| 94 | + Output: "<no output>", |
| 95 | + }, nil |
| 96 | + } |
| 97 | + |
| 98 | + return &tools.ToolCallResult{ |
| 99 | + Output: fmt.Sprintf("Output: %s", output), |
| 100 | + }, nil |
| 101 | + } |
70 | 102 | } |
71 | 103 |
|
72 | 104 | func NewShellTool(env []string) *ShellTool { |
|
0 commit comments