mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
207 lines
6.3 KiB
Go
207 lines
6.3 KiB
Go
//go:build linux
|
|
|
|
package process
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSupervisor_CleanExit(t *testing.T) {
|
|
// Given
|
|
supervisor := newHelperSupervisor(t.Context(), "clean", nil)
|
|
|
|
// When
|
|
requireNoError(t, supervisor.Start())
|
|
requireNoError(t, supervisor.WaitReady(t.Context()))
|
|
|
|
// Then
|
|
requireNoError(t, supervisor.Wait(t.Context()))
|
|
}
|
|
|
|
func TestSupervisor_RunsChildWithConfiguredCredential(t *testing.T) {
|
|
// Given
|
|
credentialDirectory, err := os.MkdirTemp("/tmp", "agentcompat-credential-")
|
|
requireNoError(t, err)
|
|
t.Cleanup(func() { _ = os.RemoveAll(credentialDirectory) })
|
|
requireNoError(t, os.Chmod(credentialDirectory, 0o777))
|
|
marker := filepath.Join(credentialDirectory, "credential.txt")
|
|
supervisor := newHelperSupervisor(t.Context(), "credential", []string{helperMarkerEnv + "=" + marker})
|
|
testBinary, err := os.ReadFile(os.Args[0])
|
|
requireNoError(t, err)
|
|
executablePath := filepath.Join(credentialDirectory, "process-helper")
|
|
requireNoError(t, os.WriteFile(executablePath, testBinary, 0o755))
|
|
uncredentialed := newHelperSupervisor(t.Context(), "credential", []string{helperMarkerEnv + "=" + marker})
|
|
uncredentialed.spec.Path = executablePath
|
|
requireNoError(t, uncredentialed.Start())
|
|
requireNoError(t, uncredentialed.Wait(t.Context()))
|
|
requireNoError(t, os.Remove(marker))
|
|
|
|
supervisor.spec.Path = executablePath
|
|
supervisor.spec.Credential = &syscall.Credential{Uid: 65534, Gid: 65534}
|
|
|
|
// When
|
|
if err := supervisor.Start(); err != nil {
|
|
if errors.Is(err, syscall.EPERM) {
|
|
t.Skipf("credentialed helper execution is not permitted: %v", err)
|
|
}
|
|
t.Fatalf("start credentialed helper: %v", err)
|
|
}
|
|
requireNoError(t, supervisor.Wait(t.Context()))
|
|
|
|
// Then
|
|
content, err := os.ReadFile(marker)
|
|
requireNoError(t, err)
|
|
if strings.TrimSpace(string(content)) != "65534:65534" {
|
|
t.Fatalf("child credential = %q, want 65534:65534", content)
|
|
}
|
|
}
|
|
|
|
func TestSupervisor_KillsProcessTree(t *testing.T) {
|
|
// Given
|
|
marker := filepath.Join(t.TempDir(), "grandchild.pid")
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
supervisor := newHelperSupervisor(ctx, "tree", []string{helperMarkerEnv + "=" + marker})
|
|
requireNoError(t, supervisor.Start())
|
|
requireNoError(t, supervisor.WaitReady(t.Context()))
|
|
grandchildPID := readPID(t, marker)
|
|
processGroupID := supervisor.ProcessGroupID()
|
|
|
|
// When
|
|
cancel()
|
|
select {
|
|
case <-supervisor.cleanupDone:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("context cancellation did not complete process-tree cleanup")
|
|
}
|
|
|
|
// Then
|
|
requirePIDGone(t, supervisor.PID())
|
|
requirePIDGone(t, grandchildPID)
|
|
if err := syscall.Kill(-processGroupID, 0); !errors.Is(err, syscall.ESRCH) {
|
|
t.Fatalf("process group %d remains: %v", processGroupID, err)
|
|
}
|
|
}
|
|
|
|
func TestSupervisor_AdoptsListener(t *testing.T) {
|
|
// Given
|
|
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
|
requireNoError(t, err)
|
|
tcpListener := listener.(*net.TCPListener)
|
|
inheritedFile, err := tcpListener.File()
|
|
requireNoError(t, err)
|
|
requireNoError(t, tcpListener.Close())
|
|
supervisor := newHelperSupervisor(t.Context(), "listener", []string{helperFDEnv + "=3"})
|
|
supervisor.spec.ExtraFiles = []*os.File{inheritedFile}
|
|
|
|
// When
|
|
requireNoError(t, supervisor.Start())
|
|
requireNoError(t, supervisor.WaitReady(t.Context()))
|
|
sample, err := SampleProcess(supervisor.PID())
|
|
requireNoError(t, err)
|
|
|
|
// Then
|
|
if sample.TCPListenerCount != 1 {
|
|
t.Fatalf("TCP listeners = %d, want 1", sample.TCPListenerCount)
|
|
}
|
|
requireNoError(t, supervisor.Stop(t.Context()))
|
|
requirePIDGone(t, supervisor.PID())
|
|
}
|
|
|
|
func TestSupervisor_RedactsLogs(t *testing.T) {
|
|
// Given
|
|
var output bytes.Buffer
|
|
supervisor := newHelperSupervisor(t.Context(), "logs", nil)
|
|
supervisor.spec.MaxLogBytes = 128
|
|
supervisor.spec.Stdout = &output
|
|
supervisor.spec.Stderr = &output
|
|
|
|
// When
|
|
requireNoError(t, supervisor.Start())
|
|
requireNoError(t, supervisor.WaitReady(t.Context()))
|
|
requireNoError(t, supervisor.Wait(t.Context()))
|
|
|
|
// Then
|
|
logged := output.String()
|
|
if strings.Contains(logged, "top-secret") || strings.Contains(logged, "eyJsecret") {
|
|
t.Fatalf("secret survived supervisor log redaction: %s", logged)
|
|
}
|
|
if output.Len() > supervisor.spec.MaxLogBytes*2 {
|
|
t.Fatalf("combined log bytes = %d, per-stream limit = %d", output.Len(), supervisor.spec.MaxLogBytes)
|
|
}
|
|
if !strings.Contains(logged, truncationMarker) {
|
|
t.Fatalf("truncation marker missing: %q", logged)
|
|
}
|
|
}
|
|
|
|
func TestSupervisor_RecordsForcedCleanupForSIGTERMIgnoringChild(t *testing.T) {
|
|
// Given
|
|
resultsDir := t.TempDir()
|
|
marker := filepath.Join(t.TempDir(), "forced-grandchild.pid")
|
|
supervisor := newHelperSupervisor(t.Context(), "force-tree", []string{helperMarkerEnv + "=" + marker})
|
|
requireNoError(t, supervisor.Start())
|
|
requireNoError(t, supervisor.WaitReady(t.Context()))
|
|
grandchildPID := readPID(t, marker)
|
|
|
|
// When
|
|
requireNoError(t, supervisor.Stop(t.Context()))
|
|
receipt := NewCleanupReceipt([]CleanupRecord{supervisor.CleanupRecord()})
|
|
receiptPath := filepath.Join(resultsDir, "cleanup.json")
|
|
requireNoError(t, WriteCleanupReceipt(receiptPath, receipt))
|
|
|
|
// Then
|
|
if !supervisor.ForcedCleanup() {
|
|
t.Fatal("forced cleanup was not recorded")
|
|
}
|
|
data, err := os.ReadFile(receiptPath)
|
|
requireNoError(t, err)
|
|
if !strings.Contains(string(data), `"forced": true`) {
|
|
t.Fatalf("cleanup receipt = %s", data)
|
|
}
|
|
requirePIDGone(t, supervisor.PID())
|
|
requirePIDGone(t, grandchildPID)
|
|
}
|
|
|
|
func TestSupervisor_InterruptSignalCleansProcessTree(t *testing.T) {
|
|
// Given
|
|
marker := filepath.Join(t.TempDir(), "interrupt-grandchild.pid")
|
|
command := exec.Command(os.Args[0], "-test.run=^TestProcessHelper$")
|
|
command.Env = append(os.Environ(), helperModeEnv+"=interrupt-probe", helperMarkerEnv+"="+marker)
|
|
output, err := command.StdoutPipe()
|
|
requireNoError(t, err)
|
|
command.Stderr = os.Stderr
|
|
requireNoError(t, command.Start())
|
|
scanner := bufio.NewScanner(output)
|
|
ready := false
|
|
for scanner.Scan() {
|
|
if scanner.Text() == "PROBE_READY" {
|
|
ready = true
|
|
break
|
|
}
|
|
}
|
|
requireNoError(t, scanner.Err())
|
|
if !ready {
|
|
t.Fatal("interrupt probe exited before readiness")
|
|
}
|
|
leaderPID := readPID(t, marker+".leader")
|
|
grandchildPID := readPID(t, marker)
|
|
|
|
// When
|
|
requireNoError(t, command.Process.Signal(syscall.SIGTERM))
|
|
requireNoError(t, command.Wait())
|
|
|
|
// Then
|
|
requirePIDGone(t, leaderPID)
|
|
requirePIDGone(t, grandchildPID)
|
|
}
|