From e391138337ec44497cfbd98b8069c914fd686b3a Mon Sep 17 00:00:00 2001 From: lzrdblzzrd Date: Sun, 4 Oct 2026 19:56:46 +0300 Subject: [PATCH] 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. --- README.md | 23 +++-- main.go | 260 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 241 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 8f899ac..d4f9b1c 100644 --- a/README.md +++ b/README.md @@ -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` помечает строки `(план)`. ## Ограничения diff --git a/main.go b/main.go index 8265bea..916bd35 100644 --- a/main.go +++ b/main.go @@ -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)) } @@ -296,15 +299,24 @@ func singleInstance() (syscall.Handle, bool, error) { } type app struct { - mem, gameMem uint64 - dryRun bool - gameMode bool - self uint32 - states map[processKey]*processState - blocked map[processKey]bool - unreadable map[uint32]procInfo - lastScan []procInfo - nameBuf [32768]uint16 + mem, gameMem uint64 + dryRun bool + gameMode bool + self uint32 + states map[processKey]*processState + blocked map[processKey]bool + 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 newGames[s.info.Key] { + a.handleTrigger(s, target, hasGame) + 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) - } 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)