refactor: single-file stealth build without tray, tables or tests

Keep the fixed scan and priority core in one main.go: PID+creation identity, query-only snapshots with write-only upgrades, single-attempt blocklists, external-change baselines, inherited-IDLE adoption and plain %TEMP% line logging. Drop the tray UI, icons, pretty tables, rotation and test scaffolding.
This commit is contained in:
lzrdblzzrd committed 2026-10-04 19:30:54 +03:00
1 parent 4880aa46dc
commit 4600cc0862
26 files changed
+944 -3128

No files matched your search

+44 -83
View File
@@ -1,39 +1,8 @@
# autoPriority # autoPriority
Windows-утилита для управления приоритетами процессов по расходу оперативной памяти. Работает из одного EXE, без установки и службы. Тихая Windows-утилита: назначает приоритеты процессам по расходу оперативной памяти. Один файл `main.go` собирается в один EXE без установки, службы, иконок и сторонних модулей. Программа не показывает окон и значков — только процесс в диспетчере задач и строки в журнале.
## Запуск и выход ## Запуск
Запустите `autoPriority.exe`. Первая проверка процессов выполняется при запуске, последующие — через заданный интервал, по умолчанию раз в минуту.
Значок в трее открывает меню с пунктом **«Выход»** по левой, правой, средней или дополнительной кнопке мыши. Меню доступно и с клавиатуры. При выходе программа останавливает проверки, восстанавливает доступные для восстановления приоритеты и убирает значок. Консольная сборка также поддерживает Ctrl+C.
В одном сеансе Windows работает один экземпляр. Повторный запуск завершается, не затрагивая работающий экземпляр и его журнал. Программа не настраивает автозапуск и не запрашивает повышение прав.
## Как назначаются приоритеты
Порог памяти применяется к рабочему набору процесса (RSS).
| Режим и условие | Приоритет |
|---|---|
| Обычный режим: RSS не меньше `-mem` | HIGH |
| Обычный режим: RSS меньше `-mem`, текущий приоритет выше NORMAL | NORMAL |
| Обычный режим: RSS меньше `-mem`, текущий приоритет NORMAL, BELOW_NORMAL или IDLE | Без изменения |
| Игровой режим: процесс достиг `-game-mem` | HIGH |
| Игровой режим: остальные процессы | IDLE |
**Игровой режим не распознаёт игры.** Его может включить любой процесс, достигший `-game-mem`, в том числе браузер. Такой процесс остаётся участником режима до завершения, даже если расход памяти уменьшился. Если участников несколько, режим заканчивается после выхода последнего.
После окончания игрового режима программа снимает временное понижение и применяет обычные правила. При штатном выходе самой autoPriority она восстанавливает собственные изменения, включая повышения вне игрового режима. Замеченное внешнее изменение приоритета становится новой исходной точкой.
## Параметры
| Флаг | По умолчанию | Описание |
|---|---|---|
| `-mem` | `512M` | Обычный порог памяти: положительное целое число с необязательным суффиксом K/M/G |
| `-game-mem` | `2G` | Порог игрового режима: больше `-mem`; `0` отключает режим |
| `-interval` | `1m` | Интервал проверок; значения меньше 10 секунд ограничиваются 10 секундами |
| `-dry-run` | `false` | Запись предполагаемых изменений в журнал без изменения приоритетов |
```text ```text
autoPriority.exe autoPriority.exe
@@ -42,70 +11,62 @@ autoPriority.exe -game-mem=0
autoPriority.exe -dry-run autoPriority.exe -dry-run
``` ```
| Флаг | По умолчанию | Описание |
|---|---|---|
| `-mem` | `512M` | Обычный порог памяти: положительное целое число с суффиксом K/M/G |
| `-game-mem` | `2G` | Порог тяжёлого режима: больше `-mem`; `0` отключает режим |
| `-interval` | `1m` | Интервал проверок; меньше 10 секунд ограничивается 10 секундами |
| `-dry-run` | `false` | Только запись плана в журнал без изменения приоритетов |
Первая проверка выполняется при запуске, дальше — по интервалу. В одном сеансе Windows работает один экземпляр; повторный запуск завершается, не трогая работающий. Программа не настраивает автозапуск и не запрашивает повышение прав.
Остановка — завершением процесса в диспетчере задач или сигналом. При штатном завершении по Ctrl+C / SIGTERM программа один раз восстанавливает собственные изменения. Принудительное завершение восстановления не гарантирует.
## Как назначаются приоритеты
Порог применяется к рабочему набору процесса (RSS).
| Условие | Приоритет |
|---|---|
| RSS не меньше `-mem` | HIGH |
| RSS меньше `-mem`, текущий приоритет выше NORMAL | NORMAL |
| RSS меньше `-mem`, приоритет NORMAL, BELOW_NORMAL или IDLE | Без изменения |
| Процесс достиг `-game-mem` | HIGH, остальным IDLE |
`-game-mem` — просто порог, не распознавание игр: режим может включить любой тяжёлый процесс. Участник остаётся в режиме до завершения, даже если расход памяти упал. После выхода последнего участника временное понижение снимается и применяются обычные правила. При выходе самой программы восстанавливаются её собственные изменения. Чужое изменение приоритета становится новой исходной точкой, а не перезаписывается.
После отказа процесс попадает в блоклист в памяти: повторных попыток нет, остальные обрабатываются независимо. Блоклист очищается при перезапуске. Запись снимается, если PID исчез или его занял другой экземпляр.
## Журнал ## Журнал
Файл журнала находится во временной папке Windows: **`%TEMP%\autopriority.log`**. Путь определяется через `os.TempDir`. Обычные строки во временной папке Windows: **`%TEMP%\autopriority.log`**. При каждом запуске файл очищается. Формат строк:
При каждом запуске журнал очищается, архив предыдущей сессии удаляется. В текущей сессии сохраняются основной файл и один архив `autopriority.log.1`, каждый размером до 4 МиБ. Слишком длинная запись сокращается с отметкой.
Изменения выводятся ASCII-таблицей:
```text ```text
Процесс | PID | Память | Повышено | Понижено [04.10.2026 18:14:53] PROMOTE Game.exe (PID 4321) RSS=5.8GB, NORMAL -> HIGH
[04.10.2026 18:14:53] GAME Browser.exe (PID 5678) RSS=740MB, HIGH -> IDLE
[04.10.2026 18:14:53] BLOCK svchost.exe (PID 1448): IDLE: OpenProcess(1448) failed: Access is denied. (повторов не будет)
[04.10.2026 18:14:53] SKIP System (PID 4): ... (повторов не будет)
``` ```
При включении игрового режима его участники идут первыми, фоновые процессы — ниже. Уже установленный HIGH отмечается как «уже HIGH». Если изменить приоритет участника не удалось, его строка содержит отметку «в блоклисте». Режим `-dry-run` помечает строки `[DRY-RUN]`.
Ошибки выводятся отдельным блоком «ОШИБКИ»:
```text
Процесс | PID | Описание ошибки
```
Под таблицей нет дублирующих сообщений. Режим `-dry-run` явно помечает записи как план. При ошибке записи журнала программа завершает работу с попыткой восстановления приоритетов.
## Блоклист
После ошибки чтения или изменения приоритета процесс попадает в блоклист. Повторных попыток для него нет, в том числе при выходе; остальные процессы обрабатываются независимо. Блоклист хранится в памяти и очищается при перезапуске autoPriority.
Если время создания доступно, блокировка привязана к PID и времени создания. Другой экземпляр с тем же PID обрабатывается отдельно. Если Windows запрещает даже чтение процесса, запись снимается при исчезновении PID из снимка либо смене имени или родителя. Замену недоступного процесса другим с тем же PID, именем и родителем между двумя снимками надёжно различить нельзя.
Процесс, доступный для чтения, но недоступный для изменения, может включать игровой режим по расходу памяти.
## Ограничения ## Ограничения
- Восстановление незаблокированного процесса выполняется один раз. Если Windows отказывает в доступе, программа сообщает о невосстановленных приоритетах. - Windows может запретить чтение или изменение приоритета — программа пишет строку в журнал и больше не трогает процесс.
- Принудительное завершение, сбой ОС или отключение питания не позволяют гарантировать восстановление. - Восстановление выполняется один раз; о невосстановленном сообщается в журнале.
- Между чтением и записью приоритета другая программа может изменить его значение. - Наследованный дочерними процессами IDLE определяется по родству и времени создания. Это эвристика.
- Наследованный дочерними процессами IDLE определяется по родству и времени создания. Это эвристика: она может затронуть явно заданный ребёнку IDLE и не обнаружить цепочку, если родитель завершился между проверками. - Программа не обходит защиту процессов и не гарантирует совместимость с античитом.
- Программа не обходит защиту процессов, не включает SeDebugPrivilege и не гарантирует совместимость с античитом или прирост FPS.
## Сборка и проверки ## Сборка
Для сборки нужны Windows и Go 1.26 или новее. Сторонние Go-модули и CGO не требуются. Нужны Windows и Go 1.26 или новее, без сторонних модулей и CGO. Скрытый вариант без окна:
```text ```text
go build -trimpath -buildvcs=false -ldflags="-H=windowsgui -s -w -buildid=" -o autoPriority.exe . go build -trimpath -buildvcs=false -ldflags="-H=windowsgui -s -w -buildid=" -o autoPriority.exe .
go test ./...
go vet ./...
``` ```
Для консольной сборки уберите `-H=windowsgui`. Консольный вариант для отладки: уберите `-H=windowsgui`. Проверка:
Обычные тесты используют синтетические процессы. Нативные проверки включаются переменными окружения: ```text
go vet ./...
- `AP_NATIVE_PROCESSES=1`: проверка Windows API только на специально созданных дочерних процессах. ```
- `AP_NATIVE_GUI=1`: проверка собственного значка и меню трея; может кратко переключить фокус.
Нативные тесты не запускают системный сканер программы. Запускайте их вне защищённой игры.
## Иконки
Программа использует `chart_bar_edit` для трея и `cross` для пункта «Выход». PNG встроены в EXE.
Silk 1.3, **Mark James**, Creative Commons Attribution 2.5.
- Автор: http://www.famfamfam.com/lab/icons/silk/
- Источник: https://github.com/legacy-icons/famfamfam-silk
- Ревизия: `d6056f5c28e46c28403888ab7c013aced3928ec8`.
- Лицензия и контрольные суммы: `assets/SILK-LICENSE.md`, `assets/SILK-SOURCE.json`.
-84
View File
@@ -1,84 +0,0 @@
//go:build windows
package main
import (
"context"
"errors"
"flag"
"io"
"syscall"
"testing"
"time"
)
func TestOptionsValidation(t *testing.T) {
for _, args := range [][]string{{"-mem=0"}, {"-mem=1.5G"}, {"-game-mem=128M"}, {"-interval=bad"}, {"extra"}, {"-unknown"}} {
if _, err := readOptions(args, io.Discard); err == nil {
t.Fatalf("accepted %v", args)
}
}
o, err := readOptions([]string{"-interval=1ms", "-game-mem=0"}, io.Discard)
if err != nil || o.mem != 512<<20 || o.gameMem != 0 || o.interval != 10*time.Second {
t.Fatal(o, err)
}
if _, err = readOptions([]string{"-h"}, io.Discard); !errors.Is(err, flag.ErrHelp) {
t.Fatal(err)
}
}
func TestRunControllerCancellationRestores(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassBelowNormal)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
calls := 0
err := runController(ctx, options{mem: 100, gameMem: 200, interval: time.Minute}, f, func(scanReport) error {
calls++
if calls == 1 {
assertPriority(t, p, PriorityClassHigh)
cancel()
}
return nil
})
if err != nil {
t.Fatal(err)
}
assertPriority(t, p, PriorityClassBelowNormal)
assertClosed(t, f)
if calls != 2 {
t.Fatal("shutdown report missing")
}
}
func TestRunControllerLogFailureStillRestores(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
sentinel := errors.New("disk failure")
err := runController(context.Background(), options{mem: 100, interval: time.Minute}, f, func(scanReport) error { return sentinel })
if !errors.Is(err, sentinel) {
t.Fatal(err)
}
assertPriority(t, p, PriorityClassNormal)
assertClosed(t, f)
}
func TestCancelledBeforeStartupDoesNotScan(t *testing.T) {
f := newFake()
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := runController(ctx, options{mem: 100, interval: time.Minute}, f, func(scanReport) error { return nil }); err != nil {
t.Fatal(err)
}
if f.snapshots != 0 {
t.Fatal("cancelled startup still scanned processes")
}
}
func TestWaitResultContract(t *testing.T) {
for _, item := range []struct {
code uintptr
alive, bad bool
}{{0, false, false}, {0x102, true, false}, {0xffffffff, false, true}, {0x80, false, true}} {
alive, err := decodeWaitResult(item.code, syscall.Errno(6))
if alive != item.alive || (err != nil) != item.bad {
t.Fatalf("bad wait decoding for %#x", item.code)
}
}
}
-37
View File
@@ -1,37 +0,0 @@
Original readme (from author of the icons)
----------
Silk icon set 1.3
_________________________________________
Mark James
http://www.famfamfam.com/lab/icons/silk/
_________________________________________
This work is licensed under a
Creative Commons Attribution 2.5 License.
[ http://creativecommons.org/licenses/by/2.5/ ]
This means you may use it for any purpose,
and make any changes you like.
All I ask is that you include a link back
to this page in your credits.
Are you using this icon set? Send me an email
(including a link or picture if available) to
mjames@gmail.com
Any other questions about this icon set please
contact mjames@gmail.com
About the rest (all this repository but the icons)
----------
All the content of this repository (excepted the icon pack)
is licensed under the [MIT license](http://opensource.org/licenses/MIT).
Though, it is just composed a few trivial json files and a Readme.
-20
View File
@@ -1,20 +0,0 @@
{
"repository": "legacy-icons/famfamfam-silk",
"commit": "d6056f5c28e46c28403888ab7c013aced3928ec8",
"files": [
{
"name": "chart_bar_edit.png",
"path": "src/chart_bar_edit.png",
"bytes": 754,
"sha256": "f52dca6b882152f3a4f2459ea8a02a75a6d925bf18082cae31bb0ecc254fb9eb",
"identical_to_previous_asset": true
},
{
"name": "cross.png",
"path": "src/cross.png",
"bytes": 655,
"sha256": "d04ecfc93ff86c44f6fc39e35945e3d8a7648ba8fcd97a2635920df2e88893b3",
"identical_to_previous_asset": true
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

-420
View File
@@ -1,420 +0,0 @@
//go:build windows
package main
import (
"fmt"
"sort"
"time"
)
type processError struct {
Name string
PID uint32
Description string
}
type processSnapshot struct {
Processes []procInfo
// Complete PID membership, including processes whose metadata was inaccessible.
PIDs map[uint32]bool
Errors []processError
}
type processBackend interface {
Snapshot() (processSnapshot, error)
Alive(*procInfo) (bool, error)
Priority(*procInfo) (uint32, error)
SetPriority(*procInfo, uint32) error
Close(*procInfo)
}
type priorityChange struct {
Name string
PID uint32
RSS uint64
Before, After uint32
Heavy, Unchanged, Blocked bool
}
type scanReport struct {
Title string
Rows []priorityChange
Errors []processError
DryRun bool
Unrestored int
}
type processState struct {
info procInfo
original, last uint32
idleSince uint64
changed, game, overlay bool
gameBase uint32
}
type changeKind uint8
const (
normalChange changeKind = iota
gameChange
restoringChange
)
type controller struct {
api processBackend
mem, gameMem uint64
dryRun, gameMode bool
states map[processKey]*processState
now func() time.Time
blocked map[processKey]bool
out scanReport
}
func newController(api processBackend, mem, gameMem uint64, dryRun bool, _ time.Duration) *controller {
return &controller{api: api, mem: mem, gameMem: gameMem, dryRun: dryRun, states: make(map[processKey]*processState), blocked: make(map[processKey]bool), now: time.Now}
}
func (c *controller) ordered() []*processState {
list := make([]*processState, 0, len(c.states))
for _, s := range c.states {
list = append(list, s)
}
sort.Slice(list, func(i, j int) bool {
a, b := list[i], list[j]
if a.game != b.game {
return a.game
}
if a.info.PID != b.info.PID {
return a.info.PID < b.info.PID
}
return a.info.Key.Created < b.info.Key.Created
})
return list
}
func (c *controller) alive(s *processState, snapshot *processSnapshot) (bool, error) {
if snapshot != nil {
if !snapshot.PIDs[s.info.PID] {
return false, nil
}
for _, p := range snapshot.Processes {
if p.PID == s.info.PID && p.Key != s.info.Key {
return false, nil
}
}
}
if s.info.Waitable {
return c.api.Alive(&s.info)
}
// Without SYNCHRONIZE, only absence from a complete snapshot proves exit.
// Retained handles still bind all restoration writes to the original instance.
return true, nil
}
func (s *processState) observe(current uint32) {
if (s.changed || s.overlay) && current != s.last {
// Do not undo another actor's change. The next own write starts a new baseline.
s.changed = false
s.overlay = false
s.idleSince = 0
}
s.info.Prio = current
if !s.changed && !s.overlay {
s.original = current
}
}
func (c *controller) reconcile(snapshot processSnapshot) map[processKey]bool {
fresh := make(map[processKey]bool, len(snapshot.Processes))
instances := make(map[uint32]processKey, len(snapshot.Processes))
for _, p := range snapshot.Processes {
instances[p.PID] = p.Key
}
for key := range c.blocked {
current, known := instances[key.PID]
if !snapshot.PIDs[key.PID] || (known && current != key) {
delete(c.blocked, key)
}
}
for key, s := range c.states {
alive, err := c.alive(s, &snapshot)
if err != nil {
c.failed(s, s.info.Prio, fmt.Errorf("статус процесса: %w", err))
}
if err == nil && !alive {
c.api.Close(&s.info)
delete(c.states, key)
}
}
for i := range snapshot.Processes {
p := &snapshot.Processes[i]
fresh[p.Key] = true
if s := c.states[p.Key]; s != nil {
s.info.Name = p.Name
s.info.RSS = p.RSS
s.info.ParentPID = p.ParentPID
s.observe(p.Prio)
c.api.Close(p)
} else {
c.states[p.Key] = &processState{info: *p, original: p.Prio}
}
}
c.out.Errors = append(c.out.Errors, snapshot.Errors...)
return fresh
}
func (c *controller) failed(s *processState, target uint32, err error) {
if c.blocked[s.info.Key] {
return
}
c.blocked[s.info.Key] = true
c.out.Errors = append(c.out.Errors, processError{Name: s.info.Name, PID: s.info.PID, Description: fmt.Sprintf("%s: %v; внесён в блоклист", prioName(target), err)})
}
func (c *controller) change(s *processState, target uint32, kind changeKind) bool {
if c.blocked[s.info.Key] {
return false
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, target, err)
return false
}
s.observe(current)
if kind == normalChange && desiredPriority(s.info.RSS, c.mem, current, false, false) == 0 {
return true
}
if kind == restoringChange && !s.changed && !s.overlay {
return true
}
if current == target {
return true
}
row := priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: current, After: target, Heavy: s.game}
if c.dryRun {
c.out.Rows = append(c.out.Rows, row)
return true
}
stamp := filetimeValue(c.now())
if err = c.api.SetPriority(&s.info, target); err != nil {
c.failed(s, target, err)
return false
}
// Save ownership immediately after a successful write, even if verification fails.
if !s.changed {
s.original = current
}
if kind == gameChange && !s.overlay {
s.overlay = true
s.gameBase = current
}
s.changed = true
s.last = target
s.info.Prio = target
if target == PriorityClassIdle {
s.idleSince = stamp
} else {
s.idleSince = 0
}
actual, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, target, fmt.Errorf("изменение выполнено, проверка не удалась: %w", err))
return false
}
s.last = actual
s.info.Prio = actual
s.changed = actual != s.original
if actual != PriorityClassIdle {
s.idleSince = 0
}
if actual != current {
row.After = actual
c.out.Rows = append(c.out.Rows, row)
}
if actual != target {
c.failed(s, target, fmt.Errorf("Windows установила %s вместо %s", prioName(actual), prioName(target)))
return false
}
return true
}
func (c *controller) restoreOverlay(s *processState) bool {
if c.blocked[s.info.Key] {
return false
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, s.gameBase, err)
return false
}
s.observe(current)
if !s.overlay {
return true
}
if !c.change(s, s.gameBase, restoringChange) {
return false
}
s.overlay = false
return true
}
func filetimeValue(t time.Time) uint64 {
return uint64(t.Unix())*10000000 + uint64(t.Nanosecond()/100) + 116444736000000000
}
func inheritedClass(parent uint32) uint32 {
if parent == PriorityClassIdle || parent == PriorityClassBelowNormal {
return parent
}
return PriorityClassNormal
}
func (c *controller) adoptInherited(fresh map[processKey]bool) {
if c.dryRun {
return
}
byPID := make(map[uint32]*processState, len(c.states))
for _, s := range c.states {
byPID[s.info.PID] = s
}
// Each successful pass adopts at least one previously unowned child. Cycles,
// reused parent PIDs and pre-existing IDLE processes cannot establish ownership.
for pass := 0; pass < len(c.states); pass++ {
adopted := false
for _, s := range c.states {
if !fresh[s.info.Key] || s.changed || s.overlay || s.game || s.info.Prio != PriorityClassIdle {
continue
}
parent := byPID[s.info.ParentPID]
if parent == nil || parent == s || !fresh[parent.info.Key] || (!parent.changed && !parent.overlay) || parent.last != PriorityClassIdle || parent.info.Prio != PriorityClassIdle || parent.idleSince == 0 {
continue
}
if s.info.Key.Created < parent.idleSince || s.info.Key.Created <= parent.info.Key.Created {
continue
}
s.original = inheritedClass(parent.original)
s.last = PriorityClassIdle
s.changed = s.original != s.last
s.overlay = parent.overlay
s.gameBase = inheritedClass(parent.gameBase)
s.idleSince = s.info.Key.Created
// Explicit child IDLE cannot be distinguished from inherited IDLE by polling.
adopted = adopted || s.changed || s.overlay
}
if !adopted {
break
}
}
}
func (c *controller) releaseUnused() {
for key, s := range c.states {
if !s.game && !s.changed && !s.overlay {
c.api.Close(&s.info)
delete(c.states, key)
}
}
}
func (c *controller) Step() scanReport {
c.out = scanReport{DryRun: c.dryRun}
snapshot, err := c.api.Snapshot()
if err != nil {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Снимок процессов: %v", err)})
return c.out
}
fresh := c.reconcile(snapshot)
c.adoptInherited(fresh)
newGames := make(map[processKey]bool)
for key, s := range c.states {
if fresh[key] && c.gameMem > 0 && s.info.RSS >= c.gameMem && !s.game {
s.game = true
newGames[key] = true
}
}
hasGame := false
for _, s := range c.states {
hasGame = hasGame || s.game
}
if hasGame && !c.gameMode {
c.out.Title = "ИГРОВОЙ РЕЖИМ ВКЛЮЧЁН"
}
if !hasGame && c.gameMode {
c.out.Title = "ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ"
}
c.gameMode = hasGame
for _, s := range c.ordered() {
if !hasGame && s.overlay {
if !c.restoreOverlay(s) {
continue
}
if c.out.Title == "" {
c.out.Title = "ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ"
}
}
if !fresh[s.info.Key] {
continue
}
target := desiredPriority(s.info.RSS, c.mem, s.info.Prio, hasGame, s.game)
if target == 0 {
continue
}
if target == s.info.Prio {
if newGames[s.info.Key] {
c.out.Rows = append(c.out.Rows, priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: target, After: target, Heavy: true, Unchanged: true})
}
continue
}
kind := normalChange
if hasGame {
kind = gameChange
}
if !c.change(s, target, kind) && newGames[s.info.Key] {
c.out.Rows = append(c.out.Rows, priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: s.info.Prio, After: s.info.Prio, Heavy: true, Blocked: true})
}
}
c.releaseUnused()
if c.out.Title == "" && len(c.out.Rows) > 0 {
if hasGame {
c.out.Title = "ИГРОВОЙ РЕЖИМ"
} else {
c.out.Title = "ИЗМЕНЕНИЕ ПРИОРИТЕТОВ"
}
}
return c.out
}
func (c *controller) Shutdown() scanReport {
c.out = scanReport{Title: "ЗАВЕРШЕНИЕ: ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ", DryRun: c.dryRun}
if !c.dryRun && len(c.states) > 0 {
snapshot, err := c.api.Snapshot()
if err == nil {
fresh := c.reconcile(snapshot)
c.adoptInherited(fresh)
} else {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Последний снимок процессов: %v", err)})
}
for _, s := range c.ordered() {
if !s.changed || c.blocked[s.info.Key] {
continue
}
alive, err := c.alive(s, nil)
if err == nil && !alive {
s.changed = false
continue
}
if err != nil {
c.failed(s, s.original, err)
continue
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, s.original, err)
continue
}
s.observe(current)
if !s.changed {
continue
}
if c.change(s, s.original, restoringChange) {
s.changed = false
s.overlay = false
}
}
}
for key, s := range c.states {
if s.changed {
c.out.Unrestored++
}
c.api.Close(&s.info)
delete(c.states, key)
}
if c.out.Unrestored > 0 {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Не восстановлены приоритеты %d процессов; подробности в журнале", c.out.Unrestored)})
}
c.gameMode = false
clear(c.blocked)
return c.out
}
-288
View File
@@ -1,288 +0,0 @@
//go:build windows
package main
import (
"testing"
"time"
)
func TestControllerNormalBoostRestoredAtExit(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassBelowNormal)
c := testController(f)
c.Step()
assertPriority(t, p, PriorityClassHigh)
c.Shutdown()
assertPriority(t, p, PriorityClassBelowNormal)
assertClosed(t, f)
}
func TestControllerHeavyRemainsTrackedBelowThresholdAndDuringReadFailure(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
c.Step()
g.info.RSS = 1
c.Step()
assertPriority(t, g, PriorityClassHigh)
g.readable = false
c.Step()
assertPriority(t, p, PriorityClassIdle)
g.alive = false
c.Step()
assertPriority(t, p, PriorityClassNormal)
c.Shutdown()
assertClosed(t, f)
}
func TestControllerProtectedHeavyStillEnablesMode(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
g.writable = false
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
r := c.Step()
assertPriority(t, g, PriorityClassNormal)
assertPriority(t, p, PriorityClassIdle)
if len(r.Errors) == 0 {
t.Fatal("missing denied-write diagnostic")
}
c.Shutdown()
assertClosed(t, f)
}
func TestControllerDenialPersistsForCurrentInstance(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
p.writable = false
c := testController(f)
c.Step()
p.writable = true
c.Step()
assertPriority(t, p, PriorityClassNormal)
c.now = func() time.Time { return time.Unix(1061, 0) }
c.Step()
assertPriority(t, p, PriorityClassNormal)
c.Shutdown()
assertPriority(t, p, PriorityClassNormal)
assertClosed(t, f)
}
func TestControllerPIDReuseNeverRestoresReplacement(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
old := f.add(20, 50, PriorityClassBelowNormal)
c := testController(f)
c.Step()
old.alive = false
replacement := f.child(20, 0, 116444736000000000+1001*10000000)
replacement.info.Prio = PriorityClassBelowNormal
g.alive = false
c.Step()
assertPriority(t, replacement, PriorityClassBelowNormal)
if replacement.writes != 0 {
t.Fatal("wrote to a replacement process")
}
c.Shutdown()
assertClosed(t, f)
}
func TestControllerExternalPriorityBecomesNewBaseline(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
c := testController(f)
c.Step()
p.info.Prio = PriorityClassAboveNormal
c.Shutdown()
assertPriority(t, p, PriorityClassAboveNormal)
assertClosed(t, f)
}
func TestControllerExternalChangeBeforeNextPolicyWriteIsPreservedOnExit(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
c := testController(f)
c.Step()
p.info.Prio = PriorityClassBelowNormal
c.Step()
assertPriority(t, p, PriorityClassHigh)
c.Shutdown()
assertPriority(t, p, PriorityClassBelowNormal)
assertClosed(t, f)
}
func TestControllerGameOverlayKeepsOriginalExitBaseline(t *testing.T) {
f := newFake()
p := f.add(20, 150, PriorityClassBelowNormal)
c := testController(f)
c.Step()
g := f.add(10, 250, PriorityClassNormal)
c.Step()
assertPriority(t, p, PriorityClassIdle)
g.alive = false
c.Step()
assertPriority(t, p, PriorityClassHigh)
c.Shutdown()
assertPriority(t, p, PriorityClassBelowNormal)
assertClosed(t, f)
}
func TestControllerFinalSnapshotCatchesNewChild(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
parent := f.add(20, 50, PriorityClassNormal)
c := testController(f)
c.Step()
child := f.child(30, 20, 116444736000000000+1001*10000000)
c.Shutdown()
assertPriority(t, g, PriorityClassNormal)
assertPriority(t, parent, PriorityClassNormal)
assertPriority(t, child, PriorityClassNormal)
assertClosed(t, f)
}
func TestControllerDryRunHasNoWritesOrRestores(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
p := f.add(20, 50, PriorityClassBelowNormal)
c := testController(f)
c.dryRun = true
r := c.Step()
if !r.DryRun || len(r.Rows) != 2 {
t.Fatalf("bad dry-run report: %+v", r)
}
c.Shutdown()
assertPriority(t, g, PriorityClassNormal)
assertPriority(t, p, PriorityClassBelowNormal)
if g.writes+p.writes != 0 {
t.Fatal("dry-run wrote priorities")
}
assertClosed(t, f)
}
func TestControllerShutdownWorksWithoutFinalSnapshot(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
c := testController(f)
c.Step()
f.snapshotErr = true
r := c.Shutdown()
if r.Unrestored != 0 {
t.Fatal(r)
}
assertPriority(t, p, PriorityClassNormal)
assertClosed(t, f)
}
func TestControllerUnrestoredFailuresReportedAndHandlesClosed(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
c := testController(f)
c.Step()
p.writable = false
r := c.Shutdown()
if r.Unrestored != 1 {
t.Fatalf("expected one unresolved restoration, got %+v", r)
}
assertClosed(t, f)
}
func TestControllerWindowsCoercionIsNotSuccessfulRestoration(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassRealtime)
c := testController(f)
c.Step()
p.coerced = PriorityClassHigh
r := c.Shutdown()
if r.Unrestored != 1 {
t.Fatal("coerced REALTIME restoration was falsely reported as successful")
}
assertClosed(t, f)
}
func TestControllerNoSynchronizeUsesCompletePIDMembership(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
c.Step()
c.states[g.info.Key].info.Waitable = false
g.readable = false
c.Step()
assertPriority(t, p, PriorityClassIdle)
g.alive = false
c.Step()
assertPriority(t, p, PriorityClassNormal)
c.Shutdown()
assertClosed(t, f)
}
func TestControllerSteadyStateDoesNotRepeatChanges(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassHigh)
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
r := c.Step()
if len(r.Rows) != 2 || !r.Rows[0].Unchanged {
t.Fatal("heavy trigger must be first, without a fictional raise")
}
writes := g.writes + p.writes
r = c.Step()
if len(r.Rows) != 0 || g.writes+p.writes != writes {
t.Fatal("repeated unchanged work")
}
c.Shutdown()
assertClosed(t, f)
}
func TestControllerExternalChangeToNetUnchangedOverlayIsRespected(t *testing.T) {
f := newFake()
p := f.add(20, 150, PriorityClassIdle)
c := testController(f)
c.Step()
g := f.add(10, 250, PriorityClassNormal)
p.info.RSS = 50
c.Step()
assertPriority(t, p, PriorityClassIdle)
p.info.Prio = PriorityClassBelowNormal
g.alive = false
c.Step()
assertPriority(t, p, PriorityClassBelowNormal)
c.Shutdown()
assertClosed(t, f)
}
func TestControllerRestorationRechecksOwnershipImmediatelyBeforeWrite(t *testing.T) {
for _, shutdown := range []bool{false, true} {
t.Run(map[bool]string{false: "game exit", true: "shutdown"}[shutdown], func(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
c.Step()
g.alive = false
reads := 0
f.onRead = func(v *fakeProcess) {
if v == p {
reads++
if reads == 2 {
v.info.Prio = PriorityClassBelowNormal
}
}
}
if shutdown {
c.Shutdown()
} else {
c.Step()
f.onRead = nil
assertPriority(t, p, PriorityClassBelowNormal)
c.Shutdown()
}
assertPriority(t, p, PriorityClassBelowNormal)
assertClosed(t, f)
})
}
}
func TestControllerReadFailureIsBlocked(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
p := f.add(20, 50, PriorityClassNormal)
c := testController(f)
c.Step()
g.alive = false
p.failRead = true
c.Step()
r := c.Step()
if len(r.Errors) != 0 {
t.Fatal("blocked restoration repeated its error", r.Errors)
}
p.failRead = false
c.Shutdown()
assertClosed(t, f)
}
-204
View File
@@ -1,204 +0,0 @@
//go:build windows
package main
import (
"errors"
"fmt"
"sort"
"syscall"
"testing"
"time"
)
// Only OS boundaries are simulated. The real production controller is exercised.
type fakeProcess struct {
info procInfo
alive, readable, writable bool
failSet, failRead bool
coerced uint32
writes int
}
type fakeBackend struct {
procs map[processKey]*fakeProcess
handles map[syscall.Handle]*fakeProcess
next syscall.Handle
snapshotErr bool
onRead func(*fakeProcess)
snapshots int
}
func newFake() *fakeBackend {
return &fakeBackend{procs: make(map[processKey]*fakeProcess), handles: make(map[syscall.Handle]*fakeProcess)}
}
func (f *fakeBackend) add(pid uint32, rss uint64, prio uint32) *fakeProcess {
p := &fakeProcess{info: procInfo{PID: pid, Key: processKey{pid, uint64(pid)}, Name: fmt.Sprintf("Process%d.exe", pid), RSS: rss, Prio: prio}, alive: true, readable: true, writable: true}
f.procs[p.info.Key] = p
return p
}
func (f *fakeBackend) Snapshot() (processSnapshot, error) {
f.snapshots++
if f.snapshotErr {
return processSnapshot{}, errors.New("snapshot unavailable")
}
out := processSnapshot{PIDs: make(map[uint32]bool)}
keys := make([]processKey, 0, len(f.procs))
for key := range f.procs {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool { return keys[i].PID < keys[j].PID })
for _, key := range keys {
p := f.procs[key]
if !p.alive {
continue
}
out.PIDs[key.PID] = true
if !p.readable {
continue
}
info := p.info
f.next++
info.Handle = f.next
info.Waitable = true
f.handles[info.Handle] = p
out.Processes = append(out.Processes, info)
}
return out, nil
}
func (f *fakeBackend) Alive(p *procInfo) (bool, error) {
v, ok := f.handles[p.Handle]
if !ok {
return false, errors.New("invalid handle")
}
return v.alive, nil
}
func (f *fakeBackend) Priority(p *procInfo) (uint32, error) {
v, ok := f.handles[p.Handle]
if !ok || !v.alive || v.failRead {
return 0, errors.New("priority unavailable")
}
if f.onRead != nil {
f.onRead(v)
}
return v.info.Prio, nil
}
func (f *fakeBackend) SetPriority(p *procInfo, target uint32) error {
v, ok := f.handles[p.Handle]
if !ok || !v.alive || !v.writable || v.failSet {
return errors.New("access denied")
}
v.writes++
v.info.Prio = target
if v.coerced != 0 {
v.info.Prio = v.coerced
}
return nil
}
func (f *fakeBackend) Close(p *procInfo) {
if p.Handle == 0 {
panic("double close")
}
if _, ok := f.handles[p.Handle]; !ok {
panic("invalid close")
}
delete(f.handles, p.Handle)
p.Handle = 0
}
func testController(f *fakeBackend) *controller {
c := newController(f, 100, 200, false, time.Minute)
c.now = func() time.Time { return time.Unix(1000, 0) }
return c
}
func assertPriority(t *testing.T, p *fakeProcess, want uint32) {
t.Helper()
if p.info.Prio != want {
t.Fatalf("%s: got %s; want %s", p.info.Name, prioName(p.info.Prio), prioName(want))
}
}
func (f *fakeBackend) child(pid, parent uint32, created uint64) *fakeProcess {
p := f.add(pid, 50, PriorityClassIdle)
delete(f.procs, p.info.Key)
p.info.Key.Created = created
p.info.ParentPID = parent
f.procs[p.info.Key] = p
return p
}
func TestControllerInheritedIdleDescendantsRecover(t *testing.T) {
f := newFake()
game := f.add(10, 250, PriorityClassNormal)
parent := f.add(20, 50, PriorityClassBelowNormal)
c := testController(f)
c.Step()
child := f.child(30, 20, 116444736000000000+1001*10000000)
grandchild := f.child(40, 30, 116444736000000000+1002*10000000)
oldIdle := f.child(50, 20, 116444736000000000+999*10000000)
unrelated := f.child(60, 0, 116444736000000000+1001*10000000)
c.now = func() time.Time { return time.Unix(1003, 0) }
c.Step()
game.alive = false
c.Step()
assertPriority(t, parent, PriorityClassBelowNormal)
assertPriority(t, child, PriorityClassBelowNormal)
assertPriority(t, grandchild, PriorityClassBelowNormal)
assertPriority(t, oldIdle, PriorityClassIdle)
assertPriority(t, unrelated, PriorityClassIdle)
c.Shutdown()
assertClosed(t, f)
}
func assertClosed(t *testing.T, f *fakeBackend) {
t.Helper()
if len(f.handles) != 0 {
t.Fatalf("leaked %d owned handles", len(f.handles))
}
}
func TestControllerShutdownRestoresHeavyAndBackground(t *testing.T) {
f := newFake()
game := f.add(10, 250, PriorityClassNormal)
background := f.add(20, 50, PriorityClassBelowNormal)
c := testController(f)
c.Step()
assertPriority(t, game, PriorityClassHigh)
assertPriority(t, background, PriorityClassIdle)
report := c.Shutdown()
if len(report.Errors) != 0 {
t.Fatal(report.Errors)
}
assertPriority(t, game, PriorityClassNormal)
assertPriority(t, background, PriorityClassBelowNormal)
assertClosed(t, f)
}
func TestControllerGameExitRestoresBackground(t *testing.T) {
f := newFake()
game := f.add(10, 250, PriorityClassNormal)
background := f.add(20, 50, PriorityClassBelowNormal)
c := testController(f)
c.Step()
game.alive = false
c.Step()
assertPriority(t, background, PriorityClassBelowNormal)
c.Shutdown()
assertClosed(t, f)
}
func TestControllerRestoreFailureDoesNotBlockOtherProcesses(t *testing.T) {
f := newFake()
game := f.add(10, 250, PriorityClassNormal)
blocked := f.add(20, 50, PriorityClassNormal)
healthy := f.add(30, 50, PriorityClassBelowNormal)
c := testController(f)
c.Step()
game.alive = false
blocked.failSet = true
newHeavy := f.add(40, 150, PriorityClassNormal)
c.Step()
assertPriority(t, healthy, PriorityClassBelowNormal)
assertPriority(t, newHeavy, PriorityClassHigh)
blocked.failSet = false
r := c.Shutdown()
if r.Unrestored != 1 {
t.Fatal("blocked restoration incorrectly reported successful", r)
}
assertPriority(t, blocked, PriorityClassIdle)
assertPriority(t, newHeavy, PriorityClassNormal)
assertClosed(t, f)
}
-52
View File
@@ -1,52 +0,0 @@
//go:build windows
package main
import (
"strings"
"testing"
"time"
)
func TestOrdinaryPolicyUsesFreshPriorityAndReleasesHandles(t *testing.T) {
t.Run("do not raise a light process after an external demotion", func(t *testing.T) {
f := newFake()
p := f.add(10, 50, PriorityClassHigh)
c := testController(f)
defer c.Shutdown()
f.onRead = func(v *fakeProcess) { v.info.Prio = PriorityClassBelowNormal; f.onRead = nil }
c.Step()
assertPriority(t, p, PriorityClassBelowNormal)
if p.writes != 0 {
t.Fatal("stale priority decision performed a write")
}
})
t.Run("release obsolete denied-write state", func(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
p.writable = false
c := testController(f)
defer c.Shutdown()
c.Step()
p.info.RSS = 50
c.Step()
assertClosed(t, f)
})
}
func TestDeniedHeavyTriggerIsStillLoggedBeforeBackground(t *testing.T) {
f := newFake()
g := f.add(10, 250, PriorityClassNormal)
g.info.Name = "BlockedGame.exe"
g.writable = false
b := f.add(20, 50, PriorityClassNormal)
b.info.Name = "Background.exe"
c := testController(f)
defer c.Shutdown()
text := formatReport(c.Step(), time.Unix(0, 0))
if strings.Index(text, "BlockedGame.exe") < 0 || strings.Index(text, "BlockedGame.exe") > strings.Index(text, "Background.exe") {
t.Fatal("the triggering process is hidden below background changes", text)
}
if strings.Contains(text, "NORMAL -> HIGH") {
t.Fatal("denied raise was logged as a success")
}
}
-153
View File
@@ -1,153 +0,0 @@
//go:build windows
package main
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
func cleanLogText(s string, limit int) string {
var out []rune
for _, r := range s {
if unicode.IsControl(r) || unicode.In(r, unicode.Cf) || r == '|' {
r = ' '
}
if len(out) >= limit {
if limit >= 3 {
out = append(out[:limit-3], '.', '.', '.')
}
break
}
out = append(out, r)
}
return string(out)
}
func priorityRank(p uint32) int {
switch p {
case PriorityClassIdle:
return 0
case PriorityClassBelowNormal:
return 1
case PriorityClassNormal:
return 2
case PriorityClassAboveNormal:
return 3
case PriorityClassHigh:
return 4
case PriorityClassRealtime:
return 5
}
return -1
}
func logMemory(b uint64) string {
if b >= 1<<30 {
return fmt.Sprintf("%.1f ГБ", float64(b)/(1<<30))
}
if b >= 1<<20 {
return fmt.Sprintf("%d МБ", b>>20)
}
if b >= 1<<10 {
return fmt.Sprintf("%d КБ", b>>10)
}
return fmt.Sprintf("%d Б", b)
}
func writeLogTable(b *strings.Builder, cells [][]string) {
widths := make([]int, len(cells[0]))
for _, row := range cells {
for i, value := range row {
widths[i] = max(widths[i], utf8.RuneCountInString(value))
}
}
var border strings.Builder
border.WriteByte('+')
for _, width := range widths {
border.WriteString(strings.Repeat("-", width+2))
border.WriteByte('+')
}
border.WriteByte('\n')
b.WriteString(border.String())
for n, row := range cells {
b.WriteByte('|')
for i, value := range row {
fmt.Fprintf(b, " %-*s |", widths[i], value)
}
b.WriteByte('\n')
if n == 0 {
b.WriteString(border.String())
}
}
b.WriteString(border.String())
}
func formatReport(r scanReport, when time.Time) string {
if r.Title == "" && len(r.Rows) == 0 && len(r.Errors) == 0 {
return ""
}
var b strings.Builder
header := func(title string) {
fmt.Fprintf(&b, "[%s] %s", when.Format("02.01.2006 15:04:05"), cleanLogText(title, 200))
if r.DryRun {
b.WriteString(" [DRY-RUN: ПЛАН, БЕЗ ИЗМЕНЕНИЙ]")
}
b.WriteByte('\n')
}
if r.Title != "" || len(r.Rows) > 0 {
title := r.Title
if title == "" {
title = "ИЗМЕНЕНИЕ ПРИОРИТЕТОВ"
}
header(title)
if len(r.Rows) > 0 {
rows := append([]priorityChange(nil), r.Rows...)
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].Heavy != rows[j].Heavy {
return rows[i].Heavy
}
return rows[i].PID < rows[j].PID
})
cells := [][]string{{"Процесс", "PID", "Память", "Повышено", "Понижено"}}
for _, row := range rows {
columns := []string{cleanLogText(row.Name, 64), strconv.FormatUint(uint64(row.PID), 10), logMemory(row.RSS), "", ""}
if row.Blocked {
columns[3] = "в блоклисте"
} else if row.Unchanged {
columns[3] = "уже " + prioName(row.After)
} else {
change := prioName(row.Before) + " -> " + prioName(row.After)
if priorityRank(row.After) > priorityRank(row.Before) {
columns[3] = change
} else {
columns[4] = change
}
}
cells = append(cells, columns)
}
writeLogTable(&b, cells)
}
b.WriteByte('\n')
}
if len(r.Errors) > 0 {
header("ОШИБКИ")
rows := append([]processError(nil), r.Errors...)
sort.SliceStable(rows, func(i, j int) bool { return rows[i].PID < rows[j].PID })
cells := [][]string{{"Процесс", "PID", "Описание ошибки"}}
for _, problem := range rows {
name, pid := problem.Name, "-"
if name == "" {
name = "autoPriority"
}
if problem.PID != 0 {
pid = strconv.FormatUint(uint64(problem.PID), 10)
}
cells = append(cells, []string{cleanLogText(name, 64), pid, cleanLogText(problem.Description, 1500)})
}
writeLogTable(&b, cells)
b.WriteByte('\n')
}
return b.String()
}
-57
View File
@@ -1,57 +0,0 @@
//go:build windows
package main
import (
"strings"
"testing"
"time"
"unicode/utf8"
)
func TestReportTableHeavyFirstAndTruePriorityDirections(t *testing.T) {
r := scanReport{Title: "ИГРОВОЙ РЕЖИМ ВКЛЮЧЁН", Rows: []priorityChange{{Name: "Background.exe", PID: 20, RSS: 500, Before: PriorityClassNormal, After: PriorityClassIdle}, {Name: "Game.exe", PID: 10, RSS: 3000000000, Before: PriorityClassNormal, After: PriorityClassHigh, Heavy: true}}}
text := formatReport(r, time.Unix(0, 0))
if !strings.Contains(text, "Повышено") || !strings.Contains(text, "Понижено") || !strings.Contains(text, "+---") {
t.Fatalf("missing ASCII table: %q", text)
}
if strings.Index(text, "Game.exe") > strings.Index(text, "Background.exe") {
t.Fatal("heavy process must precede lowered processes")
}
var gameLine, bgLine string
for _, line := range strings.Split(text, "\n") {
if strings.Contains(line, "Game.exe") {
gameLine = line
}
if strings.Contains(line, "Background.exe") {
bgLine = line
}
}
if !strings.Contains(strings.Split(gameLine, "|")[4], "NORMAL -> HIGH") {
t.Fatal("raise in wrong column", gameLine)
}
if !strings.Contains(strings.Split(bgLine, "|")[5], "NORMAL -> IDLE") {
t.Fatal("numeric Win32 constants mistaken for scheduling rank", bgLine)
}
if r.Rows[0].Name != "Background.exe" {
t.Fatal("formatter mutated caller's report")
}
}
func TestReportNoFakePromotionNoControlCharacters(t *testing.T) {
r := scanReport{Title: "MODE", DryRun: true, Rows: []priorityChange{{Name: "bad\r\n\x1b|name.exe", PID: 1, Before: PriorityClassHigh, After: PriorityClassHigh, Heavy: true, Unchanged: true}}, Errors: []processError{{Description: "denied\r\nforged"}}}
s := formatReport(r, time.Unix(0, 0))
if strings.Contains(s, "HIGH -> HIGH") || strings.ContainsAny(s, "\r\x1b") {
t.Fatalf("misleading or unsafe record: %q", s)
}
if !strings.Contains(s, "уже HIGH") || !strings.Contains(s, "DRY-RUN") {
t.Fatal("missing unchanged/dry-run markers")
}
if !utf8.ValidString(s) {
t.Fatal("invalid UTF-8")
}
}
func TestEmptyReportProducesNoLog(t *testing.T) {
if got := formatReport(scanReport{}, time.Time{}); got != "" {
t.Fatalf("noisy empty report %q", got)
}
}
+900 -94
View File
File diff suppressed because it is too large. Load diff
-63
View File
@@ -1,63 +0,0 @@
//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)
}
})
}
}
-97
View File
@@ -1,97 +0,0 @@
//go:build windows
package main
import (
"fmt"
"math"
"strconv"
"strings"
)
const (
PriorityClassIdle = 0x00000040
PriorityClassNormal = 0x00000020
PriorityClassHigh = 0x00000080
PriorityClassAboveNormal = 0x00008000
PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000
)
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)
}
}
-102
View File
@@ -1,102 +0,0 @@
//go:build windows
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestNewLaunchClearsLogAndPreviousArchive(t *testing.T) {
path := filepath.Join(t.TempDir(), "autopriority.log")
for _, p := range []string{path, path + ".1"} {
if err := os.WriteFile(p, []byte("PREVIOUS SESSION\n"), 0600); err != nil {
t.Fatal(err)
}
}
w, err := openRotatingLog(path, 256)
if err != nil {
t.Fatal(err)
}
defer w.Close()
data, err := os.ReadFile(path)
if err != nil || len(data) != 0 {
t.Fatal("new session retained the previous log")
}
if _, err = os.Stat(path + ".1"); !os.IsNotExist(err) {
t.Fatal("previous-session archive retained")
}
if err = w.Write("CURRENT SESSION\n"); err != nil {
t.Fatal(err)
}
if err = w.Close(); err != nil {
t.Fatal(err)
}
data, err = os.ReadFile(path)
if err != nil || string(data) != "CURRENT SESSION\n" {
t.Fatal("wrong current-session log")
}
}
func TestDeniedProcessNotRetriedUntilNewInstance(t *testing.T) {
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
p.writable = false
c := testController(f)
defer c.Shutdown()
if r := c.Step(); len(r.Errors) != 1 {
t.Fatal("missing initial denial")
}
p.writable = true
c.now = func() time.Time { return time.Unix(100000, 0) }
if r := c.Step(); len(r.Errors) != 0 {
t.Fatal("repeated denial")
}
assertPriority(t, p, PriorityClassNormal)
p.info.Prio = PriorityClassBelowNormal
p.info.RSS = 250
c.Step()
assertPriority(t, p, PriorityClassBelowNormal)
if p.writes != 0 {
t.Fatal("blocked instance was written again")
}
p.alive = false
replacement := f.add(10, 150, PriorityClassNormal)
delete(f.procs, replacement.info.Key)
replacement.info.Key.Created++
f.procs[replacement.info.Key] = replacement
c.Step()
assertPriority(t, replacement, PriorityClassHigh)
c.Shutdown()
assertPriority(t, replacement, PriorityClassNormal)
assertClosed(t, f)
}
func TestErrorsAreOneSeparateTableWithoutRetryLines(t *testing.T) {
f := newFake()
a := f.add(4, 150, PriorityClassNormal)
a.info.Name = "System"
a.writable = false
b := f.add(148, 150, PriorityClassNormal)
b.info.Name = "Registry"
b.writable = false
c := testController(f)
defer c.Shutdown()
text := formatReport(c.Step(), time.Unix(0, 0))
if !strings.Contains(text, "Описание ошибки") || strings.Contains(text, "ОШИБКА:") || strings.Contains(text, "повтор") {
t.Fatal("errors must be a table without retry lines", text)
}
for _, name := range []string{"System", "Registry"} {
if strings.Count(text, name) != 1 {
t.Fatal("duplicated or missing process", name, text)
}
for _, line := range strings.Split(text, "\n") {
if strings.Contains(line, name) && !strings.HasPrefix(line, "|") {
t.Fatal("process outside error table", line)
}
}
}
}
-108
View File
@@ -1,108 +0,0 @@
//go:build windows
package main
import (
"errors"
"fmt"
"io"
"os"
"strings"
"unicode/utf8"
)
const truncatedLogRecord = "\n[Слишком длинная запись сокращена]\n"
type rotatingLog struct {
file *os.File
path string
limit, size int64
}
func openRotatingLog(path string, limit int64) (*rotatingLog, error) {
if limit < int64(len(truncatedLogRecord)+16) {
return nil, fmt.Errorf("log size limit is too small")
}
for _, name := range []string{path, path + ".1"} {
info, err := os.Lstat(name)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("log target is not a regular file")
}
if name != path {
if err = os.Remove(name); err != nil {
return nil, err
}
}
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return nil, err
}
return &rotatingLog{file: f, path: path, limit: limit}, nil
}
func (w *rotatingLog) Write(text string) error {
if w.file == nil {
return os.ErrClosed
}
if text == "" {
return nil
}
if !utf8.ValidString(text) {
text = strings.ToValidUTF8(text, "?")
}
if int64(len(text)) > w.limit {
end := int(w.limit) - len(truncatedLogRecord)
for end > 0 && !utf8.ValidString(text[:end]) {
end--
}
text = text[:end] + truncatedLogRecord
}
if w.size > 0 && w.size+int64(len(text)) > w.limit {
if err := w.rotate(); err != nil {
return err
}
}
n, err := w.file.WriteString(text)
w.size += int64(n)
if err == nil && n != len(text) {
return io.ErrShortWrite
}
return err
}
func (w *rotatingLog) rotate() error {
err := w.file.Close()
w.file = nil
if err != nil {
return err
}
if info, e := os.Lstat(w.path + ".1"); e == nil {
if !info.Mode().IsRegular() {
return fmt.Errorf("log archive is not a regular file")
}
if e = os.Remove(w.path + ".1"); e != nil {
return e
}
} else if !errors.Is(e, os.ErrNotExist) {
return e
}
if err = os.Rename(w.path, w.path+".1"); err != nil {
return err
}
w.file, err = os.OpenFile(w.path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
w.size = 0
return err
}
func (w *rotatingLog) Close() error {
if w.file == nil {
return nil
}
e := errors.Join(w.file.Sync(), w.file.Close())
w.file = nil
return e
}
-73
View File
@@ -1,73 +0,0 @@
//go:build windows
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"unicode/utf8"
)
func TestLogRotationBoundedUTF8AndRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "test.log")
w, e := openRotatingLog(path, 256)
if e != nil {
t.Fatal(e)
}
for i := 0; i < 8; i++ {
if e = w.Write(strings.Repeat("АБ", 40) + "\n"); e != nil {
t.Fatal(e)
}
}
if e = w.Write(strings.Repeat("Ж", 600)); e != nil {
t.Fatal(e)
}
if e = w.Close(); e != nil {
t.Fatal(e)
}
for _, p := range []string{path, path + ".1"} {
data, e := os.ReadFile(p)
if e != nil {
t.Fatal(e)
}
if len(data) > 256 {
t.Fatalf("unbounded log: %d bytes", len(data))
}
if !utf8.Valid(data) {
t.Fatal("split a UTF-8 character")
}
}
w, e = openRotatingLog(path, 256)
if e != nil {
t.Fatal(e)
}
if e = w.Write("LAST\n"); e != nil {
t.Fatal(e)
}
w.Close()
data, e := os.ReadFile(path)
if e != nil || !strings.HasSuffix(string(data), "LAST\n") {
t.Fatal("restart failed")
}
entries, _ := os.ReadDir(filepath.Dir(path))
if len(entries) != 1 {
t.Fatalf("unexpected files: %d", len(entries))
}
}
func TestLogRejectsDirectoryAndClosedWriter(t *testing.T) {
dir := t.TempDir()
if w, e := openRotatingLog(dir, 256); e == nil {
w.Close()
t.Fatal("accepted a directory")
}
w, e := openRotatingLog(filepath.Join(dir, "x.log"), 256)
if e != nil {
t.Fatal(e)
}
w.Close()
if e = w.Write("x"); e == nil {
t.Fatal("silenced write after close")
}
}
-26
View File
@@ -1,26 +0,0 @@
//go:build windows
package main
import _ "embed"
//go:embed assets/chart_bar_edit.png
var trayPNG []byte
//go:embed assets/cross.png
var exitPNG []byte
func trayActivation(version4 bool, event uint32) bool {
if version4 {
switch event {
case 0x400, 0x401, 0x7b, 0x208, 0x20c:
return true
}
} else {
switch event {
case 0x202, 0x205, 0x208, 0x20c, 0x400, 0x401, 0x7b:
return true
}
}
return false
}
-145
View File
@@ -1,145 +0,0 @@
//go:build windows
package main
import (
"context"
"sync"
"testing"
"time"
"unsafe"
)
type guiThreadInfo struct {
Size, Flags uint32
Active, Focus, Capture, MenuOwner, MoveSize, Caret uintptr
CaretRect winRect
}
func waitNative(t *testing.T, condition func() bool, what string) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
if condition() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("timeout: " + what)
}
func postOwned(t *testing.T, h uintptr, msg uint32, wp, lp uintptr) {
t.Helper()
ok, _, err := pPostMessage.Call(h, uintptr(msg), wp, lp)
if ok == 0 {
t.Fatal(err)
}
}
func ownIconRect(h uintptr) (winRect, bool) {
id := notifyIconIdentifier{Size: uint32(unsafe.Sizeof(notifyIconIdentifier{})), Window: h, ID: trayIconID}
var rect winRect
hr, _, _ := pNotifyRect.Call(uintptr(unsafe.Pointer(&id)), uintptr(unsafe.Pointer(&rect)))
return rect, hr == 0
}
func ownMenuActive(thread uintptr) bool {
info := guiThreadInfo{Size: uint32(unsafe.Sizeof(guiThreadInfo{}))}
ok, _, _ := u32.NewProc("GetGUIThreadInfo").Call(thread, uintptr(unsafe.Pointer(&info)))
return ok != 0 && info.Flags&(0x4|0x10) != 0
}
func TestNativeTrayMouseKeyboardRestartAndControllerExit(t *testing.T) {
requireNativeGUI(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
restored := make(chan struct{})
tr, err := newTray(cancel, restored)
if err != nil {
t.Fatal(err)
}
defer tr.Close()
h := tr.hwnd.Load()
thread, _, _ := u32.NewProc("GetWindowThreadProcessId").Call(h, 0)
f := newFake()
p := f.add(10, 150, PriorityClassNormal)
changed := make(chan struct{})
var once sync.Once
finished := make(chan error, 1)
go func() {
err := runController(ctx, options{mem: 100, interval: time.Minute}, f, func(r scanReport) error {
if len(r.Rows) > 0 {
once.Do(func() { close(changed) })
}
return nil
})
close(restored)
finished <- err
}()
select {
case <-changed:
case <-time.After(3 * time.Second):
t.Fatal("controller did not start")
}
waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "notification icon registration")
// Post only to OUR window. No mouse injection, no interaction with other applications.
for _, event := range []uint32{0x400, 0x7b, 0x208, 0x20c} {
rect, _ := ownIconRect(h)
wp := uintptr(uint16(rect.Left)) | uintptr(uint16(rect.Top))<<16
postOwned(t, h, trayCallbackMessage, wp, uintptr(event)|trayIconID<<16)
waitNative(t, func() bool { return ownMenuActive(thread) }, "popup for a mouse button")
postOwned(t, h, 0x1f, 0, 0)
waitNative(t, func() bool { return !tr.menuOpen.Load() }, "menu cancellation")
select {
case <-ctx.Done():
t.Fatal("cancelling menu unexpectedly exited application")
default:
}
}
// Simulate loss of THIS icon, not an actual restart of the user's Explorer.
data := notifyIconData{Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: h, ID: trayIconID}
removed, _, _ := pShellNotify.Call(2, uintptr(unsafe.Pointer(&data)))
if removed == 0 {
t.Fatal("could not remove own test icon")
}
postOwned(t, h, tr.taskbar, 0, 0)
waitNative(t, func() bool { _, ok := ownIconRect(h); return ok }, "icon recreation after TaskbarCreated")
// The keyboard notification opens the same real menu; select its only item.
postOwned(t, h, trayCallbackMessage, 0, 0x401|trayIconID<<16)
waitNative(t, func() bool { return ownMenuActive(thread) }, "keyboard menu")
postOwned(t, h, 0x100, 0x28, 0) // VK_DOWN
postOwned(t, h, 0x100, 0x0d, 0) // VK_RETURN
select {
case err = <-finished:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("menu selection did not finish restoration")
}
assertPriority(t, p, PriorityClassNormal)
assertClosed(t, f)
if err = tr.Close(); err != nil {
t.Fatal(err)
}
if _, ok := ownIconRect(h); ok {
t.Fatal("notification icon was not removed")
}
}
func TestTrayCoordinatesAndNativeLayout(t *testing.T) {
x, y := int16(-1200), int16(1400)
wp := uintptr(uint16(x)) | uintptr(uint16(y))<<16
event, id, point, anchored := trayEvent(true, wp, 0x400|trayIconID<<16)
if event != 0x400 || id != trayIconID || point.X != -1200 || point.Y != 1400 || !anchored {
t.Fatal("signed multi-monitor anchor lost")
}
_, _, _, anchored = trayEvent(true, wp, 0x7b|trayIconID<<16)
if anchored {
t.Fatal("WM_CONTEXTMENU wParam is undefined in the documented v4 contract")
}
size := unsafe.Sizeof(uintptr(0))
want := map[uintptr]uintptr{4: 956, 8: 976}[size]
if unsafe.Sizeof(notifyIconData{}) != want {
t.Fatal("NOTIFYICONDATAW layout mismatch")
}
if unsafe.Sizeof(bitmapHeader{}) != 40 {
t.Fatal("BITMAPINFOHEADER layout mismatch")
}
}
-70
View File
@@ -1,70 +0,0 @@
//go:build windows
package main
import (
"os"
"sync"
"testing"
"time"
"unsafe"
)
func requireNativeGUI(t *testing.T) {
t.Helper()
if os.Getenv("AP_NATIVE_GUI") != "1" {
t.Skip("opt-in native GUI test; no process scanner is run")
}
}
func TestNativeTrayOneExitItemAndGracefulCommand(t *testing.T) {
requireNativeGUI(t)
requested := make(chan struct{})
restored := make(chan struct{})
close(restored)
var once sync.Once
tr, err := newTray(func() { once.Do(func() { close(requested) }) }, restored)
if err != nil {
t.Fatal(err)
}
defer tr.Close()
h := tr.hwnd.Load()
count, _, _ := u32.NewProc("GetMenuItemCount").Call(tr.menu)
if count != 1 {
t.Fatalf("want exactly one menu item, got %d", count)
}
var label [32]uint16
item := menuItemInfo{Size: uint32(unsafe.Sizeof(menuItemInfo{})), Mask: 0x40 | 0x2 | 0x80, Text: &label[0], TextLen: uint32(len(label))}
ok, _, e := u32.NewProc("GetMenuItemInfoW").Call(tr.menu, 0, 1, uintptr(unsafe.Pointer(&item)))
if ok == 0 {
t.Fatal(e)
}
if item.ID != menuExitID || item.Bitmap == 0 || string(utf16Runes(label[:])) != "Выход" {
t.Fatal("missing Exit label, command, or cross bitmap")
}
ok, _, e = pPostMessage.Call(h, 0x111, menuExitID, 0)
if ok == 0 {
t.Fatal(e)
}
select {
case <-requested:
case <-time.After(3 * time.Second):
t.Fatal("Exit command did not request graceful shutdown")
}
if err = tr.Close(); err != nil {
t.Fatal(err)
}
valid, _, _ := u32.NewProc("IsWindow").Call(h)
if valid != 0 {
t.Fatal("tray window leaked")
}
}
func utf16Runes(in []uint16) []rune {
out := []rune{}
for _, v := range in {
if v == 0 {
break
}
out = append(out, rune(v))
}
return out
}
-43
View File
@@ -1,43 +0,0 @@
//go:build windows
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"image/png"
"testing"
)
func TestTrayMouseButtonsAndKeyboard(t *testing.T) {
for _, event := range []uint32{0x400, 0x401, 0x7b, 0x208, 0x20c} {
if !trayActivation(true, event) {
t.Fatalf("version 4 activation missing: %#x", event)
}
}
for _, event := range []uint32{0x202, 0x205, 0x208, 0x20c} {
if !trayActivation(false, event) {
t.Fatalf("legacy activation missing: %#x", event)
}
}
for _, event := range []uint32{0x202, 0x205, 0x200, 0} {
if trayActivation(true, event) {
t.Fatalf("duplicate or spurious version 4 activation: %#x", event)
}
}
}
func TestEmbeddedOfficialSilkAssets(t *testing.T) {
for _, test := range []struct {
data []byte
hash string
}{{trayPNG, "f52dca6b882152f3a4f2459ea8a02a75a6d925bf18082cae31bb0ecc254fb9eb"}, {exitPNG, "d04ecfc93ff86c44f6fc39e35945e3d8a7648ba8fcd97a2635920df2e88893b3"}} {
if fmt.Sprintf("%x", sha256.Sum256(test.data)) != test.hash {
t.Fatal("asset differs from pinned official repository")
}
config, err := png.DecodeConfig(bytes.NewReader(test.data))
if err != nil || config.Width != 16 || config.Height != 16 {
t.Fatal("invalid Silk image", err)
}
}
}
-453
View File
@@ -1,453 +0,0 @@
//go:build windows
package main
import (
"bytes"
"fmt"
"image/png"
"os"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
)
const (
trayCallbackMessage = 0x8001
trayQuitMessage = 0x8002
trayIconID = 1
menuExitID = 1
)
type winPoint struct{ X, Y int32 }
type winRect struct{ Left, Top, Right, Bottom int32 }
type winMessage struct {
Window uintptr
Message uint32
WParam, LParam uintptr
Time uint32
Point winPoint
Private uint32
}
type windowClass struct {
Size, Style uint32
Proc uintptr
ClassExtra, WindowExtra int32
Instance, Icon, Cursor, Background uintptr
MenuName, ClassName *uint16
SmallIcon uintptr
}
type notifyIconData struct {
Size uint32
Window uintptr
ID, Flags, Callback uint32
Icon uintptr
Tip [128]uint16
State, StateMask uint32
Info [256]uint16
Version uint32
InfoTitle [64]uint16
InfoFlags uint32
GUID [16]byte
BalloonIcon uintptr
}
type notifyIconIdentifier struct {
Size uint32
Window uintptr
ID uint32
GUID [16]byte
}
type menuItemInfo struct {
Size, Mask, Type, State, ID uint32
Submenu, Checked, Unchecked, Data uintptr
Text *uint16
TextLen uint32
Bitmap uintptr
}
type bitmapHeader struct {
Size uint32
Width, Height int32
Planes, BitCount uint16
Compression, ImageSize uint32
XPels, YPels int32
ColorsUsed, ColorsImportant uint32
}
type iconInfo struct {
Icon int32
HotspotX, HotspotY uint32
Mask, Color uintptr
}
func systemDLL(name string) *syscall.LazyDLL {
var dir [32768]uint16
r, _, err := k32.NewProc("GetSystemDirectoryW").Call(uintptr(unsafe.Pointer(&dir[0])), uintptr(len(dir)))
if r == 0 || r >= uintptr(len(dir)) {
panic(fmt.Sprintf("GetSystemDirectoryW: %v", err))
}
return syscall.NewLazyDLL(filepath.Join(syscall.UTF16ToString(dir[:r]), name))
}
var (
u32 = systemDLL("user32.dll")
shell32 = systemDLL("shell32.dll")
gdi32 = systemDLL("gdi32.dll")
pRegisterClass = u32.NewProc("RegisterClassExW")
pUnregisterClass = u32.NewProc("UnregisterClassW")
pCreateWindow = u32.NewProc("CreateWindowExW")
pDestroyWindow = u32.NewProc("DestroyWindow")
pDefWindowProc = u32.NewProc("DefWindowProcW")
pGetMessage = u32.NewProc("GetMessageW")
pTranslateMessage = u32.NewProc("TranslateMessage")
pDispatchMessage = u32.NewProc("DispatchMessageW")
pPostQuit = u32.NewProc("PostQuitMessage")
pPostMessage = u32.NewProc("PostMessageW")
pCreatePopup = u32.NewProc("CreatePopupMenu")
pDestroyMenu = u32.NewProc("DestroyMenu")
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
pTrackPopup = u32.NewProc("TrackPopupMenu")
pForeground = u32.NewProc("SetForegroundWindow")
pEndMenu = u32.NewProc("EndMenu")
pCursorPos = u32.NewProc("GetCursorPos")
pRegisterMessage = u32.NewProc("RegisterWindowMessageW")
pMessageFilter = u32.NewProc("ChangeWindowMessageFilterEx")
pCreateIcon = u32.NewProc("CreateIconIndirect")
pDestroyIcon = u32.NewProc("DestroyIcon")
pCreateDIB = gdi32.NewProc("CreateDIBSection")
pCreateBitmap = gdi32.NewProc("CreateBitmap")
pDeleteObject = gdi32.NewProc("DeleteObject")
pShellNotify = shell32.NewProc("Shell_NotifyIconW")
pNotifyRect = shell32.NewProc("Shell_NotifyIconGetRect")
)
type tray struct {
hwnd atomic.Uintptr
menu uintptr
data notifyIconData
taskbar uint32
version4 bool
menuOpen atomic.Bool
done chan struct{}
err error
failure error // message-loop thread only
onExit func()
restored <-chan struct{}
exitOnce sync.Once
added bool
}
func newTray(onExit func(), restored <-chan struct{}) (*tray, error) {
t := &tray{done: make(chan struct{}), onExit: onExit, restored: restored}
ready := make(chan error, 1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
t.err = t.loop(ready)
t.requestExit()
close(t.done)
}()
if err := <-ready; err != nil {
<-t.done
return nil, err
}
return t, nil
}
func (t *tray) Close() error {
select {
case <-t.done:
return t.err
default:
}
if h := t.hwnd.Load(); h != 0 {
r, _, e := pPostMessage.Call(h, trayQuitMessage, 0, 0)
if r == 0 {
return fmt.Errorf("PostMessage(close): %v", e)
}
}
<-t.done
return t.err
}
var trayWindows sync.Map
var trayWindowCallback = syscall.NewCallback(trayWindowProc)
func (t *tray) requestExit() {
t.exitOnce.Do(func() {
if t.onExit != nil {
t.onExit()
}
})
}
func (t *tray) fail(err error) {
if t.failure == nil {
t.failure = err
}
t.requestExit()
}
func pngBitmap(data []byte) (uintptr, error) {
image, err := png.Decode(bytes.NewReader(data))
if err != nil {
return 0, err
}
if image.Bounds().Dx() != 16 || image.Bounds().Dy() != 16 {
return 0, fmt.Errorf("expected a 16x16 Silk icon")
}
header := bitmapHeader{Size: uint32(unsafe.Sizeof(bitmapHeader{})), Width: 16, Height: -16, Planes: 1, BitCount: 32}
var bits unsafe.Pointer
bitmap, _, e := pCreateDIB.Call(0, uintptr(unsafe.Pointer(&header)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
if bitmap == 0 {
return 0, fmt.Errorf("CreateDIBSection: %v", e)
}
if bits == nil {
pDeleteObject.Call(bitmap)
return 0, fmt.Errorf("CreateDIBSection returned no pixels")
}
pixels := unsafe.Slice((*byte)(bits), 16*16*4)
for y := 0; y < 16; y++ {
for x := 0; x < 16; x++ {
r, g, b, a := image.At(x, y).RGBA()
offset := (y*16 + x) * 4
// RGBA() is premultiplied; native menu bitmaps require premultiplied BGRA.
pixels[offset] = byte(b >> 8)
pixels[offset+1] = byte(g >> 8)
pixels[offset+2] = byte(r >> 8)
pixels[offset+3] = byte(a >> 8)
}
}
return bitmap, nil
}
func pngIcon(data []byte) (uintptr, error) {
color, err := pngBitmap(data)
if err != nil {
return 0, err
}
defer pDeleteObject.Call(color)
var maskBits [32]byte
mask, _, e := pCreateBitmap.Call(16, 16, 1, 1, uintptr(unsafe.Pointer(&maskBits[0])))
if mask == 0 {
return 0, fmt.Errorf("CreateBitmap(mask): %v", e)
}
defer pDeleteObject.Call(mask)
info := iconInfo{Icon: 1, Mask: mask, Color: color}
icon, _, e := pCreateIcon.Call(uintptr(unsafe.Pointer(&info)))
if icon == 0 {
return 0, fmt.Errorf("CreateIconIndirect: %v", e)
}
return icon, nil
}
func (t *tray) addIcon() error {
t.data.Flags = 0x87 // NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP
ok, _, _ := pShellNotify.Call(0, uintptr(unsafe.Pointer(&t.data))) // NIM_ADD
if ok == 0 {
ok, _, _ = pShellNotify.Call(1, uintptr(unsafe.Pointer(&t.data)))
} // existing icon: NIM_MODIFY
if ok == 0 {
return fmt.Errorf("Shell_NotifyIcon: notification area unavailable")
}
t.added = true
t.data.Version = 4
ok, _, _ = pShellNotify.Call(4, uintptr(unsafe.Pointer(&t.data))) // NIM_SETVERSION, after EVERY add
t.version4 = ok != 0
return nil
}
func (t *tray) removeIcon() {
if t.added {
pShellNotify.Call(2, uintptr(unsafe.Pointer(&t.data)))
t.added = false
}
}
func (t *tray) loop(ready chan<- error) (result error) {
sent := false
defer func() {
if !sent {
ready <- result
}
}()
icon, err := pngIcon(trayPNG)
if err != nil {
return err
}
defer pDestroyIcon.Call(icon)
cross, err := pngBitmap(exitPNG)
if err != nil {
return err
}
defer pDeleteObject.Call(cross)
menu, _, e := pCreatePopup.Call()
if menu == 0 {
return fmt.Errorf("CreatePopupMenu: %v", e)
}
t.menu = menu
defer pDestroyMenu.Call(menu)
label, _ := syscall.UTF16PtrFromString("Выход")
item := menuItemInfo{Size: uint32(unsafe.Sizeof(menuItemInfo{})), Mask: 0x2 | 0x40 | 0x80, ID: menuExitID, Text: label, Bitmap: cross}
ok, _, e := pInsertMenuItem.Call(menu, 0, 1, uintptr(unsafe.Pointer(&item)))
if ok == 0 {
return fmt.Errorf("InsertMenuItemW: %v", e)
}
instance, _, e := k32.NewProc("GetModuleHandleW").Call(0)
if instance == 0 {
return fmt.Errorf("GetModuleHandleW: %v", e)
}
className, _ := syscall.UTF16PtrFromString(fmt.Sprintf("autoPriority.Tray.%d", os.Getpid()))
wc := windowClass{Size: uint32(unsafe.Sizeof(windowClass{})), Proc: trayWindowCallback, Instance: instance, ClassName: className, Icon: icon, SmallIcon: icon}
atom, _, e := pRegisterClass.Call(uintptr(unsafe.Pointer(&wc)))
if atom == 0 {
return fmt.Errorf("RegisterClassExW: %v", e)
}
defer pUnregisterClass.Call(uintptr(unsafe.Pointer(className)), instance)
// Hidden TOP-LEVEL window: a message-only window would miss TaskbarCreated.
h, _, e := pCreateWindow.Call(0, uintptr(unsafe.Pointer(className)), 0, 0, 0, 0, 0, 0, 0, 0, instance, 0)
if h == 0 {
return fmt.Errorf("CreateWindowExW: %v", e)
}
t.hwnd.Store(h)
trayWindows.Store(h, t)
defer func() {
if t.hwnd.Load() != 0 {
pDestroyWindow.Call(h)
}
trayWindows.Delete(h)
}()
taskbarName, _ := syscall.UTF16PtrFromString("TaskbarCreated")
taskbar, _, e := pRegisterMessage.Call(uintptr(unsafe.Pointer(taskbarName)))
if taskbar == 0 {
return fmt.Errorf("RegisterWindowMessageW: %v", e)
}
t.taskbar = uint32(taskbar)
// Only the Explorer restart message crosses an elevated window's UIPI boundary.
ok, _, e = pMessageFilter.Call(h, taskbar, 1, 0)
if ok == 0 {
return fmt.Errorf("ChangeWindowMessageFilterEx: %v", e)
}
t.data = notifyIconData{Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: h, ID: trayIconID, Callback: trayCallbackMessage, Icon: icon}
tip, _ := syscall.UTF16FromString("autoPriority")
copy(t.data.Tip[:], tip)
if err = t.addIcon(); err != nil {
return err
}
defer t.removeIcon()
ready <- nil
sent = true
for {
var msg winMessage
got, _, e := pGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
if int32(got) == -1 {
return fmt.Errorf("GetMessageW: %v", e)
}
if got == 0 {
return t.failure
}
pTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
pDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
}
}
func trayEvent(version4 bool, wp, lp uintptr) (event, id uint32, point winPoint, anchored bool) {
event = uint32(lp)
id = uint32(wp)
if version4 {
event = uint32(lp) & 0xffff
id = uint32(lp>>16) & 0xffff
if event == 0x400 || event == 0x401 || (event >= 0x200 && event <= 0x20e) {
point = winPoint{int32(int16(wp & 0xffff)), int32(int16((wp >> 16) & 0xffff))}
anchored = true
}
}
return
}
func (t *tray) popup(point winPoint, anchored bool) {
if t.menuOpen.Swap(true) {
return
}
defer t.menuOpen.Store(false)
if !anchored {
ok, _, _ := pCursorPos.Call(uintptr(unsafe.Pointer(&point)))
if ok == 0 {
point = winPoint{}
}
}
h := t.hwnd.Load()
if h == 0 {
return
}
pForeground.Call(h)
k32.NewProc("SetLastError").Call(0)
command, _, e := pTrackPopup.Call(t.menu, 0x100|0x80|0x2, uintptr(point.X), uintptr(point.Y), 0, h, 0)
pPostMessage.Call(h, 0, 0, 0) // documented focus workaround after TrackPopupMenu
if command == menuExitID {
t.requestExit()
} else if command == 0 && e != syscall.Errno(0) {
t.fail(fmt.Errorf("TrackPopupMenu: %v", e))
}
pShellNotify.Call(3, uintptr(unsafe.Pointer(&t.data))) // NIM_SETFOCUS
}
func trayWindowProc(h uintptr, message uint32, wp, lp uintptr) uintptr {
value, ok := trayWindows.Load(h)
if !ok {
r, _, _ := pDefWindowProc.Call(h, uintptr(message), wp, lp)
return r
}
t := value.(*tray)
if message == t.taskbar {
if err := t.addIcon(); err != nil {
t.fail(err)
}
return 0
}
switch message {
case trayCallbackMessage:
event, id, point, anchored := trayEvent(t.version4, wp, lp)
if id == trayIconID && trayActivation(t.version4, event) {
t.popup(point, anchored)
}
return 0
case 0x111:
if uint32(wp)&0xffff == menuExitID {
t.requestExit()
}
return 0 // WM_COMMAND
case 0x10:
t.requestExit()
return 0 // WM_CLOSE requests cleanup, not forceful exit
case 0x11:
return 1 // WM_QUERYENDSESSION
case 0x16: // WM_ENDSESSION: give the controller a bounded chance to restore
if wp != 0 {
t.requestExit()
select {
case <-t.restored:
case <-time.After(3 * time.Second):
}
}
return 0
case 0x1f:
pEndMenu.Call()
return 0 // WM_CANCELMODE
case trayQuitMessage:
pEndMenu.Call()
pDestroyWindow.Call(h)
return 0
case 0x2:
t.removeIcon()
t.hwnd.Store(0)
t.requestExit()
pPostQuit.Call(0)
return 0 // WM_DESTROY
case 0x82:
trayWindows.Delete(h) // WM_NCDESTROY
}
r, _, _ := pDefWindowProc.Call(h, uintptr(message), wp, lp)
return r
}
func showError(err error) {
console, _, _ := k32.NewProc("GetConsoleWindow").Call()
if console != 0 {
return
}
text, _ := syscall.UTF16PtrFromString("autoPriority: " + err.Error())
title, _ := syscall.UTF16PtrFromString("autoPriority")
u32.NewProc("MessageBoxW").Call(0, uintptr(unsafe.Pointer(text)), uintptr(unsafe.Pointer(title)), 0x10)
}
-295
View File
@@ -1,295 +0,0 @@
//go:build windows
package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
)
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessSetInformation = 0x00000200
ProcessQueryLimitedInformation = 0x00001000
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
PagefileUsage uintptr
PeakPagefileUsage 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
Waitable bool
ParentPID uint32
}
var (
k32 = syscall.NewLazyDLL("kernel32.dll")
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = k32.NewProc("Process32FirstW")
procProcess32Next = k32.NewProc("Process32NextW")
procGetMemInfo = k32.NewProc("K32GetProcessMemoryInfo")
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, buf []uint16) (string, error) {
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) {
r, _, err := procWaitForSingleObject.Call(uintptr(h), 0)
return decodeWaitResult(r, err)
}
func decodeWaitResult(r uintptr, err error) (bool, error) {
switch r {
case 0:
return false, nil
case 0x102:
return true, nil
default:
return false, fmt.Errorf("WaitForSingleObject returned %#x: %v", r, err)
}
}
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 {
if e == syscall.Errno(ErrorNoMoreFiles) {
return nil, nil
}
return nil, fmt.Errorf("Process32First failed: %w", e)
}
var out []procInfo
for {
name := syscall.UTF16ToString(pe.ExeFile[:])
out = append(out, procInfo{PID: pe.PID, ParentPID: pe.ParentPID, 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
}
const processSynchronize = 0x00100000
var procWaitForSingleObject = k32.NewProc("WaitForSingleObject")
type windowsBackend struct {
list func() ([]procInfo, error)
self uint32
nameBuffer [32768]uint16
unreadable map[uint32]procInfo
}
func newWindowsBackend() *windowsBackend {
return &windowsBackend{list: allProcs, self: uint32(os.Getpid()), unreadable: make(map[uint32]procInfo)}
}
func (w *windowsBackend) inspect(p procInfo) (procInfo, error) {
h, err := openProc(p.PID, ProcessQueryLimitedInformation|processSynchronize)
p.Waitable = err == nil
if err != nil {
h, err = openProc(p.PID, ProcessQueryLimitedInformation)
}
if err != nil {
return procInfo{}, err
}
good := false
defer func() {
if !good {
closeH(h)
}
}()
created, err := processCreated(h)
if err != nil {
return procInfo{}, err
}
if name, e := processName(h, w.nameBuffer[:]); e == nil {
p.Name = name
}
p.Handle = h
p.Key = processKey{PID: p.PID, Created: created}
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 {
return procInfo{}, fmt.Errorf("K32GetProcessMemoryInfo: %w", e)
}
p.RSS = uint64(m.WorkingSetSize)
p.Prio, err = w.Priority(&p)
if err != nil {
return procInfo{}, err
}
good = true
return p, nil
}
func (w *windowsBackend) Snapshot() (processSnapshot, error) {
entries, err := w.list()
if err != nil {
return processSnapshot{}, err
}
snap := processSnapshot{PIDs: make(map[uint32]bool, len(entries)), Processes: make([]procInfo, 0, len(entries))}
for _, entry := range entries {
snap.PIDs[entry.PID] = true
if entry.PID == 0 || entry.PID == w.self {
continue
}
if blocked, ok := w.unreadable[entry.PID]; ok && blocked.Name == entry.Name && blocked.ParentPID == entry.ParentPID {
continue
}
info, err := w.inspect(entry)
if err != nil {
snap.Errors = append(snap.Errors, processError{Name: entry.Name, PID: entry.PID, Description: fmt.Sprintf("чтение: %v; внесён в блоклист", err)})
w.unreadable[entry.PID] = entry
continue
}
delete(w.unreadable, entry.PID)
snap.Processes = append(snap.Processes, info)
}
for pid := range w.unreadable {
if !snap.PIDs[pid] {
delete(w.unreadable, pid)
}
}
return snap, nil
}
func (w *windowsBackend) Alive(p *procInfo) (bool, error) { return processAlive(p.Handle) }
func (w *windowsBackend) Priority(p *procInfo) (uint32, error) {
r, _, err := procGetPriority.Call(uintptr(p.Handle))
if r == 0 {
return 0, fmt.Errorf("GetPriorityClass: %w", err)
}
return uint32(r), nil
}
func (w *windowsBackend) SetPriority(p *procInfo, target uint32) error {
// Request write access only for an actual change, never while enumerating.
h, err := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
if err != nil {
return err
}
defer closeH(h)
created, err := processCreated(h)
if err != nil {
return err
}
if created != p.Key.Created {
return fmt.Errorf("process instance changed; refusing to write")
}
return setPrio(h, p.PID, target)
}
func (w *windowsBackend) Close(p *procInfo) {
if p.Handle != 0 {
closeH(p.Handle)
p.Handle = 0
}
}
-146
View File
@@ -1,146 +0,0 @@
//go:build windows
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"sync"
"syscall"
"testing"
"time"
)
func TestNativeProcessHelper(t *testing.T) {
if os.Getenv("AP_OWNED_HELPER") != "1" {
t.Skip("helper child only")
}
fmt.Fprintln(os.Stdout, "READY")
io.Copy(io.Discard, os.Stdin)
os.Exit(0)
}
func startOwnedHelper(t *testing.T) (uint32, func()) {
t.Helper()
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
cmd := exec.CommandContext(ctx, exe, "-test.run=^TestNativeProcessHelper$")
cmd.Env = append(os.Environ(), "AP_OWNED_HELPER=1")
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
input, err := cmd.StdinPipe()
if err != nil {
cancel()
t.Fatal(err)
}
output, err := cmd.StdoutPipe()
if err != nil {
input.Close()
cancel()
t.Fatal(err)
}
if err = cmd.Start(); err != nil {
input.Close()
cancel()
t.Fatal(err)
}
var once sync.Once
stop := func() { once.Do(func() { input.Close(); cmd.Wait(); cancel() }) }
t.Cleanup(stop)
line, err := bufio.NewReader(output).ReadString('\n')
if err != nil || line != "READY\n" {
t.Fatalf("owned helper did not start: %v", err)
}
return uint32(cmd.Process.Pid), stop
}
func requireNativeProcesses(t *testing.T) {
t.Helper()
if os.Getenv("AP_NATIVE_PROCESSES") != "1" {
t.Skip("opt-in, test-owned children only; never enumerates other processes")
}
}
func TestNativeOwnedProcessRightsIdentityAndLifetime(t *testing.T) {
requireNativeProcesses(t)
pid, stop := startOwnedHelper(t)
backend := newWindowsBackend()
// The system-wide enumerator is intentionally NEVER called in native tests.
backend.list = func() ([]procInfo, error) { return []procInfo{{PID: pid, Name: "OwnedHelper.exe"}}, nil }
snap, err := backend.Snapshot()
if err != nil || len(snap.Processes) != 1 {
t.Fatalf("owned snapshot failed: %v", err)
}
p := snap.Processes[0]
defer backend.Close(&p)
if p.Key.Created == 0 || p.RSS == 0 || p.Name == "" || !p.Waitable {
t.Fatal("incomplete owned-process metadata")
}
alive, err := backend.Alive(&p)
if err != nil || !alive {
t.Fatal("live helper not recognized", err)
}
if err = setPrio(p.Handle, p.PID, p.Prio); err == nil {
t.Fatal("snapshot handle unexpectedly grants SET_INFORMATION")
}
original := p.Prio
bad := p
bad.Key.Created++
if err = backend.SetPriority(&bad, PriorityClassHigh); err == nil {
t.Fatal("identity mismatch did not prevent write")
}
now, err := backend.Priority(&p)
if err != nil || now != original {
t.Fatal("identity check changed priority")
}
if err = backend.SetPriority(&p, PriorityClassBelowNormal); err != nil {
t.Fatal(err)
}
now, err = backend.Priority(&p)
if err != nil || now != PriorityClassBelowNormal {
t.Fatal("owned priority write not observed", err)
}
// Do not request REALTIME in native tests. The child is about to terminate.
stop()
alive, err = backend.Alive(&p)
if err != nil || alive {
t.Fatal("signaled process handle not recognized as exited", err)
}
}
func TestNativeControllerRestoresOwnedChild(t *testing.T) {
requireNativeProcesses(t)
pid, _ := startOwnedHelper(t)
backend := newWindowsBackend()
backend.list = func() ([]procInfo, error) { return []procInfo{{PID: pid, Name: "OwnedHelper.exe"}}, nil }
initial, err := backend.inspect(procInfo{PID: pid})
if err != nil {
t.Fatal(err)
}
defer backend.Close(&initial)
// Establish a known, non-realtime baseline on this test-owned child.
if err = backend.SetPriority(&initial, PriorityClassBelowNormal); err != nil {
t.Fatal(err)
}
c := newController(backend, 1, 0, false, time.Minute)
r := c.Step()
if len(r.Errors) != 0 {
c.Shutdown()
t.Fatal(r.Errors)
}
actual, err := backend.Priority(&initial)
if err != nil || actual != PriorityClassHigh {
c.Shutdown()
t.Fatal("promotion failed", err)
}
r = c.Shutdown()
if r.Unrestored != 0 || len(r.Errors) != 0 {
t.Fatal(r)
}
actual, err = backend.Priority(&initial)
if err != nil || actual != PriorityClassBelowNormal {
t.Fatal("baseline not restored", err)
}
}
-15
View File
@@ -1,15 +0,0 @@
//go:build windows
package main
import (
"testing"
"unsafe"
)
func TestMemoryCounterLayout(t *testing.T) {
var m processMemoryCounters
if unsafe.Offsetof(m.PagefileUsage) >= unsafe.Offsetof(m.PeakPagefileUsage) {
t.Fatal("PagefileUsage must precede PeakPagefileUsage in the Windows ABI")
}
}