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
|
```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` помечает строки `(план)`.
|
||||||
|
|
||||||
## Ограничения
|
## Ограничения
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user