Dispose subprocess descendants after leader exit

This commit is contained in:
2026-08-11 03:22:21 +00:00
parent 80be8be4d6
commit 2545faef6c
8 changed files with 207 additions and 20 deletions

View File

@@ -6,6 +6,7 @@ import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@@ -74,6 +75,63 @@ func TestRunCaptureLimitTerminatesProcessTree(t *testing.T) {
assertDescendantDidNotSurvive(t, sentinelPath)
}
func TestRunDisposesDescendantsAfterLeaderExit(t *testing.T) {
tests := []struct {
name string
mode string
wantExitCode int
wantWaitDelay bool
ignoreTerm bool
}{
{name: "success retaining streams", mode: "leader-exit-retained", wantExitCode: 0, wantWaitDelay: true},
{name: "success redirecting streams", mode: "leader-exit-redirected", wantExitCode: 0},
{name: "failed leader", mode: "leader-fail-redirected", wantExitCode: 9, ignoreTerm: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, sentinelPath, releasePath := leaderExitRequest(t, tt.mode)
if tt.ignoreTerm {
req.EnvOverrides["SUBPROCESS_HELPER_IGNORE_TERM"] = "1"
}
outcomes := make(chan runOutcome, 1)
go func() {
result, err := Run(context.Background(), req)
outcomes <- runOutcome{result: result, err: err}
}()
var outcome runOutcome
select {
case outcome = <-outcomes:
case <-time.After(6 * time.Second):
t.Fatal("Run() did not complete bounded owned-tree disposal")
}
if outcome.result.ExitCode != tt.wantExitCode {
t.Fatalf("ExitCode = %d, want %d", outcome.result.ExitCode, tt.wantExitCode)
}
if tt.wantWaitDelay {
if !errors.Is(outcome.err, exec.ErrWaitDelay) {
t.Fatalf("error = %v, want exec.ErrWaitDelay", outcome.err)
}
} else if tt.wantExitCode == 0 && outcome.err != nil {
t.Fatalf("Run() error = %v, want nil", outcome.err)
} else if tt.wantExitCode != 0 {
var exitErr *exec.ExitError
if !errors.As(outcome.err, &exitErr) || exitErr.ExitCode() != tt.wantExitCode {
t.Fatalf("error = %v, want exit code %d", outcome.err, tt.wantExitCode)
}
}
if _, err := os.Stat(req.EnvOverrides["SUBPROCESS_HELPER_READY_PATH"]); err != nil {
t.Fatalf("descendant readiness file: %v", err)
}
if err := os.WriteFile(releasePath, []byte("release"), 0o600); err != nil {
t.Fatalf("WriteFile(release) error = %v", err)
}
assertDescendantDidNotSurvive(t, sentinelPath)
})
}
}
type runOutcome struct {
result RunResult
err error
@@ -102,6 +160,31 @@ func processTreeRequest(t *testing.T) (RunRequest, string) {
}, sentinelPath
}
func leaderExitRequest(t *testing.T, mode string) (RunRequest, string, string) {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatalf("os.Executable() error = %v", err)
}
dir := t.TempDir()
readyPath := filepath.Join(dir, "ready")
releasePath := filepath.Join(dir, "release")
sentinelPath := filepath.Join(dir, "descendant-survived")
return RunRequest{
Executable: executable,
Args: []string{"-test.run=^TestSubprocessHelper$", "--", mode},
EnvOverrides: map[string]string{
"GO_WANT_SUBPROCESS_HELPER": "1",
"SUBPROCESS_HELPER_READY_PATH": readyPath,
"SUBPROCESS_HELPER_RELEASE_PATH": releasePath,
"SUBPROCESS_HELPER_SENTINEL_PATH": sentinelPath,
},
StdoutLogPath: filepath.Join(dir, "stdout.log"),
StderrLogPath: filepath.Join(dir, "stderr.log"),
}, sentinelPath, releasePath
}
func awaitHelperReady(t *testing.T, readyPath string) {
t.Helper()