feat: manage task process lifetimes and preserve turn history

This commit is contained in:
Ed1s0nZ
2026-09-16 17:52:58 +08:00
parent fd1c13a43d
commit f7882be546
54 changed files with 3650 additions and 330 deletions
+120
View File
@@ -0,0 +1,120 @@
//go:build linux
package processguard
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
func TestCgroupContainsSetsidAndAppliesLimits(t *testing.T) {
opts := testOptions()
if opts.CgroupRoot == "" {
t.Skip("set CSAI_TEST_CGROUP_ROOT to a delegated cgroup v2 root")
}
opts.Mode = "required"
opts.CPUQuotaMicros = 50000
id := testID()
g, err := NewWithOptions(id, opts)
if err != nil {
t.Fatal(err)
}
defer closeTestGroup(t, g)
file := filepath.Join(t.TempDir(), "escaped")
cmd, err := startTestCommand(g, fmt.Sprintf("setsid sh -c 'echo $$ > %s; exec sleep 300' </dev/null >/dev/null 2>&1 &", file))
if err != nil {
t.Fatal(err)
}
reaped := make(chan struct{})
go func() { _ = cmd.Wait(); close(reaped) }()
pid := readPID(t, file)
<-reaped // The launching shell is gone; the cgroup must still own setsid descendants.
root := filepath.Join(opts.CgroupRoot, "task-"+id)
for name, want := range map[string]string{"pids.max": "64", "memory.max": "268435456", "cpu.max": "50000 100000"} {
data, err := os.ReadFile(filepath.Join(root, name))
if err != nil || strings.TrimSpace(string(data)) != want {
t.Fatalf("%s=%s err=%v", name, data, err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err = g.Close(ctx); err != nil {
t.Fatal(err)
}
<-reaped
waitGone(t, pid)
if _, err = os.Stat(root); !os.IsNotExist(err) {
t.Fatalf("cgroup retained after cleanup: %v", err)
}
}
func TestCgroupStartupDelegationAndRecovery(t *testing.T) {
opts := testOptions()
if opts.CgroupRoot == "" {
t.Skip("requires delegated cgroup fixture")
}
before, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
t.Fatal(err)
}
original := ""
for _, line := range strings.Split(string(before), "\n") {
if strings.HasPrefix(line, "0::") {
original = filepath.Join("/sys/fs/cgroup", strings.TrimPrefix(line, "0::"))
}
}
root := filepath.Join(opts.CgroupRoot, "startup-fixture")
if err = os.Mkdir(root, 0700); err != nil {
t.Fatal(err)
}
if err = os.WriteFile(filepath.Join(root, "cgroup.procs"), []byte(fmt.Sprint(os.Getpid())), 0600); err != nil {
t.Fatal(err)
}
defer func() {
_ = os.WriteFile(filepath.Join(original, "cgroup.procs"), []byte(fmt.Sprint(os.Getpid())), 0600)
if rootLock != nil {
_ = rootLock.Close()
rootLock = nil
}
_ = removeCgroupTree(root)
}()
stale := filepath.Join(root, "task-"+testID())
if err = os.Mkdir(stale, 0700); err != nil {
t.Fatal(err)
}
dir, err := os.Open(stale)
if err != nil {
t.Fatal(err)
}
defer dir.Close()
cmd := exec.Command("sh", "-c", "exec sleep 300")
cmd.SysProcAttr = &syscall.SysProcAttr{UseCgroupFD: true, CgroupFD: int(dir.Fd()), Setsid: true}
if err = cmd.Start(); err != nil {
t.Fatal(err)
}
reaped := make(chan struct{})
go func() { _ = cmd.Wait(); close(reaped) }()
opts.CgroupRoot = root
opts.Mode = "required"
if err = configurePlatform(&opts); err != nil {
_ = cmd.Process.Kill()
<-reaped
t.Fatal(err)
}
<-reaped
waitGone(t, cmd.Process.Pid)
if _, err = os.Stat(stale); !os.IsNotExist(err) {
t.Fatalf("stale task cgroup was not removed: %v", err)
}
data, err := os.ReadFile(filepath.Join(root, "cgroup.subtree_control"))
if err != nil || !strings.Contains(string(data), "memory") {
t.Fatalf("delegation not enabled: %s %v", data, err)
}
}