helpers/process/job_windows.go (106 lines of code) (raw):
package process
import (
"fmt"
"os"
"os/exec"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type osCmd struct {
internal *exec.Cmd
options CommandOptions
// A job object to helper ensure processes are killed, plus a Once
// to ensure the job object is only closed one.
jobObject uintptr
once sync.Once
}
func (c *osCmd) Start() error {
setProcessGroup(c.internal, c.options.UseWindowsLegacyProcessStrategy)
if c.options.UseWindowsJobObject {
jobObj, err := createJobObject()
if err != nil {
return fmt.Errorf("starting OS command: %w", err)
}
c.jobObject = jobObj
}
err := c.internal.Start()
if err != nil {
return fmt.Errorf("starting OS command: %w", err)
}
if c.options.UseWindowsJobObject {
// Any failures here are ignored, since we've already started the process running.
if err := assignProcessToJobObject(c.internal.Process.Pid, c.jobObject); err != nil {
c.options.Logger.Warn("assigning process to job object:", err)
}
}
return nil
}
func (c *osCmd) Wait() error {
err := c.internal.Wait()
c.closeJobObject()
return err
}
func (c *osCmd) Process() *os.Process {
return c.internal.Process
}
func newOSCmd(c *exec.Cmd, options CommandOptions) Commander {
return &osCmd{
internal: c,
options: options,
}
}
func (c *osCmd) closeJobObject() {
if !c.options.UseWindowsJobObject {
return
}
c.once.Do(func() {
windows.CloseHandle(windows.Handle(c.jobObject))
})
}
func setProcessGroup(c *exec.Cmd, useLegacyStrategy bool) {
if useLegacyStrategy {
return
}
c.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
func createJobObject() (uintptr, error) {
jobObj, err := windows.CreateJobObject(nil, nil)
if err != nil {
return 0, fmt.Errorf("creating job object: %w", err)
}
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(
jobObj,
windows.JobObjectExtendedLimitInformation,
uintptr(unsafe.Pointer(&info)),
uint32(unsafe.Sizeof(info))); err != nil {
return 0, fmt.Errorf("setting job object information: %w", err)
}
return uintptr(jobObj), nil
}
// Assign the process with specified PID to the specified job object. Processes created as children of that one will
// also be assigned to the job. When the last handle on the job is closed, all associated processes will be terminated.
func assignProcessToJobObject(pid int, jobObject uintptr) error {
procHandle, err := findProcessHandleFromPID(pid)
if err != nil {
return fmt.Errorf("failed to retrieve handle for process: %w", err)
}
if err = windows.AssignProcessToJobObject(windows.Handle(jobObject), windows.Handle(procHandle)); err != nil {
return fmt.Errorf("failed to assign process to job: %w", err)
}
return nil
}
func findProcessHandleFromPID(pid int) (uintptr, error) {
const da = windows.PROCESS_TERMINATE | windows.PROCESS_SET_QUOTA
h, err := syscall.OpenProcess(da, false, uint32(pid))
if err != nil {
return 0, fmt.Errorf("calling OpenProcess: %w", err)
}
if uintptr(h) == 0 {
return 0, fmt.Errorf("getting process handle for pid %q", pid)
}
return uintptr(h), nil
}