Files
autoPriority/windows.go
T

296 lines
8.0 KiB
Go

//go:build windows
package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
)
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessSetInformation = 0x00000200
ProcessQueryLimitedInformation = 0x00001000
ErrorNoMoreFiles = 18
ErrorAlreadyExists = 183
)
type processEntry32 struct {
Size uint32
CntUsage uint32
PID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentPID uint32
PrioClass int32
Flags uint32
ExeFile [260]uint16
}
type processMemoryCounters struct {
CBM uint32
PageFaultCount uint32
PeakWorkingSetSize uintptr
WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr
QuotaPeakNonPagedPoolUsage uintptr
QuotaNonPagedPoolUsage uintptr
PagefileUsage uintptr
PeakPagefileUsage uintptr
PrivateUsage uintptr
}
type processKey struct {
PID uint32
Created uint64
}
type procInfo struct {
PID uint32
Name string
RSS uint64
Key processKey
Prio uint32
Handle syscall.Handle
Waitable bool
ParentPID uint32
}
var (
k32 = syscall.NewLazyDLL("kernel32.dll")
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = k32.NewProc("Process32FirstW")
procProcess32Next = k32.NewProc("Process32NextW")
procGetMemInfo = k32.NewProc("K32GetProcessMemoryInfo")
procSetPriority = k32.NewProc("SetPriorityClass")
procGetPriority = k32.NewProc("GetPriorityClass")
procOpenProcess = k32.NewProc("OpenProcess")
procCloseHandle = k32.NewProc("CloseHandle")
procGetProcessTimes = k32.NewProc("GetProcessTimes")
procQueryImageName = k32.NewProc("QueryFullProcessImageNameW")
procCreateMutex = k32.NewProc("CreateMutexW")
)
func closeH(h syscall.Handle) {
_, _, _ = procCloseHandle.Call(uintptr(h))
}
func openProc(pid uint32, acc uint32) (syscall.Handle, error) {
r, _, e := procOpenProcess.Call(uintptr(acc), 0, uintptr(pid))
if r == 0 {
return 0, fmt.Errorf("OpenProcess(%d) failed: %w", pid, e)
}
return syscall.Handle(r), nil
}
func setPrio(h syscall.Handle, pid uint32, cls uint32) error {
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
if r == 0 {
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
}
return nil
}
func processCreated(h syscall.Handle) (uint64, error) {
var created, exited, kernel, user syscall.Filetime
r, _, e := procGetProcessTimes.Call(
uintptr(h),
uintptr(unsafe.Pointer(&created)),
uintptr(unsafe.Pointer(&exited)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r == 0 {
return 0, fmt.Errorf("GetProcessTimes failed: %w", e)
}
return uint64(created.HighDateTime)<<32 | uint64(created.LowDateTime), nil
}
func processName(h syscall.Handle, buf []uint16) (string, error) {
size := uint32(len(buf))
r, _, e := procQueryImageName.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)))
if r == 0 {
return "", fmt.Errorf("QueryFullProcessImageName failed: %w", e)
}
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
}
func processAlive(h syscall.Handle) (bool, error) {
r, _, err := procWaitForSingleObject.Call(uintptr(h), 0)
return decodeWaitResult(r, err)
}
func decodeWaitResult(r uintptr, err error) (bool, error) {
switch r {
case 0:
return false, nil
case 0x102:
return true, nil
default:
return false, fmt.Errorf("WaitForSingleObject returned %#x: %v", r, err)
}
}
func allProcs() ([]procInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == ^uintptr(0) {
return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e)
}
defer closeH(syscall.Handle(snap))
pe := processEntry32{Size: uint32(unsafe.Sizeof(processEntry32{}))}
r, _, e := procProcess32First.Call(snap, uintptr(unsafe.Pointer(&pe)))
if r == 0 {
if e == syscall.Errno(ErrorNoMoreFiles) {
return nil, nil
}
return nil, fmt.Errorf("Process32First failed: %w", e)
}
var out []procInfo
for {
name := syscall.UTF16ToString(pe.ExeFile[:])
out = append(out, procInfo{PID: pe.PID, ParentPID: pe.ParentPID, Name: name})
pe.Size = uint32(unsafe.Sizeof(processEntry32{}))
r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe)))
if r == 0 {
if e == syscall.Errno(ErrorNoMoreFiles) {
break
}
return nil, fmt.Errorf("Process32Next failed: %w", e)
}
}
return out, nil
}
func singleInstance() (syscall.Handle, bool, error) {
name, err := syscall.UTF16PtrFromString(`Local\autoPriority`)
if err != nil {
return 0, false, err
}
r, _, e := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
if r == 0 {
return 0, false, fmt.Errorf("CreateMutex failed: %w", e)
}
return syscall.Handle(r), e == syscall.Errno(ErrorAlreadyExists), nil
}
const processSynchronize = 0x00100000
var procWaitForSingleObject = k32.NewProc("WaitForSingleObject")
type windowsBackend struct {
list func() ([]procInfo, error)
self uint32
nameBuffer [32768]uint16
unreadable map[uint32]procInfo
}
func newWindowsBackend() *windowsBackend {
return &windowsBackend{list: allProcs, self: uint32(os.Getpid()), unreadable: make(map[uint32]procInfo)}
}
func (w *windowsBackend) inspect(p procInfo) (procInfo, error) {
h, err := openProc(p.PID, ProcessQueryLimitedInformation|processSynchronize)
p.Waitable = err == nil
if err != nil {
h, err = openProc(p.PID, ProcessQueryLimitedInformation)
}
if err != nil {
return procInfo{}, err
}
good := false
defer func() {
if !good {
closeH(h)
}
}()
created, err := processCreated(h)
if err != nil {
return procInfo{}, err
}
if name, e := processName(h, w.nameBuffer[:]); e == nil {
p.Name = name
}
p.Handle = h
p.Key = processKey{PID: p.PID, Created: created}
var m processMemoryCounters
m.CBM = uint32(unsafe.Sizeof(m))
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
if r == 0 {
return procInfo{}, fmt.Errorf("K32GetProcessMemoryInfo: %w", e)
}
p.RSS = uint64(m.WorkingSetSize)
p.Prio, err = w.Priority(&p)
if err != nil {
return procInfo{}, err
}
good = true
return p, nil
}
func (w *windowsBackend) Snapshot() (processSnapshot, error) {
entries, err := w.list()
if err != nil {
return processSnapshot{}, err
}
snap := processSnapshot{PIDs: make(map[uint32]bool, len(entries)), Processes: make([]procInfo, 0, len(entries))}
for _, entry := range entries {
snap.PIDs[entry.PID] = true
if entry.PID == 0 || entry.PID == w.self {
continue
}
if blocked, ok := w.unreadable[entry.PID]; ok && blocked.Name == entry.Name && blocked.ParentPID == entry.ParentPID {
continue
}
info, err := w.inspect(entry)
if err != nil {
snap.Errors = append(snap.Errors, processError{Name: entry.Name, PID: entry.PID, Description: fmt.Sprintf("чтение: %v; внесён в блоклист", err)})
w.unreadable[entry.PID] = entry
continue
}
delete(w.unreadable, entry.PID)
snap.Processes = append(snap.Processes, info)
}
for pid := range w.unreadable {
if !snap.PIDs[pid] {
delete(w.unreadable, pid)
}
}
return snap, nil
}
func (w *windowsBackend) Alive(p *procInfo) (bool, error) { return processAlive(p.Handle) }
func (w *windowsBackend) Priority(p *procInfo) (uint32, error) {
r, _, err := procGetPriority.Call(uintptr(p.Handle))
if r == 0 {
return 0, fmt.Errorf("GetPriorityClass: %w", err)
}
return uint32(r), nil
}
func (w *windowsBackend) SetPriority(p *procInfo, target uint32) error {
// Request write access only for an actual change, never while enumerating.
h, err := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
if err != nil {
return err
}
defer closeH(h)
created, err := processCreated(h)
if err != nil {
return err
}
if created != p.Key.Created {
return fmt.Errorf("process instance changed; refusing to write")
}
return setPrio(h, p.PID, target)
}
func (w *windowsBackend) Close(p *procInfo) {
if p.Handle != 0 {
closeH(p.Handle)
p.Handle = 0
}
}