1

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)
This commit is contained in:
2026-06-23 11:16:51 +03:00
parent da13a6d549
commit 0e8d7333c1
2 changed files with 163 additions and 593 deletions
-361
View File
@@ -1,361 +0,0 @@
# Техническое задание: autoPriority
## Назначение
Программа для Windows, которая автоматически управляет приоритетами процессов на основе их потребления оперативной памяти. Работает в фоне, периодически сканируя все процессы и корректируя их приоритеты CPU.
## Параметры командной строки
| Параметр | По умолчанию | Описание |
|---|---|---|
| `-mem` | `512M` | Порог памяти для повышения приоритета. Процессы с RSS ≥ этого значения повышаются до HIGH |
| `-game-mem` | `4G` | Порог памяти для активации игрового режима. Процессы с RSS ≥ этого значения считаются играми. `0` — игровой режим отключён. Должен быть больше `-mem` |
| `-interval` | `1m` | Интервал сканирования. Минимум 10 секунд |
| `-dry-run` | `false` | Только логирование, без изменения приоритетов |
Формат памяти: число с суффиксом `K`/`M`/`G` (например `512M`, `4G`, `2048M`). Без суффикра — байты.
## Структуры данных
Программа поддерживает четыре основные карты (map), хранящиеся в памяти между сканами:
### `promoted: map[pid → name]`
Процессы, повышенные до HIGH в нормальном режиме. Если RSS процесса падает ниже порога `-mem`, он понижается до NORMAL и удаляется из карты. При выходе из программы все процессы из карты восстанавливаются до NORMAL.
### `blocked: map[pid → name]`
Процессы, для которых не удалось открыть или изменить приоритет (системные процессы, античит). Полностью исключаются из попыток смены приоритета, но их RSS **всё равно измеряется** каждый скан (для определения игрового режима). Удаляются из карты, когда PID больше не существует в списке процессов.
### `gameProcs: map[pid → name]`
Процессы, превысившие `-game-mem` порог. Определяют, активен ли игровой режим. Удаляются, когда процесс умирает (PID исчезает из списка или имя не совпадает).
### `gameSaved: map[pid → {name, originalPriority}]`
Оригинальные приоритеты процессов, пониженных до IDLE в игровом режиме. Используются для восстановления при выключении игрового режима. Заполняется только при успешном понижении приоритета. Каждый PID сохраняется один раз (при первом понижении), чтобы запомнить приоритет, который был **до** игрового режима.
## Запуск программы
1. Разбор флагов командной строки
2. Проверка: если `game-mem > 0` и `game-mem <= mem` — выход с ошибкой
3. Если `interval < 10s` — принудительно установить 10s
4. **Уничтожение других экземпляров:** получить список всех процессов, найти процессы с тем же именем файла, что и у текущего процесса, но с другим PID, и завершить их через `TerminateProcess`
5. Подождать 100мс (ОС освобождает файловые дескрипторы)
6. Удалить старый лог-файл, создать новый (в `%TEMP%\autopriority.log`)
7. Установить собственный приоритет в IDLE
8. Записать в лог параметры запуска
9. Инициализировать пустые карты: `promoted`, `blocked`, `gameProcs`, `gameSaved`
10. Зарегистрировать deferred-функцию восстановления (см. раздел «Завершение»)
11. Выполнить первый скан немедленно
12. Войти в цикл: ждать сигнала завершения ИЛИ тикера интервала, при тике — выполнять скан
## Алгоритм сканирования (двухпроходный)
Каждый скан состоит из двух проходов. **Первый проход** собирает данные о памяти и определяет игровые процессы. **Второй проход** применяет приоритеты, уже зная, активен ли игровой режим. Это необходимо, чтобы игровой режим срабатывал на первом же скане, а не на следующем.
### Предварительный этап: очистка карт
Перед проходами программа получает список всех процессов и строит `pidMap` (pid → name). Затем очищает четыре карты:
- **`promoted`**: если PID исчез или имя не совпадает — удалить (процесс умер или PID перешёл другому процессу)
- **`blocked`**: если PID исчез — удалить (процесс умер, PID может быть переиспользован)
- **`gameProcs`**: если PID исчез или имя не совпадает — удалить
- **`gameSaved`**: если PID исчез или имя не совпадается — удалить
Проверка имени защищает от PID reuse: если процесс умер и его PID получил другой процесс, старая запись удаляется.
### Проход 1: Сбор RSS и определение игровых процессов
Для каждого процесса (кроме себя и PID 0):
1. Открыть процесс с правом `PROCESS_QUERY_LIMITED_INFORMATION` (0x1000). Это узкое право, которое дают почти все процессы, включая защищённые. Использование `PROCESS_QUERY_INFORMATION` (0x0400) приводит к Access Denied для многих пользовательских процессов.
2. Если `OpenProcess` упал:
- Добавить PID в `blocked` (если ещё не там)
- Записать в лог `BLOCK ... (added to exclusion list)`
- **Перейти к следующему процессу** (не добавлять в `procList`)
- Примечание: blocked процессы не попадают в `procList`, но если они были добавлены в `blocked` на предыдущем скане и всё ещё живы, они НЕ измеряются повторно. Это допустимо, потому что blocked процессы не могут менять приоритет.
**Важное исключение:** если процесс УЖЕ в `blocked` (добавлен ранее), то не логировать повторно — просто пропустить.
3. Вызвать `GetProcessMemoryInfo` для получения `WorkingSetSize` (RSS)
4. Если упало — пропустить процесс
5. Закрыть handle
6. Записать `{PID, Name, RSS}` в `procList`
7. Если `game-mem > 0` и `RSS >= game-mem`:
- Добавить PID в `gameProcs` (если ещё не там)
После прохода 1 вычислить:
```
hasGame = (game-mem > 0) AND (len(gameProcs) > 0)
```
### Проход 2: Применение приоритетов
Для каждого процесса из `procList` (в порядке обхода):
Определить `isBlocked`: процесс находится в `blocked`.
#### Вариант A: Процесс — игровой (есть в `gameProcs`)
Если `hasGame` (игровой режим активен):
- Если `isBlocked`: записать в лог `GAME ... — tracked as game (blocked)`, перейти к следующему
- Открыть с `PROCESS_QUERY_LIMITED_INFORMATION`
- Получить текущий приоритет
- Если уже HIGH — пропустить
- Если `dry-run`: записать `would set HIGH (game)`, пропустить
- Иначе: попытаться установить HIGH через `trySetPrio` (см. ниже)
- Успех → `GAME ... -> HIGH`
- Провал → `GAME ... — tracked as game (priority change failed)`
Перейти к следующему процессу.
#### Вариант B: Игровой режим активен, процесс НЕ игровой
Если `isBlocked`: пропустить (не пытаться понизить)
Открыть с `PROCESS_QUERY_LIMITED_INFORMATION`
Получить текущий приоритет
Если уже IDLE — пропустить
Если `dry-run`: записать `would set IDLE (game)`, пропустить
Иначе: попытаться установить IDLE через `trySetPrio`
- Успех → сохранить оригинальный приоритет в `gameSaved` (если ещё не сохранён), удалить из `promoted` (если там был), записать `GAME ... -> IDLE`
- Провал → процесс добавлен в `blocked`, больше не пытаться
Перейти к следующему процессу.
#### Вариант C: Нормальный режим (игровой режим не активен)
Если `isBlocked`: пропустить
Открыть с `PROCESS_QUERY_LIMITED_INFORMATION`
**Если RSS < `-mem`:**
- Получить текущий приоритет
- Если приоритет ABOVE_NORMAL / HIGH / REALTIME → понизить до NORMAL (DEMOTE), удалить из `promoted` (если там был)
- Иначе пропустить
**Если RSS >= `-mem`:**
- Получить текущий приоритет
- Если уже HIGH — пропустить (процесс уже повышен, возможно из `promoted`)
- Если `dry-run`: записать `would set HIGH`, пропустить
- Иначе: попытаться установить HIGH через `trySetPrio`
- Успех → добавить в `promoted`, записать `PROMOTE ... -> HIGH`
- Провал → процесс добавлен в `blocked`
### Финал скана: управление игровым режимом
После прохода 2, если `game-mem > 0`:
Вычислить `newHasGame = len(gameProcs) > 0`
**Если `newHasGame == true` и `gameMode == false`:**
- `gameMode = true`
- Записать `GAME MODE ON`
**Если `newHasGame == false` и `gameMode == true`:**
- `gameMode = false`
- Записать `GAME MODE OFF`
- **Восстановить приоритеты:** для каждого PID в `gameSaved`:
- Проверить, что PID всё ещё существует и имя совпадает (защита от PID reuse)
- Если `dry-run`: записать `[DRY-RUN] RESTORE ... -> originalPriority`
- Иначе: установить оригинальный приоритет через `setPrio` (отдельный `OpenProcess` с `PROCESS_SET_INFORMATION`)
- Успех → `RESTORE ... -> originalPriority`
- Провал → `RESTORE ... error`
- Удалить запись из `gameSaved`
- Очистить `gameSaved` (все записи обработаны)
## Функция trySetPrio
```
trySetPrio(queryHandle, pid, name, targetClass) → bool
```
1. Закрыть `queryHandle` (handle от `PROCESS_QUERY_LIMITED_INFORMATION`)
2. Открыть процесс заново с правом `PROCESS_SET_INFORMATION` (0x0200)
- Разделение прав: многие процессы дают `QUERY` но не дают `SET` в комбинации
3. Если `OpenProcess` упал:
- Добавить PID в `blocked`
- Записать `BLOCK ... open SetInformation failed`
- Вернуть `false`
4. Вызвать `SetPriorityClass(handle, targetClass)`
5. Закрыть handle (через defer)
6. Если `SetPriorityClass` упал:
- Добавить PID в `blocked`
- Записать `BLOCK ... SetPriorityClass failed`
- Вернуть `false`
7. Подождать 50мс (плавная смена приоритетов, предотвращает статтеры)
8. Вернуть `true`
## Функция setPrio (для восстановления)
```
setPrio(pid, targetClass) → error
```
1. Открыть с `PROCESS_SET_INFORMATION`
2. Вызвать `SetPriorityClass`
3. Закрыть handle
4. Вернуть ошибку или nil
## Завершение программы (deferred)
При получении сигнала SIGINT/SIGTERM:
1. Получить свежий список всех процессов
2. Построить `restoreMap` (pid → name)
3. **Восстановить `gameSaved`:** для каждого PID — проверить имя, установить оригинальный приоритет, записать `RESTORE ... -> originalPriority`
4. **Восстановить `promoted`:** для каждого PID — проверить имя, установить NORMAL, записать `RESTORE ... -> NORMAL`
5. **Восстановить `gameProcs`:** для каждого PID — проверить имя, установить NORMAL, записать `RESTORE ... -> NORMAL`
6. Записать `autoPriority stopped`
Порядок важен: `gameSaved` восстанавливается первым, потому что `promoted` мог быть удалён из `promoted` во время игрового режима, но его оригинальный приоритет сохранён в `gameSaved`.
## Классы приоритетов Windows
| Константа | Значение | Имя в логе |
|---|---|---|
| IDLE | 0x0040 | IDLE |
| BELOW_NORMAL | 0x4000 | BELOW_NORMAL |
| NORMAL | 0x0020 | NORMAL |
| ABOVE_NORMAL | 0x8000 | ABOVE_NORMAL |
| HIGH | 0x0080 | HIGH |
| REALTIME | 0x0100 | REALTIME |
### Логика isAboveNormal
Возвращает `true` для `ABOVE_NORMAL`, `HIGH`, `REALTIME`. НЕ возвращает `true` для `BELOW_NORMAL`, потому что числовое значение `BELOW_NORMAL` (0x4000) больше `NORMAL` (0x0020), хотя семантически ниже. Поэтому используется явная проверка, а не числовое сравнение.
## Логирование
Лог пишется в `%TEMP%\autopriority.log`. При запуске старый лог удаляется, создаётся новый. Каждая запись синхронизируется на диск (`Sync()`). Формат: `[DD.MM.YYYY HH:MM:SS] сообщение`.
### Префиксы логов
| Префикс | Значение |
|---|---|
| `PROMOTE` | Процесс повышен до HIGH (нормальный режим) |
| `DEMOTE` | Процесс понижен до NORMAL (нормальный режим) |
| `GAME` | Процесс изменён вgame режиме (→ HIGH или → IDLE) |
| `GAME MODE ON`| Игровой режим активирован |
| `GAME MODE OFF`| Игровой режим деактивирован, процессы восстановлены |
| `BLOCK` | OpenProcess или SetPriorityClass упал; процесс в исключениях |
| `RESTORE` | Процесс восстановлен в оригинальный приоритет |
| `[DRY-RUN]` | Было бы изменение приоритета (dry-run режим) |
## Windows API
Программа использует только `kernel32.dll` и `psapi.dll` через raw syscall. Без внешних зависимостей.
### Используемые функции
- `CreateToolhelp32Snapshot` — снимок списка процессов
- `Process32FirstW` / `Process32NextW` — обход снимка
- `OpenProcess` — открытие handle процесса
- `GetProcessMemoryInfo` — получение WorkingSetSize (RSS)
- `GetPriorityClass` — чтение текущего приоритета
- `SetPriorityClass` — установка приоритета
- `CloseHandle` — закрытие handle
- `TerminateProcess` — завершение других экземпляров
### Права доступа
- `PROCESS_QUERY_LIMITED_INFORMATION` (0x1000) — для чтения памяти и приоритета. Это право дают почти все процессы.
- `PROCESS_SET_INFORMATION` (0x0200) — для изменения приоритета. Запрашивается отдельно, только когда нужно менять приоритет.
- `PROCESS_TERMINATE` (0x0001) — для уничтожения других экземпляров.
Разделение прав критично: запрос `QUERY|SET` комбинированно приводит к Access Denied для многих пользовательских процессов (KeePassXC, HD-Player), хотя каждое право по отдельности они дают.
## Структура processEntry32
```
Size uint32
CntUsage uint32
PID uint32
DefaultHeapID uintptr // НЕ uint32 — intptr на x64
ModuleID uint32
CntThreads uint32
ParentPID uint32
PrioClass int32
Flags uint32
ExeFile [260]uint16 // UTF-16
```
`DefaultHeapID` должен быть `uintptr`, не `uint32` — иначе на x64 структура неправильно выровнена и `Process32Next` падает.
## Сценарии работы
### Сценарий 1: Обычная работа
1. Запуск программы
2. Chrome потребляет 600MB (≥ 512MB) → `PROMOTE Chrome.exe -> HIGH`, добавляется в `promoted`
3. Chrome сбрасывает память до 300MB (< 512MB) → `DEMOTE Chrome.exe -> NORMAL`, удаляется из `promoted`
4. Discord потребляет 700MB → `PROMOTE Discord.exe -> HIGH`
5. Discord сбрасывает до 200MB → `DEMOTE Discord.exe -> NORMAL`
### Сценарий 2: Понижение высокого приоритета
1. `wininit.exe` запущен с HIGH, потребляет 6MB (< 512MB порога)
2. Программа видит: RSS < mem, приоритет ABOVE_NORMAL → `DEMOTE wininit.exe -> NORMAL`
3. Это применяется ко всем процессам (включая ранее повышенные, если их RSS упал ниже порога)
### Сценарий 3: Игровой режим
1. Запуск RustClient, потребление растёт с 1GB до 8GB
2. На скане, где RSS ≥ 4GB: RustClient добавляется в `gameProcs`
3. `hasGame = true`
4. RustClient → `trySetPrio` HIGH → если античит блокирует → `BLOCK RustClient.exe` + `GAME RustClient.exe — tracked as game (priority change failed)`
5. RustClient остаётся в `gameProcs` (важно: блокировка SetPriorityClass не удаляет из gameProcs)
6. Все остальные процессы → IDLE (`GAME Chrome.exe -> IDLE`, etc.)
7. `GAME MODE ON`
8. При следующем скане RustClient всё ещё в `gameProcs`, остальные процессы уже IDLE — пропускаются
### Сценарий 4: Выход из игрового режима
1. RustClient закрыт
2. `gameProcs` очищен (PID исчез)
3. `newHasGame = false`, `gameMode = true` → переход
4. `gameMode = false`, `GAME MODE OFF`
5. Для каждого процесса в `gameSaved`: восстановить оригинальный приоритет
- `RESTORE Chrome.exe -> NORMAL` (был NORMAL до игрового режима)
- `RESTORE Discord.exe -> HIGH` (был HIGH, потому что был в `promoted`)
6. `gameSaved` очищен
7. Нормальный режим возобновляется: процессы с RSS ≥ mem повышаются, с RSS < mem и выше NORMAL — понижаются
### Сценарий 5: Заблокированный процесс становится игрой
1. RustClient стартует, потребляет 3.9GB
2. RSS ≥ 512MB (mem), не в `promoted` → попытка PROMOTE → античит блокирует → `BLOCK RustClient.exe` (добавлен в `blocked`)
3. RustClient дорастает до 8GB
4. На следующем скане: RustClient в `blocked`, но **проход 1 всё равно пытается его открыть**
5. `OpenProcess(QUERY_LIMITED)` для blocked процесса — если упадёт, пропустит. Если пройдёт — RSS измеряется
6. Если RustClient в `blocked` из-за `SetPriorityClass` (не `OpenProcess`), то `OpenProcess(QUERY_LIMITED)` пройдёт, RSS измерится, и он попадёт в `gameProcs`
7. `hasGame = true`, `GAME MODE ON`
8. В проходе 2: RustClient в `gameProcs`, но `isBlocked = true``GAME RustClient.exe — tracked as game (blocked)`, приоритет не меняется
9. Остальные процессы понижаются до IDLE
### Сценарий 6: PID reuse
1. Процесс A (PID 1234, "chrome.exe") повышен, в `promoted`
2. Процесс A умирает
3. Новый процесс B (PID 1234, "notepad.exe") запускается
4. На следующем скане: `pidMap[1234] = "notepad.exe"`, но `promoted[1234] = "chrome.exe"`
5. Имя не совпадает → `delete(promoted, 1234)`
6. Процесс B обрабатывается как новый процесс
### Сценарий 7: Системные процессы
1. `svchost.exe` (PID 932) — `OpenProcess(QUERY_LIMITED)` возвращает Access Denied
2. Добавляется в `blocked`, логируется `BLOCK`
3. На следующем скане: PID 932 всё ещё в `blocked`, `OpenProcess` снова падает, но лог НЕ пишется (процесс уже в `blocked`)
4. Когда процесс умирает: PID исчезает из `pidMap` → удаляется из `blocked`
## Сборка
```
go build -ldflags="-H windowsgui -s -w" -o autopriority.exe .
```
- `-H windowsgui` — без консольного окна
- `-s -w` — strip debug info, уменьшение размера
Требуется Go 1.26+. Только Windows (`//go:build windows`).
## Автозапуск
Поместить ярлык на `autopriority.exe` в папку `shell:startup` (Win+R → `shell:startup`).
+163 -232
View File
@@ -19,7 +19,6 @@ const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessTerminate = 0x00000001
ProcessSetInformation = 0x00000200
ProcessQueryInformation = 0x00000400
ProcessQueryLimitedInformation = 0x00001000
PriorityClassIdle = 0x00000040
PriorityClassNormal = 0x00000020
@@ -44,22 +43,28 @@ type processEntry32 struct {
}
type processMemoryCounters struct {
CBM uint32
PageFaultCount uint32
PeakWorkingSetSize uintptr
WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr
CBM uint32
PageFaultCount uint32
PeakWorkingSetSize uintptr
WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr
QuotaPeakNonPagedPoolUsage uintptr
QuotaNonPagedPoolUsage uintptr
PeakPagefileUsage uintptr
PagefileUsage uintptr
PrivateUsage uintptr
QuotaNonPagedPoolUsage uintptr
PeakPagefileUsage uintptr
PagefileUsage uintptr
PrivateUsage uintptr
}
type processInfo struct {
type procInfo struct {
PID uint32
Name string
RSS uint64
}
type savedPrio struct {
name string
prio uint32
}
var (
@@ -102,14 +107,6 @@ func setPrio(pid uint32, cls uint32) error {
return nil
}
func curPrioWithHandle(h syscall.Handle) (uint32, error) {
r, _, e := procGetPriority.Call(uintptr(h))
if r == 0 {
return 0, fmt.Errorf("GetPriorityClass failed: %w", e)
}
return uint32(r), nil
}
func utf16Trim(p []uint16) string {
for i, v := range p {
if v == 0 {
@@ -162,37 +159,6 @@ func formatMemSize(b uint64) string {
return fmt.Sprintf("%dKB", b/1024)
}
func allProcs() ([]processInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 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 []processInfo
for {
name := utf16Trim(pe.ExeFile[:])
if name != "" {
out = append(out, processInfo{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 isAboveNormal(c uint32) bool {
return c == PriorityClassAboveNormal ||
c == PriorityClassHigh ||
@@ -218,7 +184,36 @@ func prioName(c uint32) string {
}
}
func allProcs() ([]procInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 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 := utf16Trim(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 killOtherInstances() {
myPID := os.Getpid()
@@ -240,7 +235,7 @@ func killOtherInstances() {
func main() {
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.")
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()
@@ -271,8 +266,7 @@ func main() {
var logFile *os.File
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)
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err == nil {
logFile = f
defer logFile.Close()
@@ -292,9 +286,11 @@ func main() {
}
if gameMem > 0 {
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)", formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
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)
logf("autoPriority started (mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), *interval, *dryRun)
}
if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil {
@@ -303,53 +299,27 @@ func main() {
logf("own priority set to IDLE")
}
promoted := make(map[uint32]string)
blocked := make(map[uint32]string)
gameProcs := make(map[uint32]string)
gameSaved := make(map[uint32]savedPrio)
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 gameMode {
restoreProcs, _ := allProcs()
rm := make(map[uint32]string, len(restoreProcs))
for _, p := range restoreProcs {
rm[p.PID] = p.Name
}
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 {
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
}
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, sv := range gameSaved {
if cur, ok := rm[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))
}
}
}
logf("autoPriority stopped")
@@ -362,25 +332,6 @@ 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 {
@@ -393,31 +344,25 @@ func main() {
pidMap[p.PID] = p.Name
}
for pid, name := range promoted {
if cur, ok := pidMap[pid]; !ok || cur != name {
delete(promoted, pid)
}
}
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
for pid, sv := range gameSaved {
if cur, ok := pidMap[pid]; !ok || cur != sv.name {
delete(gameSaved, pid)
}
}
var procList []procRSS
for _, p := range procs {
var list []procInfo
for i := range procs {
p := &procs[i]
if p.PID == myPID || p.PID == 0 {
continue
}
@@ -434,67 +379,56 @@ func main() {
var m processMemoryCounters
m.CBM = uint32(unsafe.Sizeof(m))
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
closeH(h)
if r == 0 {
closeH(h)
if _, ok := blocked[p.PID]; !ok {
logf("GetProcessMemoryInfo(%d) error: %v", p.PID, e)
}
continue
}
closeH(h)
rssBytes := uint64(m.WorkingSetSize)
procList = append(procList, procRSS{p, rssBytes})
p.RSS = uint64(m.WorkingSetSize)
list = append(list, *p)
if gameMem > 0 && rssBytes >= gameMem {
if gameMem > 0 && p.RSS >= gameMem {
if _, ok := gameProcs[p.PID]; !ok {
gameProcs[p.PID] = p.Name
logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS))
}
}
}
hasGame := gameMem > 0 && len(gameProcs) > 0
for _, pr := range procList {
p := pr.info
rssBytes := pr.rss
isBlocked := false
if _, ok := blocked[p.PID]; ok {
isBlocked = true
}
for i := range list {
p := &list[i]
_, isBlocked := blocked[p.PID]
_, isGame := gameProcs[p.PID]
if _, ok := gameProcs[p.PID]; ok {
if hasGame {
if isBlocked {
logf("GAME %s (PID %d) RSS=%s — tracked as game (blocked)", p.Name, p.PID, formatMemSize(rssBytes))
continue
}
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 {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (game)",
p.Name, p.PID, formatMemSize(rssBytes))
} else {
if trySetPrio(h, p.PID, p.Name, PriorityClassHigh) {
logf("GAME %s (PID %d) RSS=%s -> HIGH",
p.Name, p.PID, formatMemSize(rssBytes))
} else {
logf("GAME %s (PID %d) RSS=%s — tracked as game (priority change failed)",
p.Name, p.PID, formatMemSize(rssBytes))
}
}
if hasGame && isGame {
if isBlocked {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (game)", p.Name, p.PID, formatMemSize(p.RSS))
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur != 0 && uint32(cur) == PriorityClassHigh {
continue
}
if err := setPrio(p.PID, PriorityClassHigh); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
logf("GAME %s (PID %d) RSS=%s — tracked as game (priority change failed)", p.Name, p.PID, formatMemSize(p.RSS))
} else {
logf("GAME %s (PID %d) RSS=%s -> HIGH", p.Name, p.PID, formatMemSize(p.RSS))
time.Sleep(100 * time.Millisecond)
}
continue
}
@@ -503,33 +437,55 @@ func main() {
if isBlocked {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set IDLE (game)", p.Name, p.PID, formatMemSize(p.RSS))
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur == 0 || uint32(cur) == PriorityClassIdle {
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)
if err := setPrio(p.PID, PriorityClassIdle); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} 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)
if _, saved := gameSaved[p.PID]; !saved {
gameSaved[p.PID] = savedPrio{name: p.Name, prio: uint32(cur)}
}
logf("GAME %s (PID %d) RSS=%s, %s -> IDLE", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
time.Sleep(100 * time.Millisecond)
}
continue
}
if p.RSS < mem {
if isBlocked {
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur == 0 || !isAboveNormal(uint32(cur)) {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set NORMAL", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
continue
}
if err := setPrio(p.PID, PriorityClassNormal); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} else {
logf("DEMOTE %s (PID %d) RSS=%s, %s -> NORMAL", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
time.Sleep(100 * time.Millisecond)
}
continue
}
@@ -537,54 +493,29 @@ func main() {
if isBlocked {
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) {
delete(promoted, p.PID)
logf("DEMOTE %s (PID %d) RSS=%s, %s -> NORMAL",
p.Name, p.PID, formatMemSize(rssBytes), prioName(origPrio))
}
}
} else {
closeH(h)
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur != 0 && uint32(cur) == PriorityClassHigh {
continue
}
origPrio, prioErr := curPrioWithHandle(h)
if prioErr != nil {
closeH(h)
continue
origStr := ""
if cur != 0 {
origStr = prioName(uint32(cur))
}
if origPrio == PriorityClassHigh {
closeH(h)
continue
}
origStr := prioName(origPrio)
if *dryRun {
closeH(h)
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH",
p.Name, p.PID, formatMemSize(rssBytes), origStr)
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr)
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)
if err := setPrio(p.PID, PriorityClassHigh); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} else {
logf("PROMOTE %s (PID %d) RSS=%s, %s -> HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr)
time.Sleep(100 * time.Millisecond)
}
}
@@ -598,8 +529,7 @@ func main() {
gameMode = false
logf("GAME MODE OFF")
for pid, sv := range gameSaved {
cur, ok := pidMap[pid]
if !ok || cur != sv.name {
if cur, ok := pidMap[pid]; !ok || cur != sv.name {
delete(gameSaved, pid)
continue
}
@@ -610,6 +540,7 @@ func main() {
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))
time.Sleep(100 * time.Millisecond)
}
}
delete(gameSaved, pid)