1

Fix process entry struct, add demote logic, block protected processes, improve logging

This commit is contained in:
2026-06-21 15:11:15 +03:00
parent d4da33ca5e
commit b5885791fa
2 changed files with 176 additions and 65 deletions
+30 -2
View File
@@ -1,9 +1,24 @@
# autoPriority # autoPriority
Automatically sets high CPU priority for processes exceeding a memory threshold. Monitors process memory usage on Windows and automatically adjusts CPU priority: promotes memory-heavy processes to HIGH and demotes everything else to NORMAL.
**Windows only.** **Windows only.**
## How it works
Every scan interval the program iterates over all running processes:
| RSS vs threshold | Current priority | Action |
|---|---|---|
| ≥ threshold | anything except HIGH | → HIGH (logged as `PROMOTE`) |
| ≥ threshold | already HIGH | skip |
| < threshold | ABOVE_NORMAL, HIGH, or REALTIME | → NORMAL (logged as `DEMOTE`) |
| < threshold | NORMAL, BELOW_NORMAL, or IDLE | skip |
If `SetPriorityClass` fails for a process (e.g., anti-cheat protection), the process is added to an in-memory exclusion list and never touched again (logged as `BLOCK`). When the process exits, it is automatically removed from the list.
On shutdown, all promoted processes are restored to NORMAL.
## Build ## Build
Requires Go 1.26+. Requires Go 1.26+.
@@ -38,7 +53,20 @@ autopriority -mem=1073741824 -interval=30s
autopriority -dry-run autopriority -dry-run
``` ```
Log is always written to `%TEMP%\autopriority.log`. ## Log
Log is always written to `%TEMP%\autopriority.log`. A new log file is created on each run (previous log is deleted).
Log entries:
| Prefix | Meaning |
|---|---|
| `PROMOTE` | Priority raised to HIGH |
| `DEMOTE` | Priority lowered to NORMAL |
| `BLOCK` | SetPriorityClass failed; process added to exclusion list |
| `RESTORE` | Promoted process restored to NORMAL on shutdown |
| `SKIP RESTORE` | PID reused by a different process; restore skipped |
| `[DRY-RUN]` | Would change priority (dry-run mode) |
## Auto-start ## Auto-start
+137 -54
View File
@@ -3,6 +3,7 @@
package main package main
import ( import (
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@@ -20,16 +21,23 @@ const (
PriorityClassIdle = 0x00000040 PriorityClassIdle = 0x00000040
PriorityClassNormal = 0x00000020 PriorityClassNormal = 0x00000020
PriorityClassHigh = 0x00000080 PriorityClassHigh = 0x00000080
PriorityClassAboveNormal = 0x00008000
PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000
MessageBoxIconWarning = 0x00000030 MessageBoxIconWarning = 0x00000030
ErrorAlreadyExists = 183 ErrorAlreadyExists = 183
ErrorNoMoreFiles = 18
ErrorAccessDenied = 5
ErrorInvalidParameter = 87
) )
type processEntry32 struct { type processEntry32 struct {
Size uint32 Size uint32
CntUsage uint32 CntUsage uint32
PID uint32 PID uint32
DefaultHeapID uint32 DefaultHeapID uintptr
ModuleID uint32 ModuleID uint32
CntThreads uint32
ParentPID uint32 ParentPID uint32
PrioClass int32 PrioClass int32
Flags uint32 Flags uint32
@@ -69,7 +77,6 @@ var (
procCloseHandle = k32.NewProc("CloseHandle") procCloseHandle = k32.NewProc("CloseHandle")
procCreateMutex = k32.NewProc("CreateMutexW") procCreateMutex = k32.NewProc("CreateMutexW")
procMessageBox = k32.NewProc("MessageBoxW") procMessageBox = k32.NewProc("MessageBoxW")
procGetLastError = k32.NewProc("GetLastError")
) )
func closeH(h syscall.Handle) { func closeH(h syscall.Handle) {
@@ -97,33 +104,17 @@ func setPrio(pid uint32, cls uint32) error {
return nil return nil
} }
func curPrio(pid uint32) (uint32, error) { func ignoreOpenError(err error) bool {
h, err := openProc(pid, ProcessQueryInformation) return errors.Is(err, syscall.Errno(ErrorAccessDenied)) ||
if err != nil { errors.Is(err, syscall.Errno(ErrorInvalidParameter))
return 0, err
}
defer closeH(h)
r, _, e := procGetPriority.Call(uintptr(h))
if r == 0 {
return 0, fmt.Errorf("GetPriorityClass(%d) failed: %w", pid, e)
}
return uint32(r), nil
} }
func rss(pid uint32) (uint64, error) { func curPrioWithHandle(h syscall.Handle) (uint32, error) {
h, err := openProc(pid, ProcessQueryInformation) r, _, e := procGetPriority.Call(uintptr(h))
if err != nil {
return 0, err
}
defer closeH(h)
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 { if r == 0 {
return 0, fmt.Errorf("GetProcessMemoryInfo(%d) failed: %w", pid, e) return 0, fmt.Errorf("GetPriorityClass failed: %w", e)
} }
return uint64(m.WorkingSetSize), nil return uint32(r), nil
} }
func utf16Trim(p []uint16) string { func utf16Trim(p []uint16) string {
@@ -157,8 +148,7 @@ func allProcs() ([]processInfo, error) {
pe.Size = uint32(unsafe.Sizeof(processEntry32{})) pe.Size = uint32(unsafe.Sizeof(processEntry32{}))
r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe))) r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe)))
if r == 0 { if r == 0 {
le, _, _ := procGetLastError.Call() if e == syscall.Errno(ErrorNoMoreFiles) {
if le == 18 {
break break
} }
return nil, fmt.Errorf("Process32Next failed: %w", e) return nil, fmt.Errorf("Process32Next failed: %w", e)
@@ -167,26 +157,71 @@ func allProcs() ([]processInfo, error) {
return out, nil return out, nil
} }
func isAboveNormal(c uint32) bool {
return c == PriorityClassAboveNormal ||
c == PriorityClassHigh ||
c == PriorityClassRealtime
}
func prioName(c uint32) string { func prioName(c uint32) string {
switch c { switch c {
case PriorityClassIdle: case PriorityClassIdle:
return "IDLE" return "IDLE"
case PriorityClassBelowNormal:
return "BELOW_NORMAL"
case PriorityClassNormal: case PriorityClassNormal:
return "NORMAL" return "NORMAL"
case PriorityClassAboveNormal:
return "ABOVE_NORMAL"
case PriorityClassHigh: case PriorityClassHigh:
return "HIGH" return "HIGH"
case PriorityClassRealtime:
return "REALTIME"
default: default:
return fmt.Sprintf("0x%X", c) return fmt.Sprintf("0x%X", c)
} }
} }
func isAlive(pid uint32) bool { 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) h, err := openProc(pid, ProcessQueryInformation)
if err != nil || h == 0 { if err != nil || h == 0 {
return false return false
} }
closeH(h) closeH(h)
return true name, err := procName(pid)
if err != nil || name == "" {
return false
}
return name == expectedName
} }
func ensureSingleInstance() syscall.Handle { func ensureSingleInstance() syscall.Handle {
@@ -195,8 +230,7 @@ func ensureSingleInstance() syscall.Handle {
fmt.Fprintf(os.Stderr, "autoPriority: failed to create mutex: %v\n", err) fmt.Fprintf(os.Stderr, "autoPriority: failed to create mutex: %v\n", err)
os.Exit(1) os.Exit(1)
} }
le, _, _ := procGetLastError.Call() if err == syscall.Errno(ErrorAlreadyExists) {
if uint32(le) == ErrorAlreadyExists {
procMessageBox.Call(0, procMessageBox.Call(0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority is already running."))), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority is already running."))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority"))), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority"))),
@@ -218,15 +252,14 @@ func main() {
} }
var logFile *os.File var logFile *os.File
logDir := os.TempDir() logPath := filepath.Join(os.TempDir(), "autopriority.log")
if logDir != "" { os.Remove(logPath)
f, err := os.OpenFile(filepath.Join(logDir, "autopriority.log"), f, err := os.OpenFile(logPath,
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err == nil { if err == nil {
logFile = f logFile = f
defer logFile.Close() defer logFile.Close()
} }
}
ts := func() string { ts := func() string {
return time.Now().Format("02.01.2006 15:04:05") return time.Now().Format("02.01.2006 15:04:05")
@@ -253,10 +286,18 @@ func main() {
} }
promoted := make(map[uint32]string) promoted := make(map[uint32]string)
blocked := make(map[uint32]string)
defer func() { defer func() {
for pid, name := range promoted { for pid, name := range promoted {
if err := setPrio(pid, PriorityClassNormal); err == nil { 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("RESTORE %s (PID %d) -> NORMAL", name, pid)
} }
} }
@@ -277,12 +318,18 @@ func main() {
return return
} }
for pid := range promoted { for pid, name := range promoted {
if !isAlive(pid) { if !isAlive(pid, name) {
delete(promoted, pid) delete(promoted, pid)
} }
} }
for pid, name := range blocked {
if !isAlive(pid, name) {
delete(blocked, pid)
}
}
for _, p := range procs { for _, p := range procs {
if p.PID == myPID || p.PID == 0 { if p.PID == myPID || p.PID == 0 {
continue continue
@@ -290,31 +337,72 @@ func main() {
if _, ok := promoted[p.PID]; ok { if _, ok := promoted[p.PID]; ok {
continue continue
} }
if _, ok := blocked[p.PID]; ok {
rssBytes, err := rss(p.PID)
if err != nil {
logf("rss(%d) error: %v", p.PID, err)
continue 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 { 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 continue
} }
rssMB := rssBytes / 1024 / 1024 rssMB := rssBytes / 1024 / 1024
origPrio, prioErr := curPrio(p.PID) origPrio, prioErr := curPrioWithHandle(h)
origStr := prioName(origPrio)
if prioErr != nil { if prioErr != nil {
origStr = "unknown" closeH(h)
continue
} }
if origPrio == PriorityClassHigh {
closeH(h)
continue
}
origStr := prioName(origPrio)
if *dryRun { if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set HIGH", logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set HIGH",
p.Name, p.PID, rssMB, origStr) p.Name, p.PID, rssMB, origStr)
continue continue
} }
if err := setPrio(p.PID, PriorityClassHigh); err != nil { sr, _, se := procSetPriority.Call(uintptr(h), uintptr(PriorityClassHigh))
logf("setPrio(%d, HIGH) error: %v", p.PID, err) 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 continue
} }
@@ -325,18 +413,13 @@ func main() {
} }
scan() scan()
select {
case <-stop:
logf("received shutdown signal")
case <-ticker.C:
for { for {
scan()
select { select {
case <-stop: case <-stop:
logf("received shutdown signal") logf("received shutdown signal")
return return
case <-ticker.C: case <-ticker.C:
} scan()
} }
} }
} }