//go:build windows package execenv import ( "fmt" "os/exec" "time" "unsafe" "golang.org/x/sys/windows" ) type preparationProcessController struct { job windows.Handle } // jobObjectBasicAccountingInformation mirrors JOBOBJECT_BASIC_ACCOUNTING_INFORMATION. // x/sys exposes QueryInformationJobObject and the information-class constant, // but not this result struct. type jobObjectBasicAccountingInformation struct { TotalUserTime int64 TotalKernelTime int64 ThisPeriodTotalUserTime int64 ThisPeriodTotalKernelTime int64 TotalPageFaultCount uint32 TotalProcesses uint32 ActiveProcesses uint32 TotalTerminatedProcesses uint32 } func newPreparationProcessController(_ *exec.Cmd) (*preparationProcessController, error) { job, err := windows.CreateJobObject(nil, nil) if err != nil { return nil, err } 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 nil, fmt.Errorf("set KILL_ON_JOB_CLOSE: %w", err) } return &preparationProcessController{job: job}, nil } func (c *preparationProcessController) attach(cmd *exec.Cmd) error { process, err := windows.OpenProcess( windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid), ) if err != nil { return fmt.Errorf("open helper process: %w", err) } defer windows.CloseHandle(process) if err := windows.AssignProcessToJobObject(c.job, process); err != nil { return fmt.Errorf("assign helper to job object: %w", err) } return nil } func (c *preparationProcessController) stop(_ *exec.Cmd) error { if err := windows.TerminateJobObject(c.job, 1); err != nil { active, queryErr := c.activeProcesses() if queryErr == nil && active == 0 { return nil } return err } return nil } // finish is called only after the direct helper has exited. Terminate any // descendant it left behind, then wait until the Job Object reports no active // members before the daemon may release directory ownership and retry. func (c *preparationProcessController) finish() error { active, err := c.activeProcesses() if err != nil { return err } if active > 0 { if err := windows.TerminateJobObject(c.job, 1); err != nil { return err } } for { active, err = c.activeProcesses() if err != nil { return err } if active == 0 { return nil } time.Sleep(10 * time.Millisecond) } } func (c *preparationProcessController) activeProcesses() (uint32, error) { var info jobObjectBasicAccountingInformation if err := windows.QueryInformationJobObject( c.job, windows.JobObjectBasicAccountingInformation, uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)), nil, ); err != nil { return 0, fmt.Errorf("query job object members: %w", err) } return info.ActiveProcesses, nil } func (c *preparationProcessController) close() { if c.job == 0 { return } _ = windows.CloseHandle(c.job) c.job = 0 }