//go:build !windows package proc import ( "fmt" "os/exec" "syscall" "testing" "time" ) // TestUnixGroup_KillsChildProcess 验证整树清理:父 shell 派生一个后台 sleep // 子进程并打印其 PID,TerminateGroup 后该子进程(孙级)也应被 kill(-pgid) 回收, // 证明终止覆盖整个进程组而非仅组长。 func TestUnixGroup_KillsChildProcess(t *testing.T) { g := NewGroup() // `sleep 30 &` 后台子进程;`echo $!` 打印其 PID;`wait` 让 shell 阻塞存活。 cmd := exec.Command("sh", "-c", "sleep 30 & echo $!; wait") g.Prepare(cmd) stdout, err := cmd.StdoutPipe() if err != nil { t.Fatalf("stdout pipe: %v", err) } if err := cmd.Start(); err != nil { t.Fatalf("start: %v", err) } if err := g.Adopt(cmd); err != nil { t.Fatalf("adopt: %v", err) } defer g.Close() var childPID int if _, err := fmt.Fscan(stdout, &childPID); err != nil { t.Fatalf("read child pid: %v", err) } if childPID <= 0 { t.Fatalf("invalid child pid: %d", childPID) } done := make(chan struct{}) go func() { _ = cmd.Wait() close(done) }() g.TerminateGroup(done, 2*time.Second) select { case <-done: case <-time.After(10 * time.Second): t.Fatal("parent not terminated") } // 给内核一点时间回收子进程,然后用 kill(pid, 0) 探测:返回错误(ESRCH)=已死。 deadline := time.Now().Add(5 * time.Second) for { if err := syscall.Kill(childPID, 0); err != nil { return // 子进程已死,符合预期。 } if time.Now().After(deadline) { t.Fatalf("child process %d still alive after group terminate", childPID) } time.Sleep(100 * time.Millisecond) } }