From b5885791facb91b2f338094469f0aa7b066f8aff Mon Sep 17 00:00:00 2001 From: lzrdblzzrd Date: Sun, 21 Jun 2026 15:11:15 +0300 Subject: [PATCH] Fix process entry struct, add demote logic, block protected processes, improve logging --- README.md | 32 ++++++++- main.go | 209 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 176 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 0c1a013..6b73032 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,24 @@ # 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.** +## 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 Requires Go 1.26+. @@ -38,7 +53,20 @@ autopriority -mem=1073741824 -interval=30s 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 diff --git a/main.go b/main.go index 5b71695..1276b4f 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( + "errors" "flag" "fmt" "os" @@ -20,16 +21,23 @@ const ( 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 uint32 + DefaultHeapID uintptr ModuleID uint32 + CntThreads uint32 ParentPID uint32 PrioClass int32 Flags uint32 @@ -43,7 +51,7 @@ type processMemoryCounters struct { WorkingSetSize uintptr QuotaPeakPagedPoolUsage uintptr QuotaPagedPoolUsage uintptr - QuotaPeakNonPagedPoolUsage uintptr + QuotaPeakNonPagedPoolUsage uintptr QuotaNonPagedPoolUsage uintptr PeakPagefileUsage uintptr PagefileUsage uintptr @@ -69,7 +77,6 @@ var ( procCloseHandle = k32.NewProc("CloseHandle") procCreateMutex = k32.NewProc("CreateMutexW") procMessageBox = k32.NewProc("MessageBoxW") - procGetLastError = k32.NewProc("GetLastError") ) func closeH(h syscall.Handle) { @@ -97,33 +104,17 @@ func setPrio(pid uint32, cls uint32) error { return nil } -func curPrio(pid uint32) (uint32, error) { - h, err := openProc(pid, ProcessQueryInformation) - if err != nil { - 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 ignoreOpenError(err error) bool { + return errors.Is(err, syscall.Errno(ErrorAccessDenied)) || + errors.Is(err, syscall.Errno(ErrorInvalidParameter)) } -func rss(pid uint32) (uint64, error) { - h, err := openProc(pid, ProcessQueryInformation) - 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))) +func curPrioWithHandle(h syscall.Handle) (uint32, error) { + r, _, e := procGetPriority.Call(uintptr(h)) 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 { @@ -157,8 +148,7 @@ func allProcs() ([]processInfo, error) { pe.Size = uint32(unsafe.Sizeof(processEntry32{})) r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe))) if r == 0 { - le, _, _ := procGetLastError.Call() - if le == 18 { + if e == syscall.Errno(ErrorNoMoreFiles) { break } return nil, fmt.Errorf("Process32Next failed: %w", e) @@ -167,26 +157,71 @@ func allProcs() ([]processInfo, error) { 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 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) if err != nil || h == 0 { return false } closeH(h) - return true + name, err := procName(pid) + if err != nil || name == "" { + return false + } + return name == expectedName } func ensureSingleInstance() syscall.Handle { @@ -195,8 +230,7 @@ func ensureSingleInstance() syscall.Handle { fmt.Fprintf(os.Stderr, "autoPriority: failed to create mutex: %v\n", err) os.Exit(1) } - le, _, _ := procGetLastError.Call() - if uint32(le) == ErrorAlreadyExists { + if err == syscall.Errno(ErrorAlreadyExists) { procMessageBox.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority is already running."))), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("autoPriority"))), @@ -218,14 +252,13 @@ func main() { } var logFile *os.File - logDir := os.TempDir() - if logDir != "" { - f, err := os.OpenFile(filepath.Join(logDir, "autopriority.log"), - os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err == nil { - logFile = f - defer logFile.Close() - } + 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 { @@ -253,10 +286,18 @@ func main() { } promoted := make(map[uint32]string) + blocked := make(map[uint32]string) defer func() { 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) } } @@ -277,12 +318,18 @@ func main() { return } - for pid := range promoted { - if !isAlive(pid) { + 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 @@ -290,31 +337,72 @@ func main() { if _, ok := promoted[p.PID]; ok { continue } - - rssBytes, err := rss(p.PID) - if err != nil { - logf("rss(%d) error: %v", p.PID, err) + 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 := curPrio(p.PID) - origStr := prioName(origPrio) + origPrio, prioErr := curPrioWithHandle(h) if prioErr != nil { - origStr = "unknown" + 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 } - if err := setPrio(p.PID, PriorityClassHigh); err != nil { - logf("setPrio(%d, HIGH) error: %v", p.PID, err) + 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 } @@ -325,18 +413,13 @@ func main() { } scan() - select { - case <-stop: - logf("received shutdown signal") - case <-ticker.C: - for { + for { + select { + case <-stop: + logf("received shutdown signal") + return + case <-ticker.C: scan() - select { - case <-stop: - logf("received shutdown signal") - return - case <-ticker.C: - } } } }