feat: add scoped browser stop and job control
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user