13 Commits
Author SHA1 Message Date
lzrdblzzrd 4f8f6218ad Rewrite: improve process handling and restoration 2026-08-08 14:08:38 +03:00
lzrdblzzrd bf37dd6974 Update README: fix defaults (game-mem=2G), remove SKIP RESTORE, add GAME DETECT, update game mode description 2026-06-23 11:20:55 +03:00
lzrdblzzrd 0e8d7333c1 Rewrite: simplify to strict algorithm, remove promoted map
- Remove promoted map entirely — processes are demoted when RSS < -mem
  and promoted when RSS >= -mem, no permanent state
- Remove trySetPrio closure — use setPrio directly for simplicity
- Remove curPrioWithHandle helper — inline procGetPriority.Call
- Remove GAME ... tracked as game (blocked) repeated log — log once
  on detection via GAME DETECT, silently skip on subsequent scans
- Game mode: log GAME DETECT once when process first exceeds game-mem
- gameSaved stores original priority before IDLE demote, restores on
  GAME MODE OFF or shutdown
- Delay between priority changes: 100ms
- Delete TZ.md (was a test for other AI)
2026-06-23 11:16:51 +03:00
lzrdblzzrd da13a6d549 Demote promoted processes when RSS drops below threshold
Previously, once a process was promoted to HIGH, it stayed HIGH forever
even if its RSS dropped well below the -mem threshold. Now promoted
processes are demoted back to NORMAL when RSS < -mem, and removed from
the promoted map.

Also: added TZ.md with full technical specification of the project.
2026-06-23 08:58:11 +03:00
lzrdblzzrd ea914101bb Fix: blocked processes still measured for game-mem detection
Previously, when SetPriorityClass failed for a process (e.g. anti-cheat),
it was added to 'blocked' and completely skipped in future scans —
including RSS measurement. This meant a game that started below game-mem
threshold and got blocked before reaching it would never trigger game mode.

Now pass 1 measures RSS for ALL processes (including blocked ones) and
adds them to gameProcs if they exceed game-mem. Pass 2 skips priority
changes for blocked processes but still tracks them as game processes.

In game mode, blocked game procs log 'tracked as game (blocked)' instead
of trying SetPriorityClass again.
2026-06-22 17:41:10 +03:00
lzrdblzzrd 565b751195 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
2026-06-22 17:24:21 +03:00
lzrdblzzrd b5885791fa Fix process entry struct, add demote logic, block protected processes, improve logging 2026-06-21 15:11:15 +03:00
lzrdblzzrd d4da33ca5e fix: ErrorAlreadyExists 1832->183, wrap syscall errors, signal handling, mutex cleanup, log sync 2026-06-20 23:41:59 +03:00
lzrdblzzrd 33dfd4b4c5 fix: processEntry32 DefaultHeapID uint32 instead of uintptr 2026-06-19 23:24:01 +03:00
lzrdblzzrd b999b7ec4f refactor: error handling, single rss call, stale PID cleanup, utf16 trim, O_APPEND log 2026-06-19 23:20:10 +03:00
lzrdblzzrd d0cafe1332 log: move log file to temp directory with fallback 2026-06-19 21:39:45 +03:00
lzrdblzzrd c3cb8ae847 rewrite: single file, zero deps, Windows API, logging, CLI flags 2026-06-19 21:37:01 +03:00
lzrdblzzrd c8648da296 updated with opencode 2026-06-17 02:15:43 +03:00
7 changed files with 815 additions and 90 deletions
+11
View File
@@ -0,0 +1,11 @@
*.exe
*.log
*.exe~
*.swp
*.swo
*~
.env
.idea/
.vscode/
.DS_Store
Thumbs.db
+104 -1
View File
@@ -1,3 +1,106 @@
# autoPriority # autoPriority
A program that automatically sets a high priority for game processes Отслеживает использование памяти процессами в Windows и автоматически управляет их приоритетом: ресурсоемким процессам назначает HIGH, а менее ресурсоемким снижает приоритет до NORMAL, только если он был выше. При превышении повышенного порога может включать **игровой режим**, назначая обнаруженному процессу HIGH, а всем остальным — IDLE.
**Только для Windows.**
## Принцип работы
### Обычный режим
Через заданный интервал программа проверяет все запущенные процессы:
| RSS относительно `-mem` | Текущий приоритет | Действие |
|---|---|---|
| ≥ порога | любой, кроме HIGH | → HIGH (запись `PROMOTE` в журнале) |
| ≥ порога | уже HIGH | пропустить |
| < порога | ABOVE_NORMAL, HIGH или REALTIME | → NORMAL (запись `DEMOTE` в журнале) |
| < порога | NORMAL, BELOW_NORMAL или IDLE | пропустить |
### Игровой режим (`-game-mem`)
Когда любой процесс достигает или превышает порог `-game-mem`:
1. Этому процессу назначается **HIGH** (запись `GAME` в журнале).
2. Всем остальным процессам назначается **IDLE** с сохранением исходных приоритетов.
3. В журнал добавляется запись `GAME MODE ON`.
Когда все такие процессы завершаются:
1. Всем процессам, приоритет которых был снижен в игровом режиме, **возвращается исходный приоритет** (запись `RESTORE` в журнале).
2. В журнал добавляется запись `GAME MODE OFF`.
3. Возобновляется обычный режим, и приоритеты пересчитываются по правилам `-mem`.
Если изменить приоритет процесса невозможно, например из-за античита или системных ограничений, процесс добавляется во внутренний список исключений и больше не изменяется (запись `BLOCK` в журнале). Его RSS продолжает измеряться, поэтому он может включить игровой режим. Процессы, память которых прочитать не удалось, пропускаются до следующей проверки.
При штатном завершении программа несколько раз пытается восстановить приоритеты всех процессов, измененных в игровом режиме. Если Windows отклоняет восстановление, в журнал записывается предупреждение.
В одном сеансе Windows может работать только один экземпляр программы. Второй экземпляр завершается, не прерывая работу первого.
## Сборка
Требуется Go 1.26 или новее.
```
# Обычная сборка с окном консоли
go build -o autopriority.exe .
# Фоновая сборка без консоли, минимальный размер
go build -trimpath -buildvcs=false -gcflags="all=-l" -ldflags="-H=windowsgui -s -w -buildid=" -o autopriority.exe .
```
## Использование
```
autopriority [flags]
```
| Флаг | Значение по умолчанию | Описание |
|-------------|---------------|----------------------------------|
| `-mem` | 512M | Порог памяти, например 512M, 1G или 2048M |
| `-game-mem` | 2G | Порог игрового режима, например 2G или 4G. Должен быть больше `-mem`. 0 отключает режим |
| `-interval` | 1 минута | Интервал проверки, минимум 10 секунд |
| `-dry-run` | false | Только записывать решения в журнал, не менять приоритеты |
Примеры:
```
# Порог 1 ГБ, проверка каждые 30 секунд
autopriority -mem=1G -interval=30s
# Запуск со значениями по умолчанию: порог 512M, игровой порог 2G
autopriority
# Обычный порог 512M, игровой порог 4G
autopriority -mem=512M -game-mem=4G
# Пробный запуск: записывать решения, ничего не изменяя
autopriority -dry-run
```
## Журнал
Журнал всегда записывается в `%TEMP%\autopriority.log`. При каждом запуске создается новый файл, а предыдущий удаляется.
Типы записей:
| Префикс | Значение |
|---|---|
| `PROMOTE` | Приоритет повышен до HIGH в обычном режиме |
| `DEMOTE` | Приоритет снижен до NORMAL в обычном режиме |
| `GAME DETECT` | Впервые обнаружен процесс, превысивший порог `-game-mem` |
| `GAME` | Процессу назначен HIGH или IDLE в игровом режиме |
| `GAME MODE ON` | Игровой режим включен |
| `GAME MODE OFF` | Игровой режим выключен, приоритеты восстановлены |
| `BLOCK` | Ошибка OpenProcess или SetPriorityClass, процесс добавлен в список исключений |
| `SKIP` | Не удалось прочитать сведения о процессе, попытка повторится при следующей проверке |
| `RESTORE` | Восстановлен исходный приоритет при выходе из игрового режима или завершении программы |
| `[DRY-RUN]` | Приоритет был бы изменен в пробном режиме |
## Автозапуск
Нажмите **Win+R**, введите `shell:startup` и нажмите Enter. Поместите ярлык `autopriority.exe` в открывшуюся папку.
## Зависимости
Отсутствуют. Программа напрямую вызывает Windows API (`kernel32`, `psapi`) через syscall.
+2 -12
View File
@@ -1,13 +1,3 @@
module autoPriority module autopriority
go 1.19 go 1.26
require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/postfinance/single v0.0.2 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/sys v0.4.0 // indirect
)
-22
View File
@@ -1,22 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/postfinance/single v0.0.2 h1:YLjLxxeGDnsW93oK4CxxOFSVOOiBi1OyoK4ZTl5biJw=
github.com/postfinance/single v0.0.2/go.mod h1:OYWUsdMIZK9eQyZYpAsMHN+j6+jXJ6RFUNqIWH0oC5U=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+635
View File
@@ -0,0 +1,635 @@
//go:build windows
package main
import (
"flag"
"fmt"
"math"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
)
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessSetInformation = 0x00000200
ProcessQueryLimitedInformation = 0x00001000
PriorityClassIdle = 0x00000040
PriorityClassNormal = 0x00000020
PriorityClassHigh = 0x00000080
PriorityClassAboveNormal = 0x00008000
PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000
ErrorNoMoreFiles = 18
ErrorAlreadyExists = 183
)
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 processKey struct {
PID uint32
Created uint64
}
type procInfo struct {
PID uint32
Name string
RSS uint64
Key processKey
Prio uint32
Handle syscall.Handle
CanSet bool
Keep bool
}
type trackedProc struct {
name string
handle syscall.Handle
}
type savedPrio struct {
name string
prio uint32
handle syscall.Handle
}
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")
procGetProcessTimes = k32.NewProc("GetProcessTimes")
procQueryImageName = k32.NewProc("QueryFullProcessImageNameW")
procCreateMutex = k32.NewProc("CreateMutexW")
)
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(h syscall.Handle, pid uint32, cls uint32) error {
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
if r == 0 {
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
}
return nil
}
func processCreated(h syscall.Handle) (uint64, error) {
var created, exited, kernel, user syscall.Filetime
r, _, e := procGetProcessTimes.Call(
uintptr(h),
uintptr(unsafe.Pointer(&created)),
uintptr(unsafe.Pointer(&exited)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r == 0 {
return 0, fmt.Errorf("GetProcessTimes failed: %w", e)
}
return uint64(created.HighDateTime)<<32 | uint64(created.LowDateTime), nil
}
func processName(h syscall.Handle) (string, error) {
buf := make([]uint16, 32768)
size := uint32(len(buf))
r, _, e := procQueryImageName.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)))
if r == 0 {
return "", fmt.Errorf("QueryFullProcessImageName failed: %w", e)
}
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
}
func processAlive(h syscall.Handle) (bool, error) {
var created, exited, kernel, user syscall.Filetime
r, _, e := procGetProcessTimes.Call(
uintptr(h),
uintptr(unsafe.Pointer(&created)),
uintptr(unsafe.Pointer(&exited)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r == 0 {
return false, fmt.Errorf("GetProcessTimes failed: %w", e)
}
return exited.HighDateTime == 0 && exited.LowDateTime == 0, nil
}
func parseMemSize(s string) (uint64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty value")
}
multiplier := uint64(1)
switch s[len(s)-1] {
case 'k', 'K':
multiplier = 1024
s = s[:len(s)-1]
case 'm', 'M':
multiplier = 1024 * 1024
s = s[:len(s)-1]
case 'g', 'G':
multiplier = 1024 * 1024 * 1024
s = s[:len(s)-1]
}
n, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
if n > math.MaxUint64/multiplier {
return 0, fmt.Errorf("value overflows uint64")
}
return n * multiplier, 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 isAboveNormal(c uint32) bool {
return c == PriorityClassAboveNormal ||
c == PriorityClassHigh ||
c == PriorityClassRealtime
}
func desiredPriority(rss, mem uint64, current uint32, gameMode, isGame bool) uint32 {
if gameMode {
if isGame {
return PriorityClassHigh
}
return PriorityClassIdle
}
if rss >= mem {
return PriorityClassHigh
}
if isAboveNormal(current) {
return PriorityClassNormal
}
return 0
}
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 allProcs() ([]procInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == ^uintptr(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 []procInfo
for {
name := syscall.UTF16ToString(pe.ExeFile[:])
if name != "" {
out = append(out, procInfo{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 singleInstance() (syscall.Handle, bool, error) {
name, err := syscall.UTF16PtrFromString(`Local\autoPriority`)
if err != nil {
return 0, false, err
}
r, _, e := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
if r == 0 {
return 0, false, fmt.Errorf("CreateMutex failed: %w", e)
}
return syscall.Handle(r), e == syscall.Errno(ErrorAlreadyExists), nil
}
func main() {
memStr := flag.String("mem", "512M", "memory threshold (e.g. 512M, 1G, 2048M)")
gameMemStr := flag.String("game-mem", "2G", "game memory threshold (e.g. 2G, 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()
if *interval < 10*time.Second {
*interval = 10 * time.Second
}
mem, err := parseMemSize(*memStr)
if err != nil || mem == 0 {
if err == nil {
err = fmt.Errorf("must be greater than zero")
}
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)
}
mutex, exists, err := singleInstance()
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: %v\n", err)
os.Exit(1)
}
if exists {
closeH(mutex)
fmt.Fprintln(os.Stderr, "autoPriority: already running")
return
}
defer closeH(mutex)
logPath := filepath.Join(os.TempDir(), "autopriority.log")
logFile, err := os.Create(logPath)
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: cannot create log: %v\n", err)
os.Exit(1)
}
defer logFile.Close()
logf := func(format string, a ...any) {
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
fmt.Fprintf(logFile, format+"\n", a...)
}
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
if !*dryRun {
pid := uint32(os.Getpid())
h, err := openProc(pid, ProcessSetInformation)
if err == nil {
err = setPrio(h, pid, PriorityClassIdle)
closeH(h)
}
if err != nil {
logf("warning: could not set own priority to IDLE: %v", err)
} else {
logf("own priority set to IDLE")
}
}
blocked := make(map[processKey]string)
unreadable := make(map[uint32]string)
gameProcs := make(map[processKey]trackedProc)
gameSaved := make(map[processKey]savedPrio)
gameMode := false
myPID := uint32(os.Getpid())
readProcs := func() ([]procInfo, error) {
procs, err := allProcs()
if err != nil {
return nil, err
}
live := make(map[uint32]string, len(procs))
list := make([]procInfo, 0, len(procs))
for i := range procs {
p := &procs[i]
live[p.PID] = p.Name
if p.PID == myPID || p.PID == 0 {
continue
}
h, setErr := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
p.CanSet = setErr == nil
openErr := error(nil)
if !p.CanSet {
h, openErr = openProc(p.PID, ProcessQueryLimitedInformation)
}
if openErr != nil {
if unreadable[p.PID] != p.Name {
unreadable[p.PID] = p.Name
logf("SKIP %s (PID %d): %v", p.Name, p.PID, openErr)
}
continue
}
name, readErr := processName(h)
if readErr == nil {
p.Name = name
}
created := uint64(0)
if readErr == nil {
created, readErr = processCreated(h)
}
var m processMemoryCounters
if readErr == nil {
m.CBM = uint32(unsafe.Sizeof(m))
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
if r == 0 {
readErr = fmt.Errorf("GetProcessMemoryInfo failed: %w", e)
}
}
var cur uintptr
if readErr == nil {
r, _, e := procGetPriority.Call(uintptr(h))
cur = r
if cur == 0 {
readErr = fmt.Errorf("GetPriorityClass failed: %w", e)
}
}
if readErr != nil {
closeH(h)
if unreadable[p.PID] != p.Name {
unreadable[p.PID] = p.Name
logf("SKIP %s (PID %d): %v", p.Name, p.PID, readErr)
}
continue
}
delete(unreadable, p.PID)
p.Key = processKey{PID: p.PID, Created: created}
p.RSS = uint64(m.WorkingSetSize)
p.Prio = uint32(cur)
p.Handle = h
if !p.CanSet {
if _, ok := blocked[p.Key]; !ok {
blocked[p.Key] = p.Name
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, setErr)
}
}
list = append(list, *p)
}
for pid, name := range unreadable {
if live[pid] != name {
delete(unreadable, pid)
}
}
return list, nil
}
restore := func(list []procInfo) {
current := make(map[processKey]*procInfo, len(list))
for i := range list {
current[list[i].Key] = &list[i]
}
for key, saved := range gameSaved {
alive, err := processAlive(saved.handle)
if err != nil {
logf("RESTORE %s (PID %d) status error: %v", saved.name, key.PID, err)
continue
}
if !alive {
closeH(saved.handle)
delete(gameSaved, key)
continue
}
cur, _, e := procGetPriority.Call(uintptr(saved.handle))
if cur == 0 {
logf("RESTORE %s (PID %d) priority error: %v", saved.name, key.PID, e)
continue
}
if uint32(cur) != saved.prio {
if err := setPrio(saved.handle, key.PID, saved.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", saved.name, key.PID, prioName(saved.prio), err)
continue
}
logf("RESTORE %s (PID %d) -> %s", saved.name, key.PID, prioName(saved.prio))
}
if p := current[key]; p != nil {
p.Prio = saved.prio
}
closeH(saved.handle)
delete(gameSaved, key)
}
}
defer func() {
for attempt := 0; attempt < 3 && len(gameSaved) > 0; attempt++ {
restore(nil)
if len(gameSaved) > 0 {
time.Sleep(50 * time.Millisecond)
}
}
if len(gameSaved) > 0 {
logf("warning: %d process priorities could not be restored", len(gameSaved))
for _, saved := range gameSaved {
closeH(saved.handle)
}
}
for _, game := range gameProcs {
closeH(game.handle)
}
logf("autoPriority stopped")
logFile.Sync()
}()
ticker := time.NewTicker(*interval)
defer ticker.Stop()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
scan := func() {
list, err := readProcs()
if err != nil {
logf("process scan error: %v", err)
return
}
defer func() {
for _, p := range list {
if !p.Keep {
closeH(p.Handle)
}
}
}()
current := make(map[processKey]string, len(list))
for _, p := range list {
current[p.Key] = p.Name
}
for key, name := range blocked {
if current[key] != name {
delete(blocked, key)
}
}
for key, game := range gameProcs {
alive, err := processAlive(game.handle)
if err != nil {
logf("GAME %s (PID %d) status error: %v", game.name, key.PID, err)
continue
}
if !alive {
closeH(game.handle)
delete(gameProcs, key)
}
}
for key, saved := range gameSaved {
alive, err := processAlive(saved.handle)
if err == nil && !alive {
closeH(saved.handle)
delete(gameSaved, key)
}
}
for i := range list {
p := &list[i]
if gameMem > 0 && p.RSS >= gameMem {
if _, ok := gameProcs[p.Key]; !ok {
gameProcs[p.Key] = trackedProc{name: p.Name, handle: p.Handle}
p.Keep = true
logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS))
}
}
}
hasGame := gameMem > 0 && len(gameProcs) > 0
enteringGame := hasGame && !gameMode
if enteringGame {
gameMode = true
}
if !hasGame && gameMode {
restore(list)
if len(gameSaved) == 0 {
gameMode = false
logf("GAME MODE OFF")
} else {
return
}
}
if hasGame {
sort.SliceStable(list, func(i, j int) bool {
_, iGame := gameProcs[list[i].Key]
_, jGame := gameProcs[list[j].Key]
return iGame && !jGame
})
}
for i := range list {
p := &list[i]
_, isBlocked := blocked[p.Key]
_, isGame := gameProcs[p.Key]
target := desiredPriority(p.RSS, mem, p.Prio, hasGame, isGame)
action := "GAME"
if !hasGame && target == PriorityClassHigh {
action = "PROMOTE"
} else if !hasGame {
action = "DEMOTE"
}
if target == 0 || target == p.Prio || isBlocked {
continue
}
if *dryRun {
logf("[DRY-RUN] %s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
continue
}
if err := setPrio(p.Handle, p.PID, target); err != nil {
blocked[p.Key] = p.Name
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, err)
continue
}
if hasGame && !isGame && target == PriorityClassIdle {
if _, saved := gameSaved[p.Key]; !saved {
gameSaved[p.Key] = savedPrio{name: p.Name, prio: p.Prio, handle: p.Handle}
p.Keep = true
}
}
logf("%s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
p.Prio = target
}
if enteringGame {
logf("GAME MODE ON")
}
}
scan()
for {
select {
case <-stop:
logf("received shutdown signal")
return
case <-ticker.C:
scan()
}
}
}
+63
View File
@@ -0,0 +1,63 @@
//go:build windows
package main
import (
"math"
"testing"
)
func TestParseMemSize(t *testing.T) {
tests := []struct {
input string
want uint64
bad bool
}{
{"512M", 512 * 1024 * 1024, false},
{" 1g ", 1024 * 1024 * 1024, false},
{"0", 0, false},
{"18446744073709551615", math.MaxUint64, false},
{"17179869183G", 17179869183 * 1024 * 1024 * 1024, false},
{"17179869184G", 0, true},
{"", 0, true},
{"G", 0, true},
{"1GB", 0, true},
{"1.5G", 0, true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := parseMemSize(tt.input)
if (err != nil) != tt.bad || got != tt.want {
t.Fatalf("parseMemSize(%q) = %d, %v; want %d, bad=%v", tt.input, got, err, tt.want, tt.bad)
}
})
}
}
func TestDesiredPriority(t *testing.T) {
const mem = 512 * 1024 * 1024
tests := []struct {
name string
rss uint64
current uint32
gameMode, isGame bool
want uint32
}{
{"promote heavy", mem, PriorityClassNormal, false, false, PriorityClassHigh},
{"keep heavy high", mem, PriorityClassHigh, false, false, PriorityClassHigh},
{"demote light high", mem - 1, PriorityClassHigh, false, false, PriorityClassNormal},
{"demote light realtime", mem - 1, PriorityClassRealtime, false, false, PriorityClassNormal},
{"keep light normal", mem - 1, PriorityClassNormal, false, false, 0},
{"boost game", 1, PriorityClassIdle, true, true, PriorityClassHigh},
{"idle non-game", mem * 10, PriorityClassHigh, true, false, PriorityClassIdle},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := desiredPriority(tt.rss, mem, tt.current, tt.gameMode, tt.isGame); got != tt.want {
t.Fatalf("desiredPriority() = %#x, want %#x", got, tt.want)
}
})
}
}
-55
View File
@@ -1,55 +0,0 @@
package main
/*
#include <windows.h>
BOOL SetPriority(DWORD pid, int priority) {
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
if (!h) return FALSE;
BOOL success = SetPriorityClass(h, priority);
CloseHandle(h);
return success;
}
*/
import "C"
import (
"github.com/postfinance/single"
"github.com/shirou/gopsutil/process"
"os"
"runtime"
"syscall"
"time"
"unsafe"
)
func MessageBox(lpText string, lpCaption string, uType int) int {
var convertedCaption, _ = syscall.UTF16PtrFromString(lpCaption)
var convertedTitle, _ = syscall.UTF16PtrFromString(lpText)
returnCode, _, _ := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
uintptr(0),
uintptr(unsafe.Pointer(convertedCaption)),
uintptr(unsafe.Pointer(convertedTitle)),
uintptr(uType),
)
return int(returnCode)
}
var lockFile, _ = single.New("autoPriority")
func main() {
if err := lockFile.Lock(); err != nil {
MessageBox("The program is already running", "Only one instance of the program can be run\nYou can close it via the Task Manager", 48)
os.Exit(1)
}
C.SetPriority(C.DWORD(os.Getpid()), 0x00000040)
for range time.Tick(time.Minute) {
processes, _ := process.Processes()
for _, proc := range processes {
memoryInfo, err := proc.MemoryInfo()
if err == nil && memoryInfo.RSS >= (0.5*1024*1024*1024) {
C.SetPriority(C.DWORD(proc.Pid), 0x00000080)
}
}
runtime.GC()
}
}