feat: add tray control, priority restoration and session logging
This commit is contained in:
1 parent
4f8f6218ad
commit
4880aa46dc
25 files changed
+3072
-676
No files matched your search
+420
@@ -0,0 +1,420 @@
|
||||
//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
|
||||
}
|
||||
Reference in new issue
Block a user