# Техническое задание: 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`).