1
Files
autoPriority/main.go
T

426 lines
10 KiB
Go

//go:build windows
package main
import (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"unsafe"
)
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessSetInformation = 0x00000200
ProcessQueryInformation = 0x00000400
PriorityClassIdle = 0x00000040
PriorityClassNormal = 0x00000020
PriorityClassHigh = 0x00000080
PriorityClassAboveNormal = 0x00008000
PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000
MessageBoxIconWarning = 0x00000030
ErrorAlreadyExists = 183
ErrorNoMoreFiles = 18
ErrorAccessDenied = 5
ErrorInvalidParameter = 87
)
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
PeakPagefileUsage uintptr
PagefileUsage uintptr
PrivateUsage uintptr
}
type processInfo struct {
PID uint32
Name string
}
var (
k32 = syscall.NewLazyDLL("kernel32.dll")
ps = syscall.NewLazyDLL("psapi.dll")
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = k32.NewProc("Process32FirstW")
procProcess32Next = k32.NewProc("Process32NextW")
procGetMemInfo = ps.NewProc("GetProcessMemoryInfo")
procSetPriority = k32.NewProc("SetPriorityClass")
procGetPriority = k32.NewProc("GetPriorityClass")
procOpenProcess = k32.NewProc("OpenProcess")
procCloseHandle = k32.NewProc("CloseHandle")
procCreateMutex = k32.NewProc("CreateMutexW")
procMessageBox = k32.NewProc("MessageBoxW")
)
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(pid uint32, cls uint32) error {
h, err := openProc(pid, ProcessSetInformation)
if err != nil {
return err
}
defer closeH(h)
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
if r == 0 {
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
}
return nil
}
func ignoreOpenError(err error) bool {
return errors.Is(err, syscall.Errno(ErrorAccessDenied)) ||
errors.Is(err, syscall.Errno(ErrorInvalidParameter))
}
func curPrioWithHandle(h syscall.Handle) (uint32, error) {
r, _, e := procGetPriority.Call(uintptr(h))
if r == 0 {
return 0, fmt.Errorf("GetPriorityClass failed: %w", e)
}
return uint32(r), nil
}
func utf16Trim(p []uint16) string {
for i, v := range p {
if v == 0 {
return syscall.UTF16ToString(p[:i])
}
}
return syscall.UTF16ToString(p)
}
func allProcs() ([]processInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 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 {
return nil, fmt.Errorf("Process32First failed: %w", e)
}
var out []processInfo
for {
name := utf16Trim(pe.ExeFile[:])
if name != "" {
out = append(out, processInfo{PID: pe.PID, 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 isAboveNormal(c uint32) bool {
return c == PriorityClassAboveNormal ||
c == PriorityClassHigh ||
c == PriorityClassRealtime
}
func prioName(c uint32) string {
switch c {
case PriorityClassIdle:
return "IDLE"
case PriorityClassBelowNormal:
return "BELOW_NORMAL"
case PriorityClassNormal:
return "NORMAL"
case PriorityClassAboveNormal:
return "ABOVE_NORMAL"
case PriorityClassHigh:
return "HIGH"
case PriorityClassRealtime:
return "REALTIME"
default:
return fmt.Sprintf("0x%X", c)
}
}
func procName(pid uint32) (string, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 0 {
return "", 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 {
return "", fmt.Errorf("Process32First failed: %w", e)
}
for {
if pe.PID == pid {
return utf16Trim(pe.ExeFile[:]), nil
}
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 "", fmt.Errorf("Process32Next failed: %w", e)
}
}
return "", nil
}
func isAlive(pid uint32, expectedName string) bool {
h, err := openProc(pid, ProcessQueryInformation)
if err != nil || h == 0 {
return false
}
closeH(h)
name, err := procName(pid)
if err != nil || name == "" {
return false
}
return name == expectedName
}
func ensureSingleInstance() syscall.Handle {
h, _, err := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Global\\autopriority_v1"))))
if h == 0 {
fmt.Fprintf(os.Stderr, "autoPriority: failed to create mutex: %v\n", err)
os.Exit(1)
}
if err == syscall.Errno(ErrorAlreadyExists) {
procMessageBox.Call(0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority is already running."))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority"))),
MessageBoxIconWarning,
)
os.Exit(1)
}
return syscall.Handle(h)
}
func main() {
mem := flag.Uint64("mem", 512*1024*1024, "memory threshold in bytes")
interval := flag.Duration("interval", time.Minute, "scan interval")
dryRun := flag.Bool("dry-run", false, "log only, do not change priorities")
flag.Parse()
if *interval < 10*time.Second {
*interval = 10 * time.Second
}
var logFile *os.File
logPath := filepath.Join(os.TempDir(), "autopriority.log")
os.Remove(logPath)
f, err := os.OpenFile(logPath,
os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err == nil {
logFile = f
defer logFile.Close()
}
ts := func() string {
return time.Now().Format("02.01.2006 15:04:05")
}
logf := func(format string, a ...any) {
if logFile == nil {
return
}
fmt.Fprintf(logFile, "[%s] ", ts())
fmt.Fprintf(logFile, format+"\n", a...)
logFile.Sync()
}
logf("autoPriority started (mem=%d bytes, interval=%s, dry-run=%v)", *mem, *interval, *dryRun)
mutex := ensureSingleInstance()
defer closeH(mutex)
if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil {
logf("warning: could not set own priority to IDLE: %v", err)
} else {
logf("own priority set to IDLE")
}
promoted := make(map[uint32]string)
blocked := make(map[uint32]string)
defer func() {
for pid, name := range promoted {
cur, err := procName(pid)
if err != nil || cur != name {
logf("SKIP RESTORE PID %d: process name mismatch (expected %s, got %s)", pid, name, cur)
continue
}
if err := setPrio(pid, PriorityClassNormal); err != nil {
logf("RESTORE %s (PID %d) -> NORMAL error: %v", name, pid, err)
} else {
logf("RESTORE %s (PID %d) -> NORMAL", name, pid)
}
}
logf("autoPriority stopped")
}()
myPID := uint32(os.Getpid())
ticker := time.NewTicker(*interval)
defer ticker.Stop()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
scan := func() {
procs, err := allProcs()
if err != nil {
logf("allProcs error: %v", err)
return
}
for pid, name := range promoted {
if !isAlive(pid, name) {
delete(promoted, pid)
}
}
for pid, name := range blocked {
if !isAlive(pid, name) {
delete(blocked, pid)
}
}
for _, p := range procs {
if p.PID == myPID || p.PID == 0 {
continue
}
if _, ok := promoted[p.PID]; ok {
continue
}
if _, ok := blocked[p.PID]; ok {
continue
}
h, err := openProc(p.PID, ProcessQueryInformation|ProcessSetInformation)
if err != nil {
if !ignoreOpenError(err) {
logf("openProc(%s, PID %d) error: %v", p.Name, p.PID, err)
}
continue
}
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 {
closeH(h)
logf("GetProcessMemoryInfo(%d) error: %v", p.PID, e)
continue
}
rssBytes := uint64(m.WorkingSetSize)
if rssBytes < *mem {
origPrio, prioErr := curPrioWithHandle(h)
if prioErr == nil && isAboveNormal(origPrio) {
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set NORMAL",
p.Name, p.PID, rssBytes/1024/1024, prioName(origPrio))
} else {
sr, _, se := procSetPriority.Call(uintptr(h), uintptr(PriorityClassNormal))
if sr != 0 {
logf("DEMOTE %s (PID %d) RSS=%dMB, %s -> NORMAL",
p.Name, p.PID, rssBytes/1024/1024, prioName(origPrio))
} else {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, se)
}
}
}
closeH(h)
continue
}
rssMB := rssBytes / 1024 / 1024
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
continue
}
if origPrio == PriorityClassHigh {
closeH(h)
continue
}
origStr := prioName(origPrio)
if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set HIGH",
p.Name, p.PID, rssMB, origStr)
continue
}
sr, _, se := procSetPriority.Call(uintptr(h), uintptr(PriorityClassHigh))
closeH(h)
if sr == 0 {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, se)
continue
}
promoted[p.PID] = p.Name
logf("PROMOTE %s (PID %d) RSS=%dMB, %s -> HIGH",
p.Name, p.PID, rssMB, origStr)
}
}
scan()
for {
select {
case <-stop:
logf("received shutdown signal")
return
case <-ticker.C:
scan()
}
}
}