//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() } } }