Files
autoPriority/controller.go
T

421 lines
11 KiB
Go

//go:build windows
package main
import (
"fmt"
"sort"
"time"
)
type processError struct {
Name string
PID uint32
Description string
}
type processSnapshot struct {
Processes []procInfo
// Complete PID membership, including processes whose metadata was inaccessible.
PIDs map[uint32]bool
Errors []processError
}
type processBackend interface {
Snapshot() (processSnapshot, error)
Alive(*procInfo) (bool, error)
Priority(*procInfo) (uint32, error)
SetPriority(*procInfo, uint32) error
Close(*procInfo)
}
type priorityChange struct {
Name string
PID uint32
RSS uint64
Before, After uint32
Heavy, Unchanged, Blocked bool
}
type scanReport struct {
Title string
Rows []priorityChange
Errors []processError
DryRun bool
Unrestored int
}
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
)
type controller struct {
api processBackend
mem, gameMem uint64
dryRun, gameMode bool
states map[processKey]*processState
now func() time.Time
blocked map[processKey]bool
out scanReport
}
func newController(api processBackend, mem, gameMem uint64, dryRun bool, _ time.Duration) *controller {
return &controller{api: api, mem: mem, gameMem: gameMem, dryRun: dryRun, states: make(map[processKey]*processState), blocked: make(map[processKey]bool), now: time.Now}
}
func (c *controller) ordered() []*processState {
list := make([]*processState, 0, len(c.states))
for _, s := range c.states {
list = append(list, s)
}
sort.Slice(list, func(i, j int) bool {
a, b := list[i], list[j]
if a.game != b.game {
return a.game
}
if a.info.PID != b.info.PID {
return a.info.PID < b.info.PID
}
return a.info.Key.Created < b.info.Key.Created
})
return list
}
func (c *controller) alive(s *processState, snapshot *processSnapshot) (bool, error) {
if snapshot != nil {
if !snapshot.PIDs[s.info.PID] {
return false, nil
}
for _, p := range snapshot.Processes {
if p.PID == s.info.PID && p.Key != s.info.Key {
return false, nil
}
}
}
if s.info.Waitable {
return c.api.Alive(&s.info)
}
// Without SYNCHRONIZE, only absence from a complete snapshot proves exit.
// Retained handles still bind all restoration writes to the original instance.
return true, nil
}
func (s *processState) observe(current uint32) {
if (s.changed || s.overlay) && current != s.last {
// Do not undo another actor's change. The next own write starts a new baseline.
s.changed = false
s.overlay = false
s.idleSince = 0
}
s.info.Prio = current
if !s.changed && !s.overlay {
s.original = current
}
}
func (c *controller) reconcile(snapshot processSnapshot) map[processKey]bool {
fresh := make(map[processKey]bool, len(snapshot.Processes))
instances := make(map[uint32]processKey, len(snapshot.Processes))
for _, p := range snapshot.Processes {
instances[p.PID] = p.Key
}
for key := range c.blocked {
current, known := instances[key.PID]
if !snapshot.PIDs[key.PID] || (known && current != key) {
delete(c.blocked, key)
}
}
for key, s := range c.states {
alive, err := c.alive(s, &snapshot)
if err != nil {
c.failed(s, s.info.Prio, fmt.Errorf("статус процесса: %w", err))
}
if err == nil && !alive {
c.api.Close(&s.info)
delete(c.states, key)
}
}
for i := range snapshot.Processes {
p := &snapshot.Processes[i]
fresh[p.Key] = true
if s := c.states[p.Key]; s != nil {
s.info.Name = p.Name
s.info.RSS = p.RSS
s.info.ParentPID = p.ParentPID
s.observe(p.Prio)
c.api.Close(p)
} else {
c.states[p.Key] = &processState{info: *p, original: p.Prio}
}
}
c.out.Errors = append(c.out.Errors, snapshot.Errors...)
return fresh
}
func (c *controller) failed(s *processState, target uint32, err error) {
if c.blocked[s.info.Key] {
return
}
c.blocked[s.info.Key] = true
c.out.Errors = append(c.out.Errors, processError{Name: s.info.Name, PID: s.info.PID, Description: fmt.Sprintf("%s: %v; внесён в блоклист", prioName(target), err)})
}
func (c *controller) change(s *processState, target uint32, kind changeKind) bool {
if c.blocked[s.info.Key] {
return false
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, target, err)
return false
}
s.observe(current)
if kind == normalChange && desiredPriority(s.info.RSS, c.mem, current, false, false) == 0 {
return true
}
if kind == restoringChange && !s.changed && !s.overlay {
return true
}
if current == target {
return true
}
row := priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: current, After: target, Heavy: s.game}
if c.dryRun {
c.out.Rows = append(c.out.Rows, row)
return true
}
stamp := filetimeValue(c.now())
if err = c.api.SetPriority(&s.info, target); err != nil {
c.failed(s, target, err)
return false
}
// Save ownership immediately after a successful write, even if verification fails.
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 = stamp
} else {
s.idleSince = 0
}
actual, err := c.api.Priority(&s.info)
if err != nil {
c.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 {
row.After = actual
c.out.Rows = append(c.out.Rows, row)
}
if actual != target {
c.failed(s, target, fmt.Errorf("Windows установила %s вместо %s", prioName(actual), prioName(target)))
return false
}
return true
}
func (c *controller) restoreOverlay(s *processState) bool {
if c.blocked[s.info.Key] {
return false
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, s.gameBase, err)
return false
}
s.observe(current)
if !s.overlay {
return true
}
if !c.change(s, s.gameBase, restoringChange) {
return false
}
s.overlay = false
return true
}
func filetimeValue(t time.Time) uint64 {
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 (c *controller) adoptInherited(fresh map[processKey]bool) {
if c.dryRun {
return
}
byPID := make(map[uint32]*processState, len(c.states))
for _, s := range c.states {
byPID[s.info.PID] = s
}
// Each successful pass adopts at least one previously unowned child. Cycles,
// reused parent PIDs and pre-existing IDLE processes cannot establish ownership.
for pass := 0; pass < len(c.states); pass++ {
adopted := false
for _, s := range c.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
// Explicit child IDLE cannot be distinguished from inherited IDLE by polling.
adopted = adopted || s.changed || s.overlay
}
if !adopted {
break
}
}
}
func (c *controller) releaseUnused() {
for key, s := range c.states {
if !s.game && !s.changed && !s.overlay {
c.api.Close(&s.info)
delete(c.states, key)
}
}
}
func (c *controller) Step() scanReport {
c.out = scanReport{DryRun: c.dryRun}
snapshot, err := c.api.Snapshot()
if err != nil {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Снимок процессов: %v", err)})
return c.out
}
fresh := c.reconcile(snapshot)
c.adoptInherited(fresh)
newGames := make(map[processKey]bool)
for key, s := range c.states {
if fresh[key] && c.gameMem > 0 && s.info.RSS >= c.gameMem && !s.game {
s.game = true
newGames[key] = true
}
}
hasGame := false
for _, s := range c.states {
hasGame = hasGame || s.game
}
if hasGame && !c.gameMode {
c.out.Title = "ИГРОВОЙ РЕЖИМ ВКЛЮЧЁН"
}
if !hasGame && c.gameMode {
c.out.Title = "ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ"
}
c.gameMode = hasGame
for _, s := range c.ordered() {
if !hasGame && s.overlay {
if !c.restoreOverlay(s) {
continue
}
if c.out.Title == "" {
c.out.Title = "ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ"
}
}
if !fresh[s.info.Key] {
continue
}
target := desiredPriority(s.info.RSS, c.mem, s.info.Prio, hasGame, s.game)
if target == 0 {
continue
}
if target == s.info.Prio {
if newGames[s.info.Key] {
c.out.Rows = append(c.out.Rows, priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: target, After: target, Heavy: true, Unchanged: true})
}
continue
}
kind := normalChange
if hasGame {
kind = gameChange
}
if !c.change(s, target, kind) && newGames[s.info.Key] {
c.out.Rows = append(c.out.Rows, priorityChange{Name: s.info.Name, PID: s.info.PID, RSS: s.info.RSS, Before: s.info.Prio, After: s.info.Prio, Heavy: true, Blocked: true})
}
}
c.releaseUnused()
if c.out.Title == "" && len(c.out.Rows) > 0 {
if hasGame {
c.out.Title = "ИГРОВОЙ РЕЖИМ"
} else {
c.out.Title = "ИЗМЕНЕНИЕ ПРИОРИТЕТОВ"
}
}
return c.out
}
func (c *controller) Shutdown() scanReport {
c.out = scanReport{Title: "ЗАВЕРШЕНИЕ: ВОССТАНОВЛЕНИЕ ПРИОРИТЕТОВ", DryRun: c.dryRun}
if !c.dryRun && len(c.states) > 0 {
snapshot, err := c.api.Snapshot()
if err == nil {
fresh := c.reconcile(snapshot)
c.adoptInherited(fresh)
} else {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Последний снимок процессов: %v", err)})
}
for _, s := range c.ordered() {
if !s.changed || c.blocked[s.info.Key] {
continue
}
alive, err := c.alive(s, nil)
if err == nil && !alive {
s.changed = false
continue
}
if err != nil {
c.failed(s, s.original, err)
continue
}
current, err := c.api.Priority(&s.info)
if err != nil {
c.failed(s, s.original, err)
continue
}
s.observe(current)
if !s.changed {
continue
}
if c.change(s, s.original, restoringChange) {
s.changed = false
s.overlay = false
}
}
}
for key, s := range c.states {
if s.changed {
c.out.Unrestored++
}
c.api.Close(&s.info)
delete(c.states, key)
}
if c.out.Unrestored > 0 {
c.out.Errors = append(c.out.Errors, processError{Description: fmt.Sprintf("Не восстановлены приоритеты %d процессов; подробности в журнале", c.out.Unrestored)})
}
c.gameMode = false
clear(c.blocked)
return c.out
}