mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-17 23:22:27 +02:00
199 lines
5.3 KiB
Go
199 lines
5.3 KiB
Go
//go:build !windows
|
|
|
|
package security
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cloudwego/eino/adk/filesystem"
|
|
)
|
|
|
|
func readTestPID(t *testing.T, path string) int {
|
|
t.Helper()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
b, err := os.ReadFile(path)
|
|
if err == nil {
|
|
if pid, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil && pid > 0 {
|
|
return pid
|
|
}
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("process did not write PID to %s", path)
|
|
return 0
|
|
}
|
|
|
|
func requireProcessGone(t *testing.T, pid int) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if syscall.Kill(pid, 0) == syscall.ESRCH {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("process %d survived task cleanup", pid)
|
|
}
|
|
|
|
func TestProcessScope_BackgroundSurvivesToolButEndsWithTask(t *testing.T) {
|
|
executor, _ := setupTestExecutor(t)
|
|
scope := NewProcessScope()
|
|
t.Cleanup(func() { _ = scope.Close() })
|
|
taskCtx := WithProcessScope(context.Background(), scope)
|
|
ctx, cancel := context.WithCancel(context.WithoutCancel(taskCtx))
|
|
defer cancel()
|
|
pidFile := filepath.Join(t.TempDir(), "pid")
|
|
result, err := executor.executeSystemCommand(ctx, map[string]interface{}{
|
|
"command": fmt.Sprintf("echo $$ > %q; sleep 300 &", pidFile),
|
|
})
|
|
if err != nil || result.IsError {
|
|
t.Fatalf("background launch: %v, %+v", err, result)
|
|
}
|
|
pid := readTestPID(t, pidFile)
|
|
cancel() // MCP completes and cancels its per-tool context.
|
|
if err := syscall.Kill(pid, 0); err != nil {
|
|
t.Fatalf("tool completion killed task background process: %v", err)
|
|
}
|
|
if err := scope.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requireProcessGone(t, pid)
|
|
if _, err := StartManagedBackground(taskCtx, "sh", "sleep 300", ""); !errors.Is(err, ErrProcessScopeClosed) {
|
|
t.Fatalf("closed task accepted a new process: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessScope_EinoBackgroundReturnsPromptlyAndIsOwned(t *testing.T) {
|
|
for _, useFlag := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(useFlag), func(t *testing.T) {
|
|
scope := NewProcessScope()
|
|
t.Cleanup(func() { _ = scope.Close() })
|
|
ctx := WithProcessScope(context.Background(), scope)
|
|
pidFile := filepath.Join(t.TempDir(), "pid")
|
|
command := fmt.Sprintf("echo $$ > %q; sleep 300", pidFile)
|
|
if !useFlag {
|
|
command += " &"
|
|
}
|
|
stream, err := NewEinoStreamingShell().ExecuteStreaming(ctx, &filesystem.ExecuteRequest{Command: command, RunInBackendGround: useFlag})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stream.Close()
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
for {
|
|
_, err := stream.Recv()
|
|
if err != nil {
|
|
done <- err
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, io.EOF) {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("background launch waited for job completion")
|
|
}
|
|
pid := readTestPID(t, pidFile)
|
|
if err := scope.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requireProcessGone(t, pid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessScope_ForceKillsIgnoringTERMAndGrandchild(t *testing.T) {
|
|
scope := NewProcessScope()
|
|
t.Cleanup(func() { _ = scope.Close() })
|
|
ctx := WithProcessScope(context.Background(), scope)
|
|
pidFile := filepath.Join(t.TempDir(), "child")
|
|
session, err := StartManagedBackground(ctx, "sh", fmt.Sprintf("trap '' TERM; sleep 300 & echo $! > %q; wait", pidFile), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
childPID := readTestPID(t, pidFile)
|
|
if err := scope.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requireProcessGone(t, session.rootPID)
|
|
requireProcessGone(t, childPID)
|
|
if session.Cmd.ProcessState == nil {
|
|
t.Fatal("root process was not reaped")
|
|
}
|
|
}
|
|
|
|
func TestProcessScope_ConcurrentStartAndClose(t *testing.T) {
|
|
scope := NewProcessScope()
|
|
ctx := WithProcessScope(context.Background(), scope)
|
|
t.Cleanup(func() { _ = scope.Close() })
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
var sessions []*ShellSession
|
|
begin := make(chan struct{})
|
|
for i := 0; i < 24; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-begin
|
|
session, err := StartManagedBackground(ctx, "sh", "sleep 300", "")
|
|
if err != nil {
|
|
if !errors.Is(err, ErrProcessScopeClosed) {
|
|
t.Errorf("start: %v", err)
|
|
}
|
|
return
|
|
}
|
|
mu.Lock()
|
|
sessions = append(sessions, session)
|
|
mu.Unlock()
|
|
}()
|
|
}
|
|
close(begin)
|
|
if err := scope.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wg.Wait()
|
|
for _, session := range sessions {
|
|
requireProcessGone(t, session.rootPID)
|
|
}
|
|
}
|
|
|
|
func TestProcessScope_UnmanagedBackgroundRejected(t *testing.T) {
|
|
if _, err := StartManagedBackground(context.Background(), "sh", "sleep 300", ""); !errors.Is(err, ErrBackgroundNeedsTask) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestProcessScope_ForegroundExitKillsLeftoverChild(t *testing.T) {
|
|
scope := NewProcessScope()
|
|
t.Cleanup(func() { _ = scope.Close() })
|
|
ctx := WithProcessScope(context.Background(), scope)
|
|
pidFile := filepath.Join(t.TempDir(), "child")
|
|
// A shell that exits with a redirected child must not lose that child.
|
|
cmd := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("sleep 300 </dev/null >/dev/null 2>&1 & echo $! > %q", pidFile))
|
|
if _, err := combinedOutputCancellable(ctx, cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pid := readTestPID(t, pidFile)
|
|
if err := scope.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
requireProcessGone(t, pid)
|
|
}
|