120 lines
4.0 KiB
Go
120 lines
4.0 KiB
Go
//go:build windows
|
|
|
|
package proc
|
|
|
|
import (
|
|
"os/exec"
|
|
"strconv"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// SetProcessGroupKill is a no-op on Windows: the Job Object that StartTracked
|
|
// assigns reaps the whole tree on close, so Setpgid (which doesn't exist here)
|
|
// is unnecessary. It exists so non-Windows callers can request group kill
|
|
// uniformly.
|
|
func SetProcessGroupKill(*exec.Cmd) {}
|
|
|
|
// KillTree terminates cmd and every descendant it spawned. Process.Kill only
|
|
// signals the direct child, so a launcher (cmd.exe → node.exe) leaves the
|
|
// grandchild alive holding the inherited stdout/stderr pipes — which makes
|
|
// cmd.Wait block forever. taskkill /T walks the live tree and kills it all.
|
|
func KillTree(cmd *exec.Cmd) {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return
|
|
}
|
|
kill := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(cmd.Process.Pid))
|
|
HideWindow(kill)
|
|
_ = kill.Run()
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
|
|
// StartTracked starts cmd inside a new Job Object whose KILL_ON_JOB_CLOSE flag
|
|
// fells the whole tree — including a launcher's detached grandchild (cmd.exe →
|
|
// node.exe, as the CodeGraph daemon re-parents itself off the launcher) — when
|
|
// the handle closes via KillTracked or an abrupt reasonix exit. The child is
|
|
// created suspended and assigned to the job before it runs, so a fast shim can
|
|
// no longer exec its grandchild and exit before assignment, orphaning a node
|
|
// the job never captured (#3747). It is always resumed before returning, even
|
|
// when job assignment fails, so a child is never left wedged suspended. Returns
|
|
// the job handle, 0 if it could not be created — then KillTracked relies on
|
|
// KillTree alone.
|
|
func StartTracked(cmd *exec.Cmd) (uintptr, error) {
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
cmd.SysProcAttr.CreationFlags |= windows.CREATE_SUSPENDED
|
|
if err := cmd.Start(); err != nil {
|
|
return 0, err
|
|
}
|
|
defer resumeProcess(uint32(cmd.Process.Pid))
|
|
return assignJob(cmd), nil
|
|
}
|
|
|
|
func assignJob(cmd *exec.Cmd) uintptr {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return 0
|
|
}
|
|
job, err := windows.CreateJobObject(nil, nil)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
info := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{
|
|
BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{
|
|
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 0
|
|
}
|
|
h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid))
|
|
if err != nil {
|
|
_ = windows.CloseHandle(job)
|
|
return 0
|
|
}
|
|
defer func() { _ = windows.CloseHandle(h) }()
|
|
if err := windows.AssignProcessToJobObject(job, h); err != nil {
|
|
_ = windows.CloseHandle(job)
|
|
return 0
|
|
}
|
|
return uintptr(job)
|
|
}
|
|
|
|
// resumeProcess resumes every thread of pid. A CREATE_SUSPENDED process has a
|
|
// single suspended primary thread, so this releases it once the job is assigned.
|
|
func resumeProcess(pid uint32) {
|
|
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = windows.CloseHandle(snap) }()
|
|
var te windows.ThreadEntry32
|
|
te.Size = uint32(unsafe.Sizeof(te))
|
|
for err := windows.Thread32First(snap, &te); err == nil; err = windows.Thread32Next(snap, &te) {
|
|
if te.OwnerProcessID != pid {
|
|
continue
|
|
}
|
|
th, err := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, te.ThreadID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_, _ = windows.ResumeThread(th)
|
|
_ = windows.CloseHandle(th)
|
|
}
|
|
}
|
|
|
|
// KillTracked terminates cmd's whole process tree. When job (from StartTracked)
|
|
// is non-zero, terminating it kills even detached descendants; the KillTree pass
|
|
// then catches anything spawned in the gap before the job was assigned.
|
|
func KillTracked(cmd *exec.Cmd, job uintptr) {
|
|
if job != 0 {
|
|
_ = windows.TerminateJobObject(windows.Handle(job), 1)
|
|
_ = windows.CloseHandle(windows.Handle(job))
|
|
}
|
|
KillTree(cmd)
|
|
}
|