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
+241 -42

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 ```text
[04.10.2026 18:14:53] PROMOTE Game.exe (PID 4321) RSS=5.8GB, NORMAL -> HIGH ┌─ 04.10.2026 19:33:39 ─────────────────────────────
[04.10.2026 18:14:53] GAME Browser.exe (PID 5678) RSS=740MB, HIGH -> IDLE ├ GAME MODE ON — EoCApp.exe (PID 6200) RSS=2.2GB, NORMAL -> HIGH
[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): ... (повторов не будет) ├ 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` помечает строки `(план)`.
## Ограничения ## Ограничения
+224 -36
View File
@@ -14,6 +14,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"time" "time"
"unicode"
"unsafe" "unsafe"
) )
@@ -113,6 +114,8 @@ var (
var logf func(string, ...any) var logf func(string, ...any)
var logRaw func(string)
func closeH(h syscall.Handle) { func closeH(h syscall.Handle) {
_, _, _ = procCloseHandle.Call(uintptr(h)) _, _, _ = procCloseHandle.Call(uintptr(h))
} }
@@ -296,15 +299,24 @@ func singleInstance() (syscall.Handle, bool, error) {
} }
type app struct { type app struct {
mem, gameMem uint64 mem, gameMem uint64
dryRun bool dryRun bool
gameMode bool gameMode bool
self uint32 self uint32
states map[processKey]*processState states map[processKey]*processState
blocked map[processKey]bool blocked map[processKey]bool
unreadable map[uint32]procInfo unreadable map[uint32]procInfo
lastScan []procInfo lastScan []procInfo
nameBuf [32768]uint16 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 { func newApp(mem, gameMem uint64, dryRun bool) *app {
@@ -455,7 +467,12 @@ func (a *app) failed(s *processState, target uint32, err error) {
return return
} }
a.blocked[s.info.Key] = true 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 { 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 return true
} }
if a.dryRun { if a.dryRun {
logf("[DRY-RUN] %s %s (PID %d) RSS=%s, %s -> %s", a.emitChange(hasGame, target, fmt.Sprintf("[DRY-RUN] %s (PID %d) RSS=%s, %s -> %s",
actionName(hasGame, target), s.info.Name, s.info.PID, s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(target)) formatMemSize(s.info.RSS), prioName(current), prioName(target)))
return true return true
} }
if err = a.setPriority(&s.info, target); err != nil { 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 s.idleSince = 0
} }
if actual != current { if actual != current {
logf("%s %s (PID %d) RSS=%s, %s -> %s", a.emitChange(hasGame, target, fmt.Sprintf("%s (PID %d) RSS=%s, %s -> %s",
actionName(hasGame, target), s.info.Name, s.info.PID, s.info.Name, s.info.PID,
formatMemSize(s.info.RSS), prioName(current), prioName(actual)) formatMemSize(s.info.RSS), prioName(current), prioName(actual)))
} }
if actual != target { if actual != target {
a.failed(s, target, fmt.Errorf("Windows установила %s вместо %s", prioName(actual), prioName(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() { func (a *app) scan() {
a.buffering = true
a.triggerOutcomes = nil
defer a.flushScan()
entries, err := allProcs() entries, err := allProcs()
if err != nil { if err != nil {
logf("process scan error: %v", err) logf("process scan error: %v", err)
@@ -619,7 +803,7 @@ func (a *app) scan() {
} }
info, err := a.inspect(entry) info, err := a.inspect(entry)
if err != nil { 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 a.unreadable[entry.PID] = entry
continue continue
} }
@@ -669,10 +853,11 @@ func (a *app) scan() {
} }
a.adoptInherited(live) a.adoptInherited(live)
newGames := make(map[processKey]bool)
for key, s := range a.states { for key, s := range a.states {
if live[key] && a.gameMem > 0 && s.info.RSS >= a.gameMem && !s.game { if live[key] && a.gameMem > 0 && s.info.RSS >= a.gameMem && !s.game {
s.game = true 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 hasGame := false
@@ -682,26 +867,24 @@ func (a *app) scan() {
break break
} }
} }
if hasGame && !a.gameMode { enteringGame := hasGame && !a.gameMode
logf("GAME MODE ON") if enteringGame {
a.offAnnounced = false
} }
if !hasGame && a.gameMode { if !hasGame && a.gameMode {
a.restoreAll(fresh) a.restoreAll(fresh)
if !a.anyOverlay() { if !a.anyOverlay() {
a.gameMode = false a.gameMode = false
logf("GAME MODE OFF") if !a.offAnnounced {
a.gameHead = "GAME MODE OFF — восстановление"
a.offAnnounced = true
}
} else { } else {
return return
} }
} }
a.gameMode = hasGame 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() { for _, s := range a.ordered() {
if !hasGame && s.overlay { if !hasGame && s.overlay {
a.restoreOverlay(s) a.restoreOverlay(s)
@@ -713,22 +896,24 @@ func (a *app) scan() {
if target == 0 { if target == 0 {
continue continue
} }
if newGames[s.info.Key] {
a.handleTrigger(s, target, hasGame)
continue
}
if target == s.info.Prio { 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 continue
} }
kind := normalChange kind := normalChange
if hasGame { if hasGame {
kind = gameChange kind = gameChange
} }
if !a.change(s, target, kind, hasGame) && gameSeen[s.info.Key] { a.change(s, target, kind, hasGame)
logf("GAME %s (PID %d) в блоклисте, режим включён", s.info.Name, s.info.PID) }
delete(gameSeen, s.info.Key) if enteringGame {
} else if gameSeen[s.info.Key] && s.info.Prio == target { if len(a.triggerOutcomes) > 0 {
delete(gameSeen, s.info.Key) a.gameHead = "GAME MODE ON — " + strings.Join(a.triggerOutcomes, "; ")
} else {
a.gameHead = "GAME MODE ON"
} }
} }
a.releaseUnused() a.releaseUnused()
@@ -899,6 +1084,9 @@ func main() {
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05")) fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
fmt.Fprintf(logFile, format+"\n", v...) 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)", logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun) formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)