227 lines
6.6 KiB
Go
227 lines
6.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func supervisor_program(t testing.TB) []string {
|
|
t.Helper()
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return []string{executable, "-test.run=^TestSupervisorHelper$", "--", "supervisor"}
|
|
}
|
|
|
|
func supervised_manager(t testing.TB) *Manager {
|
|
t.Helper()
|
|
manager, err := NewSupervisedManager(supervisor_program(t), "auto")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
|
|
defer cancel()
|
|
if err := manager.Shutdown(ctx); err != nil {
|
|
t.Error(err)
|
|
}
|
|
if manager.Stats() != (Stats{}) {
|
|
t.Error(manager.Stats())
|
|
}
|
|
})
|
|
return manager
|
|
}
|
|
|
|
func supervised_descendant(t testing.TB, session *Session) int {
|
|
t.Helper()
|
|
result, err := session.Invoke(context.Background(), "echo", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value := wait_result(t, result)
|
|
var pid int
|
|
if value.Err != nil || json.Unmarshal(value.Value, &pid) != nil || pid <= 1 {
|
|
t.Fatalf("descendant: %+v", value)
|
|
}
|
|
return pid
|
|
}
|
|
|
|
func wait_supervised_reap(t *testing.T, pid int) {
|
|
t.Helper()
|
|
deadline := time.NewTimer(6 * time.Second)
|
|
defer deadline.Stop()
|
|
ticker := time.NewTicker(time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
if _, err := os.Stat("/proc/" + strconv.Itoa(pid)); os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) {
|
|
return
|
|
}
|
|
select {
|
|
case <-deadline.C:
|
|
t.Fatalf("supervisor did not reap descendant %d", pid)
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSupervisorHelper(t *testing.T) {
|
|
if os.Args[len(os.Args)-1] == "supervisor" {
|
|
if err := RunSupervisor(context.Background()); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
if len(os.Args) < 3 || os.Args[len(os.Args)-2] != "supervisor-owner" {
|
|
return
|
|
}
|
|
marker := os.Args[len(os.Args)-1]
|
|
manager, err := NewSupervisedManager(supervisor_program(t), "auto")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session := start_active(t, manager, "sample", "spawn-chain")
|
|
pid := supervised_descendant(t, session)
|
|
if err := os.WriteFile(marker, []byte(strconv.Itoa(pid)), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Abrupt reader death: no Disable, Shutdown, deferred cleanup or signals.
|
|
os.Exit(0)
|
|
}
|
|
|
|
func TestSupervisionWithoutInitToolsCleansDetachedDescendants(t *testing.T) {
|
|
t.Setenv("PATH", "")
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
for _, mode := range []string{"spawn", "spawn-detached", "spawn-chain"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
manager := supervised_manager(t)
|
|
first := start_active(t, manager, "first", mode)
|
|
second := start_active(t, manager, "second", "spawn-detached")
|
|
pid := supervised_descendant(t, first)
|
|
other := supervised_descendant(t, second)
|
|
first.Disable()
|
|
wait_signal(t, first.Done())
|
|
if state, err := first.Status(); state != Stopped || err != nil {
|
|
t.Fatalf("disable: %s %v", state, err)
|
|
}
|
|
wait_supervised_reap(t, pid)
|
|
if _, err := os.Stat("/proc/" + strconv.Itoa(other)); err != nil {
|
|
t.Fatal("another session was terminated", err)
|
|
}
|
|
second.Disable()
|
|
wait_signal(t, second.Done())
|
|
wait_supervised_reap(t, other)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSupervisionCleansDescendantsAfterReaderDeath(t *testing.T) {
|
|
t.Setenv("PATH", "")
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
executable, _ := os.Executable()
|
|
marker := filepath.Join(t.TempDir(), "descendant")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, executable, "-test.run=^TestSupervisorHelper$", "--", "supervisor-owner", marker)
|
|
if output, err := command.CombinedOutput(); err != nil {
|
|
t.Fatalf("owner: %v %s", err, output)
|
|
}
|
|
data, err := os.ReadFile(marker)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pid, err := strconv.Atoi(string(data))
|
|
if err != nil || pid <= 1 {
|
|
t.Fatal("invalid descendant marker")
|
|
}
|
|
wait_supervised_reap(t, pid)
|
|
}
|
|
|
|
func TestSupervisionReapsExitedDescendantWhileSessionActive(t *testing.T) {
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
manager := supervised_manager(t)
|
|
session := start_active(t, manager, "sample", "spawn-chain")
|
|
pid := supervised_descendant(t, session)
|
|
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_supervised_reap(t, pid)
|
|
if state, err := session.Status(); state != Active || err != nil {
|
|
t.Fatalf("descendant exit retired the live session: %s %v", state, err)
|
|
}
|
|
// A status flag alone does not prove the main child's protocol still works.
|
|
_ = supervised_descendant(t, session)
|
|
}
|
|
|
|
func TestSupervisionMissingProgramReleasesCapacity(t *testing.T) {
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
manager := supervised_manager(t)
|
|
spec := Spec{ID: "sample", Program: []string{filepath.Join(t.TempDir(), "missing")}}
|
|
session, err := manager.Start(context.Background(), spec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_signal(t, session.Done())
|
|
if state, failure := session.Status(); state != Failed || failure == nil {
|
|
t.Fatal(state, failure)
|
|
}
|
|
healthy := start_active(t, manager, "sample", "echo")
|
|
healthy.Disable()
|
|
wait_signal(t, healthy.Done())
|
|
}
|
|
|
|
func TestSupervisionRejectsInvalidExplicitCgroupParent(t *testing.T) {
|
|
for _, parent := range []string{"relative", t.TempDir()} {
|
|
if _, err := NewSupervisedManager(supervisor_program(t), parent); !errors.Is(err, ErrContainment) {
|
|
t.Fatalf("silently ignored explicit parent %q: %v", parent, err)
|
|
}
|
|
}
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", t.TempDir())
|
|
if _, err := NewSupervisedManager(supervisor_program(t), "auto"); !errors.Is(err, ErrContainment) {
|
|
t.Fatalf("silently ignored configured parent: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSupervisionCancelledStartupReleasesCapacity(t *testing.T) {
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
manager := supervised_manager(t)
|
|
session, err := manager.Start(context.Background(), child_spec(t, "sample", "hang-start"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_control(t, func() bool {
|
|
session.mutex.Lock()
|
|
defer session.mutex.Unlock()
|
|
return len(session.pending) > 0
|
|
})
|
|
session.Disable()
|
|
wait_signal(t, session.Done())
|
|
if state, err := session.Status(); state != Stopped || err != nil {
|
|
t.Fatal(state, err)
|
|
}
|
|
healthy := start_active(t, manager, "sample", "echo")
|
|
healthy.Disable()
|
|
wait_signal(t, healthy.Done())
|
|
}
|
|
|
|
func TestSupervisionDetectsChildExitAfterActivation(t *testing.T) {
|
|
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
|
|
manager := supervised_manager(t)
|
|
session := start_active(t, manager, "sample", "exit-active")
|
|
wait_signal(t, session.Done())
|
|
if state, err := session.Status(); state != Failed || !errors.Is(err, ErrExited) {
|
|
t.Fatal(state, err)
|
|
}
|
|
}
|