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:
1 parent
4600cc0862
commit
e391138337
2 files changed
+241
-42
No files matched your search
@@ -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` помечает строки `(план)`.
|
||||
|
||||
## Ограничения
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user