//go:build windows package browser import ( "context" "errors" "fmt" "os" "syscall" "unsafe" ) const ( jobObjectExtendedLimitClass = 9 jobObjectLimitKillOnJobClose = 0x2000 wmClose = 0x0010 ) var ( kernel32Job = syscall.NewLazyDLL("kernel32.dll") createJobObjectW = kernel32Job.NewProc("CreateJobObjectW") setInformationJobObject = kernel32Job.NewProc("SetInformationJobObject") assignProcessToJobObject = kernel32Job.NewProc("AssignProcessToJobObject") terminateJobObject = kernel32Job.NewProc("TerminateJobObject") enumWindows = user32.NewProc("EnumWindows") getWindowProcessID = user32.NewProc("GetWindowThreadProcessId") postMessageW = user32.NewProc("PostMessageW") ) type jobHandle struct{ handle syscall.Handle } type jobObjectBasicLimitInformation struct { PerProcessUserTimeLimit int64 PerJobUserTimeLimit int64 LimitFlags uint32 MinimumWorkingSetSize uintptr MaximumWorkingSetSize uintptr ActiveProcessLimit uint32 Affinity uintptr PriorityClass uint32 SchedulingClass uint32 } type ioCounters struct{ ReadOperationCount, WriteOperationCount, OtherOperationCount, ReadTransferCount, WriteTransferCount, OtherTransferCount uint64 } type jobObjectExtendedLimitInformation struct { BasicLimitInformation jobObjectBasicLimitInformation IoInfo ioCounters ProcessMemoryLimit uintptr JobMemoryLimit uintptr PeakProcessMemoryUsed uintptr PeakJobMemoryUsed uintptr } func (j *jobHandle) assign(process *os.Process) error { job, _, err := createJobObjectW.Call(0, 0) if job == 0 { return fmt.Errorf("create browser job object: %w", err) } j.handle = syscall.Handle(job) info := jobObjectExtendedLimitInformation{BasicLimitInformation: jobObjectBasicLimitInformation{LimitFlags: jobObjectLimitKillOnJobClose}} if result, _, callErr := setInformationJobObject.Call(job, jobObjectExtendedLimitClass, uintptr(unsafe.Pointer(&info)), unsafe.Sizeof(info)); result == 0 { j.close() return fmt.Errorf("configure browser job object: %w", callErr) } processHandle, err := syscall.OpenProcess(0x0100|0x0001, false, uint32(process.Pid)) if err != nil { j.close() return fmt.Errorf("open browser process for job object: %w", err) } defer syscall.CloseHandle(processHandle) if result, _, callErr := assignProcessToJobObject.Call(job, uintptr(processHandle)); result == 0 { j.close() return fmt.Errorf("assign browser process to job object: %w", callErr) } return nil } func (j *jobHandle) close() { if j == nil || j.handle == 0 { return } _ = syscall.CloseHandle(j.handle) j.handle = 0 } func (j *jobHandle) terminate() error { if j == nil || j.handle == 0 { return errors.New("browser job object is unavailable") } result, _, err := terminateJobObject.Call(uintptr(j.handle), 1) if result == 0 { return err } return nil } func (p *osProcess) Stop(ctx context.Context, force bool) error { if err := ctx.Err(); err != nil { return err } if force { return p.job.terminate() } return postCloseToProcess(p.command.Process.Pid) } func postCloseToProcess(pid int) error { if pid <= 0 { return errors.New("invalid browser PID") } callback := syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr { var windowPID uint32 getWindowProcessID.Call(hwnd, uintptr(unsafe.Pointer(&windowPID))) if int(windowPID) == pid { postMessageW.Call(hwnd, wmClose, 0, 0) } return 1 }) result, _, err := enumWindows.Call(callback, uintptr(pid)) if result == 0 && err != syscall.Errno(0) { return err } return nil }