421 lines
11 KiB
Go
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
|
|
}
|