Fix process entry struct, add demote logic, block protected processes, improve logging
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user