feat: grouped box-drawing log with game-mode sections

One box per scan, strict group order PROMOTE -> DEMOTE -> SKIP -> BLOCK, header sized to the longest row. Game trigger outcome folds into the GAME MODE ON head line instead of duplicating a PROMOTE row; mode exit renders a GAME MODE OFF head with restored rows in the usual groups.
This commit is contained in:
lzrdblzzrd committed 2026-10-04 19:56:46 +03:00
1 parent 4600cc0862
commit e391138337
2 files changed
+232 -33

No files matched your search

+17 -6
View File
@@ -39,16 +39,27 @@ autoPriority.exe -dry-run
## Журнал
Обычные строки во временной папке Windows: **`%TEMP%\autopriority.log`**. При каждом запуске файл очищается. Формат строк:
События одного скана выводятся боксом во временной папке Windows: **`%TEMP%\autopriority.log`**. При каждом запуске файл очищается. Шапка подгоняется под самую длинную строку. Порядок групп строгий: PROMOTE -> DEMOTE -> SKIP -> BLOCK. Пустой скан ничего не пишет.
```text
[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): ... (повторов не будет)
┌─ 04.10.2026 19:33:39 ─────────────────────────────
├ GAME MODE ON — EoCApp.exe (PID 6200) RSS=2.2GB, NORMAL -> HIGH
│
├ DEMOTE
│ ├ Hermes.exe (PID 10220) RSS=166MB, ABOVE_NORMAL -> IDLE
│ └ librewolf.exe (PID 6216) RSS=524MB, HIGH -> IDLE
│
├ SKIP
│ └ csrss.exe (PID 524): OpenProcess(524) failed: Access is denied.
│
├ BLOCK
│ └ winlogon.exe (PID 792): OpenProcess(792) failed: Access is denied.
└───────────────────────────────────────────────────
```
Режим `-dry-run` помечает строки `[DRY-RUN]`.
Триггер игрового режима не дублируется в PROMOTE: его исход уже вписан в строку `GAME MODE ON` (`NORMAL -> HIGH`, `уже HIGH` или `в блоклисте`). Выключение режима — секция `GAME MODE OFF — восстановление`, восстановленные процессы идут в PROMOTE/DEMOTE как обычно.
Режим `-dry-run` помечает строки `(план)`.
## Ограничения
+215 -27
View File
@@ -14,6 +14,7 @@ import (
"strings"
"syscall"
"time"
"unicode"
"unsafe"
)
@@ -113,6 +114,8 @@ var (
var logf func(string, ...any)
var logRaw func(string)
func closeH(h syscall.Handle) {
_, _, _ = procCloseHandle.Call(uintptr(h))
}
@@ -305,6 +308,15 @@ type app struct {
unreadable map[uint32]procInfo
lastScan []procInfo
nameBuf [32768]uint16
buffering bool
muteSuccess bool
offAnnounced bool
gameHead string
triggerOutcomes []string
bufPromote []string
bufDemote []string
bufSkip []string
bufBlock []string
}
func newApp(mem, gameMem uint64, dryRun bool) *app {
@@ -455,7 +467,12 @@ func (a *app) failed(s *processState, target uint32, err error) {
return
}
a.blocked[s.info.Key] = true
logf("BLOCK %s (PID %d): %s: %v (повторов не будет)", s.info.Name, s.info.PID, prioName(target), err)
detail := fmt.Sprintf("%s (PID %d): %v", s.info.Name, s.info.PID, err)
if a.buffering {
a.bufBlock = append(a.bufBlock, cleanDetail(detail))
return
}
logf("BLOCK %s (повторов не будет)", detail)
}
func actionName(hasGame bool, target uint32) string {
@@ -488,9 +505,9 @@ func (a *app) change(s *processState, target uint32, kind changeKind, hasGame bo
return true
}
if a.dryRun {
logf("[DRY-RUN] %s %s (PID %d) RSS=%s, %s -> %s",
actionName(hasGame, target), s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(target))
a.emitChange(hasGame, target, fmt.Sprintf("[DRY-RUN] %s (PID %d) RSS=%s, %s -> %s",
s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(target)))
return true
}
if err = a.setPriority(&s.info, target); err != nil {
@@ -524,9 +541,9 @@ func (a *app) change(s *processState, target uint32, kind changeKind, hasGame bo
s.idleSince = 0
}
if actual != current {
logf("%s %s (PID %d) RSS=%s, %s -> %s",
actionName(hasGame, target), s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(actual))
a.emitChange(hasGame, target, fmt.Sprintf("%s (PID %d) RSS=%s, %s -> %s",
s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(actual)))
}
if actual != target {
a.failed(s, target, fmt.Errorf("Windows установила %s вместо %s", prioName(actual), prioName(target)))
@@ -599,7 +616,174 @@ func (a *app) releaseUnused() {
}
}
func cleanDetail(s string) string {
var b strings.Builder
n := 0
for _, r := range s {
if n >= 160 {
b.WriteString("...")
break
}
if unicode.IsControl(r) || unicode.In(r, unicode.Cf) || strings.ContainsRune("│├└┌─┐┘┬┴┼┤", r) {
r = ' '
}
b.WriteRune(r)
n++
}
return b.String()
}
func runeCount(s string) int {
n := 0
for range s {
n++
}
return n
}
func (a *app) emitPromote(detail string) {
detail = cleanDetail(detail)
if a.buffering {
a.bufPromote = append(a.bufPromote, detail)
return
}
logf("PROMOTE %s", detail)
}
func (a *app) emitDemote(detail string) {
detail = cleanDetail(detail)
if a.buffering {
a.bufDemote = append(a.bufDemote, detail)
return
}
logf("DEMOTE %s", detail)
}
func (a *app) emitSkip(detail string) {
detail = cleanDetail(detail)
if a.buffering {
a.bufSkip = append(a.bufSkip, detail)
return
}
logf("SKIP %s (повторов не будет)", detail)
}
func (a *app) emitBlock(detail string) {
detail = cleanDetail(detail)
if a.buffering {
a.bufBlock = append(a.bufBlock, detail)
return
}
logf("BLOCK %s (повторов не будет)", detail)
}
func (a *app) emitChange(hasGame bool, target uint32, detail string) {
// Триггеры игрового режима не дублируются в PROMOTE/DEMOTE:
// их исход уже вписан в секцию GAME MODE ON.
if target == PriorityClassHigh {
if a.buffering {
if !a.muteSuccess {
a.bufPromote = append(a.bufPromote, cleanDetail(detail))
}
return
}
logf("%s %s", actionName(hasGame, target), detail)
return
}
if a.buffering {
if !a.muteSuccess {
a.bufDemote = append(a.bufDemote, cleanDetail(detail))
}
return
}
logf("%s %s", actionName(hasGame, target), detail)
}
// flushScan выводит события одного скана одним боксом.
// Порядок групп строгий: PROMOTE -> DEMOTE -> SKIP -> BLOCK.
func (a *app) flushScan() {
a.buffering = false
groups := []struct {
name string
rows []string
}{
{"PROMOTE", a.bufPromote},
{"DEMOTE", a.bufDemote},
{"SKIP", a.bufSkip},
{"BLOCK", a.bufBlock},
}
head := a.gameHead
a.gameHead = ""
a.bufPromote, a.bufDemote, a.bufSkip, a.bufBlock = nil, nil, nil, nil
var lines []string
if head != "" {
lines = append(lines, "├ "+head)
}
for _, g := range groups {
if len(g.rows) == 0 {
continue
}
if len(lines) > 0 {
lines = append(lines, "│")
}
lines = append(lines, "├ "+g.name)
for i, row := range g.rows {
branch := "├"
if i == len(g.rows)-1 {
branch = "└"
}
lines = append(lines, "│ "+branch+" "+row)
}
}
if len(lines) == 0 {
return
}
width := 0
for _, line := range lines {
if w := runeCount(line); w > width {
width = w
}
}
ts := time.Now().Format("02.01.2006 15:04:05")
top := "┌─ " + ts + " "
if runeCount(top) < width {
top += strings.Repeat("─", width-runeCount(top))
} else {
width = runeCount(top)
}
logRaw(top)
for _, line := range lines {
logRaw(line)
}
logRaw("└" + strings.Repeat("─", width-1))
}
// handleTrigger проводит триггер через смену приоритета, но вместо
// строки в PROMOTE возвращает исход для секции GAME MODE ON.
func (a *app) handleTrigger(s *processState, target uint32, hasGame bool) {
base := fmt.Sprintf("%s (PID %d) RSS=%s", s.info.Name, s.info.PID, formatMemSize(s.info.RSS))
if target == s.info.Prio {
a.triggerOutcomes = append(a.triggerOutcomes, base+", уже HIGH")
return
}
before := s.info.Prio
a.muteSuccess = true
ok := a.change(s, target, gameChange, hasGame)
a.muteSuccess = false
switch {
case !ok:
a.triggerOutcomes = append(a.triggerOutcomes, base+", в блоклисте")
case a.dryRun:
a.triggerOutcomes = append(a.triggerOutcomes, fmt.Sprintf("%s, %s -> %s (план)", base, prioName(before), prioName(target)))
default:
a.triggerOutcomes = append(a.triggerOutcomes, fmt.Sprintf("%s, %s -> %s", base, prioName(before), prioName(s.info.Prio)))
}
}
func (a *app) scan() {
a.buffering = true
a.triggerOutcomes = nil
defer a.flushScan()
entries, err := allProcs()
if err != nil {
logf("process scan error: %v", err)
@@ -619,7 +803,7 @@ func (a *app) scan() {
}
info, err := a.inspect(entry)
if err != nil {
logf("SKIP %s (PID %d): %v (повторов не будет)", entry.Name, entry.PID, err)
a.emitSkip(fmt.Sprintf("%s (PID %d): %v", entry.Name, entry.PID, err))
a.unreadable[entry.PID] = entry
continue
}
@@ -669,10 +853,11 @@ func (a *app) scan() {
}
a.adoptInherited(live)
newGames := make(map[processKey]bool)
for key, s := range a.states {
if live[key] && a.gameMem > 0 && s.info.RSS >= a.gameMem && !s.game {
s.game = true
logf("GAME DETECT %s (PID %d) RSS=%s", s.info.Name, s.info.PID, formatMemSize(s.info.RSS))
newGames[key] = true
}
}
hasGame := false
@@ -682,26 +867,24 @@ func (a *app) scan() {
break
}
}
if hasGame && !a.gameMode {
logf("GAME MODE ON")
enteringGame := hasGame && !a.gameMode
if enteringGame {
a.offAnnounced = false
}
if !hasGame && a.gameMode {
a.restoreAll(fresh)
if !a.anyOverlay() {
a.gameMode = false
logf("GAME MODE OFF")
if !a.offAnnounced {
a.gameHead = "GAME MODE OFF — восстановление"
a.offAnnounced = true
}
} else {
return
}
}
a.gameMode = hasGame
gameSeen := make(map[processKey]bool, len(a.states))
for key, s := range a.states {
if s.game {
gameSeen[key] = true
}
}
for _, s := range a.ordered() {
if !hasGame && s.overlay {
a.restoreOverlay(s)
@@ -713,22 +896,24 @@ func (a *app) scan() {
if target == 0 {
continue
}
if target == s.info.Prio {
if gameSeen[s.info.Key] && s.info.Prio == PriorityClassHigh && !s.changed {
logf("GAME %s (PID %d) RSS=%s уже HIGH", s.info.Name, s.info.PID, formatMemSize(s.info.RSS))
delete(gameSeen, s.info.Key)
if newGames[s.info.Key] {
a.handleTrigger(s, target, hasGame)
continue
}
if target == s.info.Prio {
continue
}
kind := normalChange
if hasGame {
kind = gameChange
}
if !a.change(s, target, kind, hasGame) && gameSeen[s.info.Key] {
logf("GAME %s (PID %d) в блоклисте, режим включён", s.info.Name, s.info.PID)
delete(gameSeen, s.info.Key)
} else if gameSeen[s.info.Key] && s.info.Prio == target {
delete(gameSeen, s.info.Key)
a.change(s, target, kind, hasGame)
}
if enteringGame {
if len(a.triggerOutcomes) > 0 {
a.gameHead = "GAME MODE ON — " + strings.Join(a.triggerOutcomes, "; ")
} else {
a.gameHead = "GAME MODE ON"
}
}
a.releaseUnused()
@@ -899,6 +1084,9 @@ func main() {
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
fmt.Fprintf(logFile, format+"\n", v...)
}
logRaw = func(s string) {
fmt.Fprintln(logFile, s)
}
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)