2026-02-17 18:01:29 +00:00
|
|
|
//go:build !windows
|
|
|
|
|
|
|
|
|
|
package tools
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"syscall"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func processExists(pid int) bool {
|
|
|
|
|
if pid <= 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
err := syscall.Kill(pid, 0)
|
|
|
|
|
return err == nil || err == syscall.EPERM
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestShellTool_TimeoutKillsChildProcess(t *testing.T) {
|
2026-02-28 08:24:26 +00:00
|
|
|
tool, err := NewExecTool(t.TempDir(), false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Errorf("unable to configure exec tool: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 18:01:29 +00:00
|
|
|
tool.SetTimeout(500 * time.Millisecond)
|
|
|
|
|
|
2026-02-20 18:03:11 +00:00
|
|
|
args := map[string]any{
|
feat(tools): add exec tool enhancement with background execution and PTY support (#1752)
- Unified exec tool with actions: run/list/poll/read/write/send-keys/kill
- PTY support using creack/pty library
- Process session management with background execution
- Process group kill for cleaning up child processes
- Session cleanup: 30-minute TTL for old sessions
- Output buffer: 100MB limit with truncation
Actions:
- run: execute command (sync or background)
- list: list all sessions
- poll: check session status
- read: read session output
- write: send input to session stdin
- send-keys: send special keys (up, down, ctrl-c, enter, etc.)
- kill: terminate session
Tests:
- PTY: allowed commands, write/read, poll, kill, process group kill
- Non-PTY: background execution, list, read, write, poll, kill, process group kill
- Session management: add/get/remove/list/cleanup
2026-03-21 14:38:03 +00:00
|
|
|
"action": "run",
|
2026-02-17 18:01:29 +00:00
|
|
|
// Spawn a child process that would outlive the shell unless process-group kill is used.
|
|
|
|
|
"command": "sleep 60 & echo $! > child.pid; wait",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := tool.Execute(context.Background(), args)
|
|
|
|
|
if !result.IsError {
|
|
|
|
|
t.Fatalf("expected timeout error, got success: %s", result.ForLLM)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(result.ForLLM, "timed out") {
|
|
|
|
|
t.Fatalf("expected timeout message, got: %s", result.ForLLM)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
childPIDPath := filepath.Join(tool.workingDir, "child.pid")
|
|
|
|
|
data, err := os.ReadFile(childPIDPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to read child pid file: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to parse child pid: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
|
|
|
for time.Now().Before(deadline) {
|
|
|
|
|
if !processExists(childPID) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Fatalf("child process %d is still running after timeout", childPID)
|
|
|
|
|
}
|