1140 lines
28 KiB
Go
1140 lines
28 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
CreateToolhelp32SnapshotProcess = 0x00000002
|
|
ProcessSetInformation = 0x00000200
|
|
ProcessQueryLimitedInformation = 0x00001000
|
|
processSynchronize = 0x00100000
|
|
|
|
PriorityClassIdle = 0x00000040
|
|
PriorityClassNormal = 0x00000020
|
|
PriorityClassHigh = 0x00000080
|
|
PriorityClassAboveNormal = 0x00008000
|
|
PriorityClassRealtime = 0x00000100
|
|
PriorityClassBelowNormal = 0x00004000
|
|
|
|
ErrorNoMoreFiles = 18
|
|
ErrorAlreadyExists = 183
|
|
)
|
|
|
|
type processEntry32 struct {
|
|
Size uint32
|
|
CntUsage uint32
|
|
PID uint32
|
|
DefaultHeapID uintptr
|
|
ModuleID uint32
|
|
CntThreads uint32
|
|
ParentPID uint32
|
|
PrioClass int32
|
|
Flags uint32
|
|
ExeFile [260]uint16
|
|
}
|
|
|
|
// Порядок PagefileUsage/PeakPagefileUsage соответствует ABI Windows.
|
|
type processMemoryCounters struct {
|
|
CBM uint32
|
|
PageFaultCount uint32
|
|
PeakWorkingSetSize uintptr
|
|
WorkingSetSize uintptr
|
|
QuotaPeakPagedPoolUsage uintptr
|
|
QuotaPagedPoolUsage uintptr
|
|
QuotaPeakNonPagedPoolUsage uintptr
|
|
QuotaNonPagedPoolUsage uintptr
|
|
PagefileUsage uintptr
|
|
PeakPagefileUsage uintptr
|
|
PrivateUsage uintptr
|
|
}
|
|
|
|
type processKey struct {
|
|
PID uint32
|
|
Created uint64
|
|
}
|
|
|
|
type procInfo struct {
|
|
PID uint32
|
|
Name string
|
|
RSS uint64
|
|
Key processKey
|
|
Prio uint32
|
|
Handle syscall.Handle
|
|
Waitable bool
|
|
ParentPID uint32
|
|
}
|
|
|
|
type processState struct {
|
|
info procInfo
|
|
original, last uint32
|
|
idleSince uint64
|
|
changed, game, overlay bool
|
|
gameBase uint32
|
|
}
|
|
|
|
type changeKind uint8
|
|
|
|
const (
|
|
normalChange changeKind = iota
|
|
gameChange
|
|
restoringChange
|
|
)
|
|
|
|
var (
|
|
k32 = syscall.NewLazyDLL("kernel32.dll")
|
|
|
|
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
|
|
procProcess32First = k32.NewProc("Process32FirstW")
|
|
procProcess32Next = k32.NewProc("Process32NextW")
|
|
procGetMemInfo = k32.NewProc("K32GetProcessMemoryInfo")
|
|
procSetPriority = k32.NewProc("SetPriorityClass")
|
|
procGetPriority = k32.NewProc("GetPriorityClass")
|
|
procOpenProcess = k32.NewProc("OpenProcess")
|
|
procCloseHandle = k32.NewProc("CloseHandle")
|
|
procGetProcessTimes = k32.NewProc("GetProcessTimes")
|
|
procQueryImageName = k32.NewProc("QueryFullProcessImageNameW")
|
|
procCreateMutex = k32.NewProc("CreateMutexW")
|
|
procWaitSingle = k32.NewProc("WaitForSingleObject")
|
|
)
|
|
|
|
var logf func(string, ...any)
|
|
|
|
var logRaw func(string)
|
|
|
|
func closeH(h syscall.Handle) {
|
|
_, _, _ = procCloseHandle.Call(uintptr(h))
|
|
}
|
|
|
|
func openProc(pid uint32, acc uint32) (syscall.Handle, error) {
|
|
r, _, e := procOpenProcess.Call(uintptr(acc), 0, uintptr(pid))
|
|
if r == 0 {
|
|
return 0, fmt.Errorf("OpenProcess(%d) failed: %w", pid, e)
|
|
}
|
|
return syscall.Handle(r), nil
|
|
}
|
|
|
|
func setPrio(h syscall.Handle, pid uint32, cls uint32) error {
|
|
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
|
|
if r == 0 {
|
|
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func processCreated(h syscall.Handle) (uint64, error) {
|
|
var created, exited, kernel, user syscall.Filetime
|
|
r, _, e := procGetProcessTimes.Call(
|
|
uintptr(h),
|
|
uintptr(unsafe.Pointer(&created)),
|
|
uintptr(unsafe.Pointer(&exited)),
|
|
uintptr(unsafe.Pointer(&kernel)),
|
|
uintptr(unsafe.Pointer(&user)),
|
|
)
|
|
if r == 0 {
|
|
return 0, fmt.Errorf("GetProcessTimes failed: %w", e)
|
|
}
|
|
return uint64(created.HighDateTime)<<32 | uint64(created.LowDateTime), nil
|
|
}
|
|
|
|
func processName(h syscall.Handle, buf []uint16) (string, error) {
|
|
size := uint32(len(buf))
|
|
r, _, e := procQueryImageName.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)))
|
|
if r == 0 {
|
|
return "", fmt.Errorf("QueryFullProcessImageName failed: %w", e)
|
|
}
|
|
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
|
|
}
|
|
|
|
func decodeWaitResult(r uintptr, err error) (bool, error) {
|
|
switch r {
|
|
case 0:
|
|
return false, nil
|
|
case 0x102:
|
|
return true, nil
|
|
default:
|
|
return false, fmt.Errorf("WaitForSingleObject returned %#x: %v", r, err)
|
|
}
|
|
}
|
|
|
|
func processAlive(h syscall.Handle) (bool, error) {
|
|
r, _, err := procWaitSingle.Call(uintptr(h), 0)
|
|
return decodeWaitResult(r, err)
|
|
}
|
|
|
|
func parseMemSize(s string) (uint64, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0, fmt.Errorf("empty value")
|
|
}
|
|
multiplier := uint64(1)
|
|
switch s[len(s)-1] {
|
|
case 'k', 'K':
|
|
multiplier = 1024
|
|
s = s[:len(s)-1]
|
|
case 'm', 'M':
|
|
multiplier = 1024 * 1024
|
|
s = s[:len(s)-1]
|
|
case 'g', 'G':
|
|
multiplier = 1024 * 1024 * 1024
|
|
s = s[:len(s)-1]
|
|
}
|
|
n, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if n > math.MaxUint64/multiplier {
|
|
return 0, fmt.Errorf("value overflows uint64")
|
|
}
|
|
return n * multiplier, nil
|
|
}
|
|
|
|
func formatMemSize(b uint64) string {
|
|
if b >= 1024*1024*1024 {
|
|
return fmt.Sprintf("%.1fGB", float64(b)/(1024*1024*1024))
|
|
}
|
|
if b >= 1024*1024 {
|
|
return fmt.Sprintf("%dMB", b/1024/1024)
|
|
}
|
|
return fmt.Sprintf("%dKB", b/1024)
|
|
}
|
|
|
|
func isAboveNormal(c uint32) bool {
|
|
return c == PriorityClassAboveNormal ||
|
|
c == PriorityClassHigh ||
|
|
c == PriorityClassRealtime
|
|
}
|
|
|
|
func desiredPriority(rss, mem uint64, current uint32, gameMode, isGame bool) uint32 {
|
|
if gameMode {
|
|
if isGame {
|
|
return PriorityClassHigh
|
|
}
|
|
return PriorityClassIdle
|
|
}
|
|
if rss >= mem {
|
|
return PriorityClassHigh
|
|
}
|
|
if isAboveNormal(current) {
|
|
return PriorityClassNormal
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func prioName(c uint32) string {
|
|
switch c {
|
|
case PriorityClassIdle:
|
|
return "IDLE"
|
|
case PriorityClassBelowNormal:
|
|
return "BELOW_NORMAL"
|
|
case PriorityClassNormal:
|
|
return "NORMAL"
|
|
case PriorityClassAboveNormal:
|
|
return "ABOVE_NORMAL"
|
|
case PriorityClassHigh:
|
|
return "HIGH"
|
|
case PriorityClassRealtime:
|
|
return "REALTIME"
|
|
default:
|
|
return fmt.Sprintf("0x%X", c)
|
|
}
|
|
}
|
|
|
|
func allProcs() ([]procInfo, error) {
|
|
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
|
|
if snap == ^uintptr(0) {
|
|
return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e)
|
|
}
|
|
defer closeH(syscall.Handle(snap))
|
|
|
|
pe := processEntry32{Size: uint32(unsafe.Sizeof(processEntry32{}))}
|
|
r, _, e := procProcess32First.Call(snap, uintptr(unsafe.Pointer(&pe)))
|
|
if r == 0 {
|
|
if e == syscall.Errno(ErrorNoMoreFiles) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("Process32First failed: %w", e)
|
|
}
|
|
|
|
var out []procInfo
|
|
for {
|
|
name := syscall.UTF16ToString(pe.ExeFile[:])
|
|
out = append(out, procInfo{PID: pe.PID, ParentPID: pe.ParentPID, Name: name})
|
|
pe.Size = uint32(unsafe.Sizeof(processEntry32{}))
|
|
r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe)))
|
|
if r == 0 {
|
|
if e == syscall.Errno(ErrorNoMoreFiles) {
|
|
break
|
|
}
|
|
return nil, fmt.Errorf("Process32Next failed: %w", e)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func singleInstance() (syscall.Handle, bool, error) {
|
|
name, err := syscall.UTF16PtrFromString(`Local\autoPriority`)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
r, _, e := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
|
|
if r == 0 {
|
|
return 0, false, fmt.Errorf("CreateMutex failed: %w", e)
|
|
}
|
|
return syscall.Handle(r), e == syscall.Errno(ErrorAlreadyExists), nil
|
|
}
|
|
|
|
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
|
|
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 {
|
|
return &app{
|
|
mem: mem, gameMem: gameMem, dryRun: dryRun,
|
|
self: uint32(os.Getpid()),
|
|
states: make(map[processKey]*processState),
|
|
blocked: make(map[processKey]bool),
|
|
unreadable: make(map[uint32]procInfo),
|
|
}
|
|
}
|
|
|
|
func (a *app) ordered() []*processState {
|
|
list := make([]*processState, 0, len(a.states))
|
|
for _, s := range a.states {
|
|
list = append(list, s)
|
|
}
|
|
sort.Slice(list, func(i, j int) bool {
|
|
x, y := list[i], list[j]
|
|
if x.game != y.game {
|
|
return x.game
|
|
}
|
|
if x.info.PID != y.info.PID {
|
|
return x.info.PID < y.info.PID
|
|
}
|
|
return x.info.Key.Created < y.info.Key.Created
|
|
})
|
|
return list
|
|
}
|
|
|
|
func (a *app) getPriority(p *procInfo) (uint32, error) {
|
|
r, _, err := procGetPriority.Call(uintptr(p.Handle))
|
|
if r == 0 {
|
|
return 0, fmt.Errorf("GetPriorityClass: %w", err)
|
|
}
|
|
return uint32(r), nil
|
|
}
|
|
|
|
// setPriority открывает процесс только для записи и проверяет,
|
|
// что PID не занят другим экземпляром.
|
|
func (a *app) setPriority(p *procInfo, target uint32) error {
|
|
h, err := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeH(h)
|
|
created, err := processCreated(h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if created != p.Key.Created {
|
|
return fmt.Errorf("экземпляр процесса сменился; запись отклонена")
|
|
}
|
|
return setPrio(h, p.PID, target)
|
|
}
|
|
|
|
func (a *app) closeInfo(p *procInfo) {
|
|
if p.Handle != 0 {
|
|
closeH(p.Handle)
|
|
p.Handle = 0
|
|
}
|
|
}
|
|
|
|
func (a *app) inspect(entry procInfo) (procInfo, error) {
|
|
h, err := openProc(entry.PID, ProcessQueryLimitedInformation|processSynchronize)
|
|
entry.Waitable = err == nil
|
|
if err != nil {
|
|
h, err = openProc(entry.PID, ProcessQueryLimitedInformation)
|
|
}
|
|
if err != nil {
|
|
return procInfo{}, err
|
|
}
|
|
good := false
|
|
defer func() {
|
|
if !good {
|
|
closeH(h)
|
|
}
|
|
}()
|
|
created, err := processCreated(h)
|
|
if err != nil {
|
|
return procInfo{}, err
|
|
}
|
|
if name, e := processName(h, a.nameBuf[:]); e == nil {
|
|
entry.Name = name
|
|
}
|
|
entry.Handle = h
|
|
entry.Key = processKey{PID: entry.PID, Created: created}
|
|
var m processMemoryCounters
|
|
m.CBM = uint32(unsafe.Sizeof(m))
|
|
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
|
|
if r == 0 {
|
|
return procInfo{}, fmt.Errorf("K32GetProcessMemoryInfo: %w", e)
|
|
}
|
|
entry.RSS = uint64(m.WorkingSetSize)
|
|
entry.Prio, err = a.getPriority(&entry)
|
|
if err != nil {
|
|
return procInfo{}, err
|
|
}
|
|
good = true
|
|
return entry, nil
|
|
}
|
|
|
|
func (a *app) alive(s *processState, pids map[uint32]bool) (bool, error) {
|
|
if pids != nil {
|
|
if !pids[s.info.PID] {
|
|
return false, nil
|
|
}
|
|
for i := range a.lastScan {
|
|
p := &a.lastScan[i]
|
|
if p.PID == s.info.PID && p.Key != s.info.Key {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
if s.info.Waitable {
|
|
return processAlive(s.info.Handle)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *processState) observe(current uint32) {
|
|
if (s.changed || s.overlay) && current != s.last {
|
|
// Чужое изменение становится новой исходной точкой.
|
|
s.changed = false
|
|
s.overlay = false
|
|
s.idleSince = 0
|
|
}
|
|
s.info.Prio = current
|
|
if !s.changed && !s.overlay {
|
|
s.original = current
|
|
}
|
|
}
|
|
|
|
func filetimeNow() uint64 {
|
|
t := time.Now()
|
|
return uint64(t.Unix())*10000000 + uint64(t.Nanosecond()/100) + 116444736000000000
|
|
}
|
|
|
|
func inheritedClass(parent uint32) uint32 {
|
|
if parent == PriorityClassIdle || parent == PriorityClassBelowNormal {
|
|
return parent
|
|
}
|
|
return PriorityClassNormal
|
|
}
|
|
|
|
func (a *app) failed(s *processState, target uint32, err error) {
|
|
if a.blocked[s.info.Key] {
|
|
return
|
|
}
|
|
a.blocked[s.info.Key] = true
|
|
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("ОШИБКА ЗАПИСИ: %s (без повторов)", detail)
|
|
}
|
|
|
|
func actionName(hasGame bool, target uint32) string {
|
|
if hasGame {
|
|
return "ИГРОВОЙ РЕЖИМ"
|
|
}
|
|
if target == PriorityClassHigh {
|
|
return "ПРИОРИТЕТ ПОВЫШЕН"
|
|
}
|
|
return "ПРИОРИТЕТ ПОНИЖЕН"
|
|
}
|
|
|
|
func (a *app) change(s *processState, target uint32, kind changeKind, hasGame bool) bool {
|
|
if a.blocked[s.info.Key] {
|
|
return false
|
|
}
|
|
current, err := a.getPriority(&s.info)
|
|
if err != nil {
|
|
a.failed(s, target, err)
|
|
return false
|
|
}
|
|
s.observe(current)
|
|
if kind == normalChange && desiredPriority(s.info.RSS, a.mem, current, false, false) == 0 {
|
|
return true
|
|
}
|
|
if kind == restoringChange && !s.changed && !s.overlay {
|
|
return true
|
|
}
|
|
if current == target {
|
|
return true
|
|
}
|
|
if a.dryRun {
|
|
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(target)))
|
|
return true
|
|
}
|
|
if err = a.setPriority(&s.info, target); err != nil {
|
|
a.failed(s, target, err)
|
|
return false
|
|
}
|
|
if !s.changed {
|
|
s.original = current
|
|
}
|
|
if kind == gameChange && !s.overlay {
|
|
s.overlay = true
|
|
s.gameBase = current
|
|
}
|
|
s.changed = true
|
|
s.last = target
|
|
s.info.Prio = target
|
|
if target == PriorityClassIdle {
|
|
s.idleSince = filetimeNow()
|
|
} else {
|
|
s.idleSince = 0
|
|
}
|
|
actual, err := a.getPriority(&s.info)
|
|
if err != nil {
|
|
a.failed(s, target, fmt.Errorf("изменение выполнено, проверка не удалась: %w", err))
|
|
return false
|
|
}
|
|
s.last = actual
|
|
s.info.Prio = actual
|
|
s.changed = actual != s.original
|
|
if actual != PriorityClassIdle {
|
|
s.idleSince = 0
|
|
}
|
|
if actual != current {
|
|
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)))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (a *app) restoreOverlay(s *processState) bool {
|
|
if a.blocked[s.info.Key] {
|
|
return false
|
|
}
|
|
current, err := a.getPriority(&s.info)
|
|
if err != nil {
|
|
a.failed(s, s.gameBase, err)
|
|
return false
|
|
}
|
|
s.observe(current)
|
|
if !s.overlay {
|
|
return true
|
|
}
|
|
if !a.change(s, s.gameBase, restoringChange, false) {
|
|
return false
|
|
}
|
|
s.overlay = false
|
|
return true
|
|
}
|
|
|
|
func (a *app) adoptInherited(fresh map[processKey]bool) {
|
|
if a.dryRun {
|
|
return
|
|
}
|
|
byPID := make(map[uint32]*processState, len(a.states))
|
|
for _, s := range a.states {
|
|
byPID[s.info.PID] = s
|
|
}
|
|
for pass := 0; pass < len(a.states); pass++ {
|
|
adopted := false
|
|
for _, s := range a.states {
|
|
if !fresh[s.info.Key] || s.changed || s.overlay || s.game || s.info.Prio != PriorityClassIdle {
|
|
continue
|
|
}
|
|
parent := byPID[s.info.ParentPID]
|
|
if parent == nil || parent == s || !fresh[parent.info.Key] || (!parent.changed && !parent.overlay) || parent.last != PriorityClassIdle || parent.info.Prio != PriorityClassIdle || parent.idleSince == 0 {
|
|
continue
|
|
}
|
|
if s.info.Key.Created < parent.idleSince || s.info.Key.Created <= parent.info.Key.Created {
|
|
continue
|
|
}
|
|
s.original = inheritedClass(parent.original)
|
|
s.last = PriorityClassIdle
|
|
s.changed = s.original != s.last
|
|
s.overlay = parent.overlay
|
|
s.gameBase = inheritedClass(parent.gameBase)
|
|
s.idleSince = s.info.Key.Created
|
|
adopted = adopted || s.changed || s.overlay
|
|
}
|
|
if !adopted {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *app) releaseUnused() {
|
|
for key, s := range a.states {
|
|
if !s.game && !s.changed && !s.overlay {
|
|
a.closeInfo(&s.info)
|
|
delete(a.states, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
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("ПРИОРИТЕТ ПОВЫШЕН: %s", detail)
|
|
}
|
|
|
|
func (a *app) emitDemote(detail string) {
|
|
detail = cleanDetail(detail)
|
|
if a.buffering {
|
|
a.bufDemote = append(a.bufDemote, detail)
|
|
return
|
|
}
|
|
logf("ПРИОРИТЕТ ПОНИЖЕН: %s", detail)
|
|
}
|
|
|
|
func (a *app) emitSkip(detail string) {
|
|
detail = cleanDetail(detail)
|
|
if a.buffering {
|
|
a.bufSkip = append(a.bufSkip, detail)
|
|
return
|
|
}
|
|
logf("ОШИБКА ЧТЕНИЯ: %s (без повторов)", detail)
|
|
}
|
|
|
|
func (a *app) emitBlock(detail string) {
|
|
detail = cleanDetail(detail)
|
|
if a.buffering {
|
|
a.bufBlock = append(a.bufBlock, detail)
|
|
return
|
|
}
|
|
logf("ОШИБКА ЗАПИСИ: %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 выводит события одного скана одним боксом.
|
|
// Порядок групп строгий: повышение -> понижение -> ошибки чтения -> ошибки записи.
|
|
type logGroup struct {
|
|
name string
|
|
rows []string
|
|
}
|
|
|
|
func renderBox(head string, groups []logGroup) {
|
|
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")
|
|
base := "┌─ " + ts + " "
|
|
if width < runeCount(base) {
|
|
width = runeCount(base)
|
|
}
|
|
// Закрытый бокс: правая стенка у каждой строки.
|
|
logRaw(base + strings.Repeat("─", width-runeCount(base)) + "┐")
|
|
for _, line := range lines {
|
|
logRaw(line + strings.Repeat(" ", width-runeCount(line)) + "│")
|
|
}
|
|
logRaw("└" + strings.Repeat("─", width-1) + "┘")
|
|
logRaw("")
|
|
}
|
|
|
|
func (a *app) flushScan() {
|
|
a.buffering = false
|
|
head := a.gameHead
|
|
a.gameHead = ""
|
|
groups := []logGroup{
|
|
{"ПРИОРИТЕТ ПОВЫШЕН", a.bufPromote},
|
|
{"ПРИОРИТЕТ ПОНИЖЕН", a.bufDemote},
|
|
{"ОШИБКА ЧТЕНИЯ", a.bufSkip},
|
|
{"ОШИБКА ЗАПИСИ", a.bufBlock},
|
|
}
|
|
a.bufPromote, a.bufDemote, a.bufSkip, a.bufBlock = nil, nil, nil, nil
|
|
renderBox(head, groups)
|
|
}
|
|
|
|
// 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("ошибка снимка процессов: %v", err)
|
|
return
|
|
}
|
|
pids := make(map[uint32]bool, len(entries))
|
|
for _, e := range entries {
|
|
pids[e.PID] = true
|
|
}
|
|
var fresh []procInfo
|
|
for _, entry := range entries {
|
|
if entry.PID == 0 || entry.PID == a.self {
|
|
continue
|
|
}
|
|
if b, ok := a.unreadable[entry.PID]; ok && b.Name == entry.Name && b.ParentPID == entry.ParentPID {
|
|
continue
|
|
}
|
|
info, err := a.inspect(entry)
|
|
if err != nil {
|
|
a.emitSkip(fmt.Sprintf("%s (PID %d): %v", entry.Name, entry.PID, err))
|
|
a.unreadable[entry.PID] = entry
|
|
continue
|
|
}
|
|
delete(a.unreadable, entry.PID)
|
|
fresh = append(fresh, info)
|
|
}
|
|
for pid := range a.unreadable {
|
|
if !pids[pid] {
|
|
delete(a.unreadable, pid)
|
|
}
|
|
}
|
|
a.lastScan = fresh
|
|
|
|
known := make(map[uint32]processKey, len(fresh))
|
|
for _, p := range fresh {
|
|
known[p.PID] = p.Key
|
|
}
|
|
for key := range a.blocked {
|
|
cur, ok := known[key.PID]
|
|
if !pids[key.PID] || (ok && cur != key) {
|
|
delete(a.blocked, key)
|
|
}
|
|
}
|
|
live := make(map[processKey]bool, len(fresh))
|
|
for key, s := range a.states {
|
|
ok, err := a.alive(s, pids)
|
|
if err != nil {
|
|
a.failed(s, s.info.Prio, fmt.Errorf("статус процесса: %w", err))
|
|
}
|
|
if err == nil && !ok {
|
|
a.closeInfo(&s.info)
|
|
delete(a.states, key)
|
|
}
|
|
}
|
|
for i := range fresh {
|
|
p := &fresh[i]
|
|
live[p.Key] = true
|
|
if s := a.states[p.Key]; s != nil {
|
|
s.info.Name = p.Name
|
|
s.info.RSS = p.RSS
|
|
s.info.ParentPID = p.ParentPID
|
|
s.observe(p.Prio)
|
|
a.closeInfo(p)
|
|
} else {
|
|
a.states[p.Key] = &processState{info: *p, original: p.Prio}
|
|
}
|
|
}
|
|
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
|
|
newGames[key] = true
|
|
}
|
|
}
|
|
hasGame := false
|
|
for _, s := range a.states {
|
|
if s.game {
|
|
hasGame = true
|
|
break
|
|
}
|
|
}
|
|
enteringGame := hasGame && !a.gameMode
|
|
if enteringGame {
|
|
a.offAnnounced = false
|
|
}
|
|
if !hasGame && a.gameMode {
|
|
a.restoreAll(fresh)
|
|
if !a.anyOverlay() {
|
|
a.gameMode = false
|
|
if !a.offAnnounced {
|
|
a.gameHead = "ИГРОВОЙ РЕЖИМ ВЫКЛЮЧЁН — восстановление"
|
|
a.offAnnounced = true
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
a.gameMode = hasGame
|
|
|
|
for _, s := range a.ordered() {
|
|
if !hasGame && s.overlay {
|
|
a.restoreOverlay(s)
|
|
}
|
|
if !live[s.info.Key] {
|
|
continue
|
|
}
|
|
target := desiredPriority(s.info.RSS, a.mem, s.info.Prio, hasGame, s.game)
|
|
if target == 0 {
|
|
continue
|
|
}
|
|
if newGames[s.info.Key] {
|
|
a.handleTrigger(s, target, hasGame)
|
|
continue
|
|
}
|
|
if target == s.info.Prio {
|
|
continue
|
|
}
|
|
kind := normalChange
|
|
if hasGame {
|
|
kind = gameChange
|
|
}
|
|
a.change(s, target, kind, hasGame)
|
|
}
|
|
if enteringGame {
|
|
if len(a.triggerOutcomes) > 0 {
|
|
a.gameHead = "ИГРОВОЙ РЕЖИМ ВКЛЮЧЁН — " + strings.Join(a.triggerOutcomes, "; ")
|
|
} else {
|
|
a.gameHead = "ИГРОВОЙ РЕЖИМ ВКЛЮЧЁН"
|
|
}
|
|
}
|
|
a.releaseUnused()
|
|
}
|
|
|
|
func (a *app) anyOverlay() bool {
|
|
for _, s := range a.states {
|
|
if s.overlay {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *app) restoreAll(fresh []procInfo) {
|
|
current := make(map[processKey]*procInfo, len(fresh))
|
|
for i := range fresh {
|
|
current[fresh[i].Key] = &fresh[i]
|
|
}
|
|
for _, s := range a.ordered() {
|
|
if !s.overlay {
|
|
continue
|
|
}
|
|
if !a.restoreOverlay(s) {
|
|
continue
|
|
}
|
|
s.overlay = false
|
|
if p := current[s.info.Key]; p != nil {
|
|
p.Prio = s.gameBase
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *app) shutdown() {
|
|
if !a.dryRun && len(a.states) > 0 {
|
|
entries, err := allProcs()
|
|
if err == nil {
|
|
var fresh []procInfo
|
|
for _, entry := range entries {
|
|
if entry.PID == 0 || entry.PID == a.self {
|
|
continue
|
|
}
|
|
if b, ok := a.unreadable[entry.PID]; ok && b.Name == entry.Name && b.ParentPID == entry.ParentPID {
|
|
continue
|
|
}
|
|
info, err := a.inspect(entry)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
fresh = append(fresh, info)
|
|
}
|
|
a.lastScan = fresh
|
|
live := make(map[processKey]bool, len(fresh))
|
|
for i := range fresh {
|
|
p := &fresh[i]
|
|
live[p.Key] = true
|
|
if s := a.states[p.Key]; s != nil {
|
|
s.info.Name = p.Name
|
|
s.info.RSS = p.RSS
|
|
s.info.ParentPID = p.ParentPID
|
|
s.observe(p.Prio)
|
|
a.closeInfo(p)
|
|
} else {
|
|
a.states[p.Key] = &processState{info: *p, original: p.Prio}
|
|
}
|
|
}
|
|
a.adoptInherited(live)
|
|
} else {
|
|
logf("ошибка финального снимка: %v", err)
|
|
}
|
|
unrestored := 0
|
|
for _, s := range a.ordered() {
|
|
if !s.changed || a.blocked[s.info.Key] {
|
|
if s.changed {
|
|
unrestored++
|
|
}
|
|
continue
|
|
}
|
|
ok, err := a.alive(s, nil)
|
|
if err == nil && !ok {
|
|
s.changed = false
|
|
continue
|
|
}
|
|
if err != nil {
|
|
a.failed(s, s.original, err)
|
|
unrestored++
|
|
continue
|
|
}
|
|
current, err := a.getPriority(&s.info)
|
|
if err != nil {
|
|
a.failed(s, s.original, err)
|
|
unrestored++
|
|
continue
|
|
}
|
|
s.observe(current)
|
|
if !s.changed {
|
|
continue
|
|
}
|
|
if a.change(s, s.original, restoringChange, false) {
|
|
s.changed = false
|
|
s.overlay = false
|
|
} else {
|
|
unrestored++
|
|
}
|
|
}
|
|
if unrestored > 0 {
|
|
logf("предупреждение: не восстановлены приоритеты %d процессов", unrestored)
|
|
}
|
|
}
|
|
for _, s := range a.states {
|
|
a.closeInfo(&s.info)
|
|
}
|
|
a.gameMode = false
|
|
}
|
|
|
|
func main() {
|
|
memStr := flag.String("mem", "512M", "memory threshold (e.g. 512M, 1G)")
|
|
gameMemStr := flag.String("game-mem", "2G", "heavy-process threshold; greater than -mem, 0 = disabled")
|
|
interval := flag.Duration("interval", time.Minute, "scan interval")
|
|
dryRun := flag.Bool("dry-run", false, "log only, do not change priorities")
|
|
flag.Parse()
|
|
|
|
if *interval < 10*time.Second {
|
|
*interval = 10 * time.Second
|
|
}
|
|
|
|
mem, err := parseMemSize(*memStr)
|
|
if err != nil || mem == 0 {
|
|
if err == nil {
|
|
err = fmt.Errorf("must be greater than zero")
|
|
}
|
|
fmt.Fprintf(os.Stderr, "autoPriority: invalid -mem value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
gameMem, err := parseMemSize(*gameMemStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: invalid -game-mem value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if gameMem > 0 && gameMem <= mem {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: -game-mem must be greater than -mem\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
mutex, exists, err := singleInstance()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if exists {
|
|
closeH(mutex)
|
|
fmt.Fprintln(os.Stderr, "autoPriority: already running")
|
|
return
|
|
}
|
|
defer closeH(mutex)
|
|
|
|
// Журнал очищается при каждом запуске.
|
|
logPath := filepath.Join(os.TempDir(), "autopriority.log")
|
|
logFile, err := os.Create(logPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: cannot create log: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer logFile.Close()
|
|
|
|
logf = func(format string, v ...any) {
|
|
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)
|
|
}
|
|
|
|
startRows := []string{fmt.Sprintf("запущен (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
|
|
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)}
|
|
|
|
if !*dryRun {
|
|
pid := uint32(os.Getpid())
|
|
h, err := openProc(pid, ProcessSetInformation)
|
|
if err == nil {
|
|
err = setPrio(h, pid, PriorityClassIdle)
|
|
closeH(h)
|
|
}
|
|
if err != nil {
|
|
startRows = append(startRows, fmt.Sprintf("предупреждение: не удалось установить собственный приоритет IDLE: %v", err))
|
|
} else {
|
|
startRows = append(startRows, "собственный приоритет: IDLE")
|
|
}
|
|
}
|
|
renderBox("", []logGroup{{"ЗАПУСК", startRows}})
|
|
|
|
a := newApp(mem, gameMem, *dryRun)
|
|
|
|
defer func() {
|
|
a.shutdown()
|
|
logf("autoPriority остановлен")
|
|
logFile.Sync()
|
|
}()
|
|
|
|
ticker := time.NewTicker(*interval)
|
|
defer ticker.Stop()
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
|
|
|
a.scan()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
logf("получен сигнал завершения")
|
|
return
|
|
case <-ticker.C:
|
|
a.scan()
|
|
}
|
|
}
|
|
}
|