//go:build windows package proc import ( "errors" "os/exec" "time" "unsafe" "golang.org/x/sys/windows" ) // windowsGroup 用 Job Object 实现整树终止:把进程纳入一个设置了 // KILL_ON_JOB_CLOSE 的 Job,此后该进程派生的所有子孙自动归入同一 Job, // 关闭 Job 句柄即整树终止。优于 Process.Kill(只杀单 PID,漏 node 等子进程)。 type windowsGroup struct { job windows.Handle } func newGroup() Group { return &windowsGroup{} } // Prepare 在 Windows 为 no-op:Job 须在进程 Start 之后才能 Assign。 func (g *windowsGroup) Prepare(cmd *exec.Cmd) {} // Adopt 必须在 cmd.Start() 之后、子进程派生之前调用。竞态窗口:父进程 exec 后 // 到首个子进程派生之间存在微秒级窗口(Go 无 pre-exec hook 无法完全消除), // 实践风险极小,文档化为已知限制。 func (g *windowsGroup) Adopt(cmd *exec.Cmd) error { if cmd.Process == nil { return errors.New("proc: Adopt before Start") } job, err := windows.CreateJobObject(nil, nil) if err != nil { return err } var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE if _, err := windows.SetInformationJobObject( job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)), ); err != nil { _ = windows.CloseHandle(job) return err } h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid)) if err != nil { _ = windows.CloseHandle(job) return err } defer windows.CloseHandle(h) if err := windows.AssignProcessToJobObject(job, h); err != nil { _ = windows.CloseHandle(job) return err } g.job = job return nil } // TerminateGroup 在 Windows 无可靠 SIGTERM 等价物,直接关闭 Job 句柄触发 // KILL_ON_JOB_CLOSE 整树终止(grace/done 不参与,仅保持接口签名一致)。 func (g *windowsGroup) TerminateGroup(_ <-chan struct{}, _ time.Duration) { g.Close() } func (g *windowsGroup) Close() { if g.job != 0 { _ = windows.CloseHandle(g.job) g.job = 0 } }