1

Rewrite game mode: two-pass scan, priority restoration, LimitedInformation access

- Use PROCESS_QUERY_LIMITED_INFORMATION instead of PROCESS_QUERY_INFORMATION
  to open processes (fixes Access Denied for KeePassXC, HD-Player, etc.)
- Separate query handle from set handle: trySetPrio reopens with
  PROCESS_SET_INFORMATION only when needed
- Two-pass scan: pass 1 collects RSS and detects game processes,
  pass 2 applies priorities with correct hasGame state
- Game mode saves original priorities in gameSaved map, restores them
  on GAME MODE OFF (not just NORMAL)
- Demote ALL non-game processes to IDLE in game mode (not just promoted)
- 50ms delay between priority changes for smooth transitions
- Replace isAlive/procName with pidMap from allProcs snapshot
- Replace mutex/MessageBox single-instance with killOtherInstances
- Defaults: -mem=512M, -game-mem=4G
- Fix double CloseHandle and handle leak in trySetPrio
- Update README with new defaults and game mode restoration behavior
This commit is contained in:
2026-06-22 17:24:21 +03:00
parent b5885791fa
commit 565b751195
2 changed files with 335 additions and 116 deletions
+298 -105
View File
@@ -3,12 +3,13 @@
package main
import (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
@@ -16,19 +17,17 @@ import (
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessTerminate = 0x00000001
ProcessSetInformation = 0x00000200
ProcessQueryInformation = 0x00000400
ProcessQueryLimitedInformation = 0x00001000
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 {
@@ -75,8 +74,7 @@ var (
procGetPriority = k32.NewProc("GetPriorityClass")
procOpenProcess = k32.NewProc("OpenProcess")
procCloseHandle = k32.NewProc("CloseHandle")
procCreateMutex = k32.NewProc("CreateMutexW")
procMessageBox = k32.NewProc("MessageBoxW")
procTerminate = k32.NewProc("TerminateProcess")
)
func closeH(h syscall.Handle) {
@@ -104,11 +102,6 @@ func setPrio(pid uint32, cls uint32) error {
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 {
@@ -126,6 +119,49 @@ func utf16Trim(p []uint16) string {
return syscall.UTF16ToString(p)
}
func parseMemSize(s string) (uint64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty value")
}
var suffix byte
switch s[len(s)-1] {
case 'k', 'K':
suffix = 'K'
s = s[:len(s)-1]
case 'm', 'M':
suffix = 'M'
s = s[:len(s)-1]
case 'g', 'G':
suffix = 'G'
s = s[:len(s)-1]
}
n, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
switch suffix {
case 'K':
return n * 1024, nil
case 'M':
return n * 1024 * 1024, nil
case 'G':
return n * 1024 * 1024 * 1024, nil
default:
return n, nil
}
}
func formatMemSize(b uint64) string {
if b >= 1024*1024*1024 {
return fmt.Sprintf("%.1fGB", float64(b)/(1024*1024*1024))
}
if b >= 1024*1024 {
return fmt.Sprintf("%dMB", b/1024/1024)
}
return fmt.Sprintf("%dKB", b/1024)
}
func allProcs() ([]processInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 0 {
@@ -182,67 +218,29 @@ func prioName(c uint32) string {
}
}
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
func killOtherInstances() {
myPID := os.Getpid()
procs, err := allProcs()
if err != nil {
return
}
myName := filepath.Base(os.Args[0])
for _, p := range procs {
if p.PID != uint32(myPID) && filepath.Base(p.Name) == myName {
h, err := openProc(p.PID, ProcessTerminate)
if err == nil {
procTerminate.Call(uintptr(h), 0)
closeH(h)
}
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")
memStr := flag.String("mem", "512M", "memory threshold (e.g. 512M, 1G, 2048M)")
gameMemStr := flag.String("game-mem", "4G", "game memory threshold (e.g. 3G, 4G). Must be greater than -mem. 0 = disabled.")
interval := flag.Duration("interval", time.Minute, "scan interval")
dryRun := flag.Bool("dry-run", false, "log only, do not change priorities")
flag.Parse()
@@ -251,6 +249,25 @@ func main() {
*interval = 10 * time.Second
}
mem, err := parseMemSize(*memStr)
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: invalid -mem value: %v\n", err)
os.Exit(1)
}
gameMem, err := parseMemSize(*gameMemStr)
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: invalid -game-mem value: %v\n", err)
os.Exit(1)
}
if gameMem > 0 && gameMem <= mem {
fmt.Fprintf(os.Stderr, "autoPriority: -game-mem must be greater than -mem\n")
os.Exit(1)
}
killOtherInstances()
time.Sleep(100 * time.Millisecond)
var logFile *os.File
logPath := filepath.Join(os.TempDir(), "autopriority.log")
os.Remove(logPath)
@@ -274,10 +291,11 @@ func main() {
logFile.Sync()
}
logf("autoPriority started (mem=%d bytes, interval=%s, dry-run=%v)", *mem, *interval, *dryRun)
mutex := ensureSingleInstance()
defer closeH(mutex)
if gameMem > 0 {
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)", formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
} else {
logf("autoPriority started (mem=%s, interval=%s, dry-run=%v)", formatMemSize(mem), *interval, *dryRun)
}
if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil {
logf("warning: could not set own priority to IDLE: %v", err)
@@ -287,11 +305,44 @@ func main() {
promoted := make(map[uint32]string)
blocked := make(map[uint32]string)
gameProcs := make(map[uint32]string)
gameMode := false
type savedPrio struct {
name string
prio uint32
}
gameSaved := make(map[uint32]savedPrio)
defer func() {
restoreProcs, _ := allProcs()
restoreMap := make(map[uint32]string, len(restoreProcs))
for _, p := range restoreProcs {
restoreMap[p.PID] = p.Name
}
for pid, sv := range gameSaved {
if cur, ok := restoreMap[pid]; !ok || cur != sv.name {
continue
}
if err := setPrio(pid, sv.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", sv.name, pid, prioName(sv.prio), err)
} else {
logf("RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
}
}
for pid, name := range promoted {
cur, err := procName(pid)
if err != nil || cur != name {
if cur, ok := restoreMap[pid]; !ok || 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)
}
}
for pid, name := range gameProcs {
if cur, ok := restoreMap[pid]; !ok || cur != name {
logf("SKIP RESTORE PID %d: process name mismatch (expected %s, got %s)", pid, name, cur)
continue
}
@@ -311,6 +362,25 @@ func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
trySetPrio := func(qh syscall.Handle, pid uint32, name string, cls uint32) bool {
closeH(qh)
sh, err := openProc(pid, ProcessSetInformation)
if err != nil {
blocked[pid] = name
logf("BLOCK %s (PID %d): open SetInformation failed: %v (added to exclusion list)", name, pid, err)
return false
}
defer closeH(sh)
sr, _, se := procSetPriority.Call(uintptr(sh), uintptr(cls))
if sr == 0 {
blocked[pid] = name
logf("BLOCK %s (PID %d): SetPriorityClass failed: %v (added to exclusion list)", name, pid, se)
return false
}
time.Sleep(50 * time.Millisecond)
return true
}
scan := func() {
procs, err := allProcs()
if err != nil {
@@ -318,34 +388,47 @@ func main() {
return
}
pidMap := make(map[uint32]string, len(procs))
for _, p := range procs {
pidMap[p.PID] = p.Name
}
for pid, name := range promoted {
if !isAlive(pid, name) {
if cur, ok := pidMap[pid]; !ok || cur != name {
delete(promoted, pid)
}
}
for pid, name := range blocked {
if !isAlive(pid, name) {
for pid := range blocked {
if _, ok := pidMap[pid]; !ok {
delete(blocked, pid)
}
}
for pid, name := range gameProcs {
if cur, ok := pidMap[pid]; !ok || cur != name {
delete(gameProcs, pid)
}
}
type procRSS struct {
info processInfo
rss uint64
}
var procList []procRSS
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)
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
if !ignoreOpenError(err) {
logf("openProc(%s, PID %d) error: %v", p.Name, p.PID, err)
}
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
continue
}
@@ -357,29 +440,116 @@ func main() {
logf("GetProcessMemoryInfo(%d) error: %v", p.PID, e)
continue
}
closeH(h)
rssBytes := uint64(m.WorkingSetSize)
if rssBytes < *mem {
origPrio, prioErr := curPrioWithHandle(h)
if prioErr == nil && isAboveNormal(origPrio) {
procList = append(procList, procRSS{p, rssBytes})
if gameMem > 0 && rssBytes >= gameMem {
if _, ok := gameProcs[p.PID]; !ok {
gameProcs[p.PID] = p.Name
}
}
}
hasGame := gameMem > 0 && len(gameProcs) > 0
for _, pr := range procList {
p := pr.info
rssBytes := pr.rss
if _, ok := gameProcs[p.PID]; ok {
if hasGame {
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
continue
}
if origPrio == PriorityClassHigh {
closeH(h)
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set NORMAL",
p.Name, p.PID, rssBytes/1024/1024, prioName(origPrio))
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (game)",
p.Name, p.PID, formatMemSize(rssBytes))
} 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))
if trySetPrio(h, p.PID, p.Name, PriorityClassHigh) {
logf("GAME %s (PID %d) RSS=%s -> HIGH",
p.Name, p.PID, formatMemSize(rssBytes))
} else {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, se)
logf("GAME %s (PID %d) RSS=%s — tracked as game (priority change failed)",
p.Name, p.PID, formatMemSize(rssBytes))
}
}
}
closeH(h)
continue
}
rssMB := rssBytes / 1024 / 1024
if hasGame {
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
continue
}
if origPrio == PriorityClassIdle {
closeH(h)
continue
}
origStr := prioName(origPrio)
if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set IDLE (game)",
p.Name, p.PID, formatMemSize(rssBytes), origStr)
} else {
if trySetPrio(h, p.PID, p.Name, PriorityClassIdle) {
if _, saved := gameSaved[p.PID]; !saved {
gameSaved[p.PID] = savedPrio{name: p.Name, prio: origPrio}
}
delete(promoted, p.PID)
logf("GAME %s (PID %d) RSS=%s, %s -> IDLE",
p.Name, p.PID, formatMemSize(rssBytes), origStr)
}
}
continue
}
if _, ok := promoted[p.PID]; ok {
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
if rssBytes < mem {
origPrio, prioErr := curPrioWithHandle(h)
if prioErr == nil && isAboveNormal(origPrio) {
if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set NORMAL",
p.Name, p.PID, formatMemSize(rssBytes), prioName(origPrio))
} else {
if trySetPrio(h, p.PID, p.Name, PriorityClassNormal) {
logf("DEMOTE %s (PID %d) RSS=%s, %s -> NORMAL",
p.Name, p.PID, formatMemSize(rssBytes), prioName(origPrio))
}
}
} else {
closeH(h)
}
continue
}
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
@@ -393,22 +563,45 @@ logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, se)
if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set HIGH",
p.Name, p.PID, rssMB, origStr)
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH",
p.Name, p.PID, formatMemSize(rssBytes), 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
if trySetPrio(h, p.PID, p.Name, PriorityClassHigh) {
promoted[p.PID] = p.Name
logf("PROMOTE %s (PID %d) RSS=%s, %s -> HIGH",
p.Name, p.PID, formatMemSize(rssBytes), origStr)
}
}
promoted[p.PID] = p.Name
logf("PROMOTE %s (PID %d) RSS=%dMB, %s -> HIGH",
p.Name, p.PID, rssMB, origStr)
if gameMem > 0 {
newHasGame := len(gameProcs) > 0
if newHasGame && !gameMode {
gameMode = true
logf("GAME MODE ON")
}
if !newHasGame && gameMode {
gameMode = false
logf("GAME MODE OFF")
for pid, sv := range gameSaved {
cur, ok := pidMap[pid]
if !ok || cur != sv.name {
delete(gameSaved, pid)
continue
}
if *dryRun {
logf("[DRY-RUN] RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
} else {
if err := setPrio(pid, sv.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", sv.name, pid, prioName(sv.prio), err)
} else {
logf("RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
}
}
delete(gameSaved, pid)
}
}
}
}