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
@@ -3,633 +3,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
CreateToolhelp32SnapshotProcess = 0x00000002
|
||||
ProcessSetInformation = 0x00000200
|
||||
ProcessQueryLimitedInformation = 0x00001000
|
||||
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
|
||||
type options struct {
|
||||
mem, gameMem uint64
|
||||
interval time.Duration
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type processMemoryCounters struct {
|
||||
CBM uint32
|
||||
PageFaultCount uint32
|
||||
PeakWorkingSetSize uintptr
|
||||
WorkingSetSize uintptr
|
||||
QuotaPeakPagedPoolUsage uintptr
|
||||
QuotaPagedPoolUsage uintptr
|
||||
QuotaPeakNonPagedPoolUsage uintptr
|
||||
QuotaNonPagedPoolUsage uintptr
|
||||
PeakPagefileUsage uintptr
|
||||
PagefileUsage 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
|
||||
CanSet bool
|
||||
Keep bool
|
||||
}
|
||||
|
||||
type trackedProc struct {
|
||||
name string
|
||||
handle syscall.Handle
|
||||
}
|
||||
|
||||
type savedPrio struct {
|
||||
name string
|
||||
prio uint32
|
||||
handle syscall.Handle
|
||||
}
|
||||
|
||||
var (
|
||||
k32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
ps = syscall.NewLazyDLL("psapi.dll")
|
||||
|
||||
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
|
||||
procProcess32First = k32.NewProc("Process32FirstW")
|
||||
procProcess32Next = k32.NewProc("Process32NextW")
|
||||
procGetMemInfo = ps.NewProc("GetProcessMemoryInfo")
|
||||
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")
|
||||
)
|
||||
|
||||
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)
|
||||
func readOptions(args []string, output io.Writer) (options, error) {
|
||||
var o options
|
||||
fs := flag.NewFlagSet("autoPriority", flag.ContinueOnError)
|
||||
fs.SetOutput(output)
|
||||
mem := fs.String("mem", "512M", "memory threshold (e.g. 512M, 1G)")
|
||||
gameMem := fs.String("game-mem", "2G", "heavy-process threshold; greater than -mem, or 0 to disable")
|
||||
fs.DurationVar(&o.interval, "interval", time.Minute, "scan interval (minimum 10s)")
|
||||
fs.BoolVar(&o.dryRun, "dry-run", false, "log the plan without changing priorities")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return o, err
|
||||
}
|
||||
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)
|
||||
if fs.NArg() != 0 {
|
||||
return o, fmt.Errorf("unexpected positional arguments")
|
||||
}
|
||||
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)
|
||||
var err error
|
||||
o.mem, err = parseMemSize(*mem)
|
||||
if err != nil || o.mem == 0 {
|
||||
return o, fmt.Errorf("invalid -mem: use a positive integer with optional K/M/G suffix")
|
||||
}
|
||||
return uint64(created.HighDateTime)<<32 | uint64(created.LowDateTime), nil
|
||||
}
|
||||
|
||||
func processName(h syscall.Handle) (string, error) {
|
||||
buf := make([]uint16, 32768)
|
||||
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 processAlive(h syscall.Handle) (bool, 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 false, fmt.Errorf("GetProcessTimes failed: %w", e)
|
||||
}
|
||||
return exited.HighDateTime == 0 && exited.LowDateTime == 0, nil
|
||||
}
|
||||
|
||||
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)
|
||||
o.gameMem, err = parseMemSize(*gameMem)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return o, fmt.Errorf("invalid -game-mem: %w", err)
|
||||
}
|
||||
if n > math.MaxUint64/multiplier {
|
||||
return 0, fmt.Errorf("value overflows uint64")
|
||||
if o.gameMem != 0 && o.gameMem <= o.mem {
|
||||
return o, fmt.Errorf("-game-mem must be greater than -mem, or 0")
|
||||
}
|
||||
return n * multiplier, nil
|
||||
if o.interval < 10*time.Second {
|
||||
o.interval = 10 * time.Second
|
||||
}
|
||||
return o, 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
|
||||
func runController(ctx context.Context, o options, api processBackend, emit func(scanReport) error) (result error) {
|
||||
c := newController(api, o.mem, o.gameMem, o.dryRun, o.interval)
|
||||
defer func() {
|
||||
report := c.Shutdown()
|
||||
result = errors.Join(result, emit(report))
|
||||
if report.Unrestored > 0 {
|
||||
result = errors.Join(result, fmt.Errorf("не восстановлены приоритеты %d процессов; см. журнал", report.Unrestored))
|
||||
}
|
||||
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 {
|
||||
return nil, fmt.Errorf("Process32First failed: %w", e)
|
||||
}
|
||||
|
||||
var out []procInfo
|
||||
}()
|
||||
ticker := time.NewTicker(o.interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
name := syscall.UTF16ToString(pe.ExeFile[:])
|
||||
if name != "" {
|
||||
out = append(out, procInfo{PID: pe.PID, Name: name})
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
if err := emit(c.Step()); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func main() {
|
||||
memStr := flag.String("mem", "512M", "memory threshold (e.g. 512M, 1G, 2048M)")
|
||||
gameMemStr := flag.String("game-mem", "2G", "game memory threshold (e.g. 2G, 4G). Must be 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)
|
||||
}
|
||||
|
||||
func runApplication(o options) (result error) {
|
||||
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
|
||||
return err
|
||||
}
|
||||
defer closeH(mutex)
|
||||
|
||||
logPath := filepath.Join(os.TempDir(), "autopriority.log")
|
||||
logFile, err := os.Create(logPath)
|
||||
if exists {
|
||||
fmt.Fprintln(os.Stderr, "autoPriority: already running")
|
||||
return nil
|
||||
}
|
||||
logFile, err := openRotatingLog(filepath.Join(os.TempDir(), "autopriority.log"), 4<<20)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "autoPriority: cannot create log: %v\n", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
logf := func(format string, a ...any) {
|
||||
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
|
||||
fmt.Fprintf(logFile, format+"\n", a...)
|
||||
defer func() { result = errors.Join(result, logFile.Close()) }()
|
||||
emit := func(r scanReport) error { return logFile.Write(formatReport(r, time.Now())) }
|
||||
if err = emit(scanReport{Title: fmt.Sprintf("autoPriority: mem=%s, game-mem=%s, interval=%s", formatMemSize(o.mem), formatMemSize(o.gameMem), o.interval), DryRun: o.dryRun}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
|
||||
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
|
||||
|
||||
if !*dryRun {
|
||||
if !o.dryRun {
|
||||
pid := uint32(os.Getpid())
|
||||
h, err := openProc(pid, ProcessSetInformation)
|
||||
if err == nil {
|
||||
err = setPrio(h, pid, PriorityClassIdle)
|
||||
h, e := openProc(pid, ProcessSetInformation)
|
||||
if e == nil {
|
||||
e = setPrio(h, pid, PriorityClassIdle)
|
||||
closeH(h)
|
||||
}
|
||||
if err != nil {
|
||||
logf("warning: could not set own priority to IDLE: %v", err)
|
||||
} else {
|
||||
logf("own priority set to IDLE")
|
||||
if e != nil {
|
||||
if err = emit(scanReport{Errors: []processError{{Name: "autoPriority.exe", PID: pid, Description: fmt.Sprintf("Собственный приоритет IDLE не установлен: %v", e)}}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blocked := make(map[processKey]string)
|
||||
unreadable := make(map[uint32]string)
|
||||
gameProcs := make(map[processKey]trackedProc)
|
||||
gameSaved := make(map[processKey]savedPrio)
|
||||
gameMode := false
|
||||
myPID := uint32(os.Getpid())
|
||||
|
||||
readProcs := func() ([]procInfo, error) {
|
||||
procs, err := allProcs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
live := make(map[uint32]string, len(procs))
|
||||
list := make([]procInfo, 0, len(procs))
|
||||
for i := range procs {
|
||||
p := &procs[i]
|
||||
live[p.PID] = p.Name
|
||||
if p.PID == myPID || p.PID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
h, setErr := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
|
||||
p.CanSet = setErr == nil
|
||||
openErr := error(nil)
|
||||
if !p.CanSet {
|
||||
h, openErr = openProc(p.PID, ProcessQueryLimitedInformation)
|
||||
}
|
||||
if openErr != nil {
|
||||
if unreadable[p.PID] != p.Name {
|
||||
unreadable[p.PID] = p.Name
|
||||
logf("SKIP %s (PID %d): %v", p.Name, p.PID, openErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
name, readErr := processName(h)
|
||||
if readErr == nil {
|
||||
p.Name = name
|
||||
}
|
||||
created := uint64(0)
|
||||
if readErr == nil {
|
||||
created, readErr = processCreated(h)
|
||||
}
|
||||
var m processMemoryCounters
|
||||
if readErr == nil {
|
||||
m.CBM = uint32(unsafe.Sizeof(m))
|
||||
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
|
||||
if r == 0 {
|
||||
readErr = fmt.Errorf("GetProcessMemoryInfo failed: %w", e)
|
||||
}
|
||||
}
|
||||
var cur uintptr
|
||||
if readErr == nil {
|
||||
r, _, e := procGetPriority.Call(uintptr(h))
|
||||
cur = r
|
||||
if cur == 0 {
|
||||
readErr = fmt.Errorf("GetPriorityClass failed: %w", e)
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
closeH(h)
|
||||
if unreadable[p.PID] != p.Name {
|
||||
unreadable[p.PID] = p.Name
|
||||
logf("SKIP %s (PID %d): %v", p.Name, p.PID, readErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
delete(unreadable, p.PID)
|
||||
p.Key = processKey{PID: p.PID, Created: created}
|
||||
p.RSS = uint64(m.WorkingSetSize)
|
||||
p.Prio = uint32(cur)
|
||||
p.Handle = h
|
||||
if !p.CanSet {
|
||||
if _, ok := blocked[p.Key]; !ok {
|
||||
blocked[p.Key] = p.Name
|
||||
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, setErr)
|
||||
}
|
||||
}
|
||||
list = append(list, *p)
|
||||
}
|
||||
|
||||
for pid, name := range unreadable {
|
||||
if live[pid] != name {
|
||||
delete(unreadable, pid)
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
restored := make(chan struct{})
|
||||
tray, err := newTray(cancel, restored)
|
||||
if err != nil {
|
||||
close(restored)
|
||||
return err
|
||||
}
|
||||
|
||||
restore := func(list []procInfo) {
|
||||
current := make(map[processKey]*procInfo, len(list))
|
||||
for i := range list {
|
||||
current[list[i].Key] = &list[i]
|
||||
}
|
||||
for key, saved := range gameSaved {
|
||||
alive, err := processAlive(saved.handle)
|
||||
if err != nil {
|
||||
logf("RESTORE %s (PID %d) status error: %v", saved.name, key.PID, err)
|
||||
continue
|
||||
}
|
||||
if !alive {
|
||||
closeH(saved.handle)
|
||||
delete(gameSaved, key)
|
||||
continue
|
||||
}
|
||||
cur, _, e := procGetPriority.Call(uintptr(saved.handle))
|
||||
if cur == 0 {
|
||||
logf("RESTORE %s (PID %d) priority error: %v", saved.name, key.PID, e)
|
||||
continue
|
||||
}
|
||||
if uint32(cur) != saved.prio {
|
||||
if err := setPrio(saved.handle, key.PID, saved.prio); err != nil {
|
||||
logf("RESTORE %s (PID %d) -> %s error: %v", saved.name, key.PID, prioName(saved.prio), err)
|
||||
continue
|
||||
}
|
||||
logf("RESTORE %s (PID %d) -> %s", saved.name, key.PID, prioName(saved.prio))
|
||||
}
|
||||
if p := current[key]; p != nil {
|
||||
p.Prio = saved.prio
|
||||
}
|
||||
closeH(saved.handle)
|
||||
delete(gameSaved, key)
|
||||
}
|
||||
defer func() { result = errors.Join(result, tray.Close()) }()
|
||||
// runController performs restoration before this channel closes and before the tray disappears.
|
||||
defer close(restored)
|
||||
return runController(ctx, o, newWindowsBackend(), emit)
|
||||
}
|
||||
func main() {
|
||||
o, err := readOptions(os.Args[1:], os.Stderr)
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for attempt := 0; attempt < 3 && len(gameSaved) > 0; attempt++ {
|
||||
restore(nil)
|
||||
if len(gameSaved) > 0 {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if len(gameSaved) > 0 {
|
||||
logf("warning: %d process priorities could not be restored", len(gameSaved))
|
||||
for _, saved := range gameSaved {
|
||||
closeH(saved.handle)
|
||||
}
|
||||
}
|
||||
for _, game := range gameProcs {
|
||||
closeH(game.handle)
|
||||
}
|
||||
logf("autoPriority stopped")
|
||||
logFile.Sync()
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(*interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
scan := func() {
|
||||
list, err := readProcs()
|
||||
if err != nil {
|
||||
logf("process scan error: %v", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
for _, p := range list {
|
||||
if !p.Keep {
|
||||
closeH(p.Handle)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
current := make(map[processKey]string, len(list))
|
||||
for _, p := range list {
|
||||
current[p.Key] = p.Name
|
||||
}
|
||||
|
||||
for key, name := range blocked {
|
||||
if current[key] != name {
|
||||
delete(blocked, key)
|
||||
}
|
||||
}
|
||||
for key, game := range gameProcs {
|
||||
alive, err := processAlive(game.handle)
|
||||
if err != nil {
|
||||
logf("GAME %s (PID %d) status error: %v", game.name, key.PID, err)
|
||||
continue
|
||||
}
|
||||
if !alive {
|
||||
closeH(game.handle)
|
||||
delete(gameProcs, key)
|
||||
}
|
||||
}
|
||||
for key, saved := range gameSaved {
|
||||
alive, err := processAlive(saved.handle)
|
||||
if err == nil && !alive {
|
||||
closeH(saved.handle)
|
||||
delete(gameSaved, key)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
p := &list[i]
|
||||
if gameMem > 0 && p.RSS >= gameMem {
|
||||
if _, ok := gameProcs[p.Key]; !ok {
|
||||
gameProcs[p.Key] = trackedProc{name: p.Name, handle: p.Handle}
|
||||
p.Keep = true
|
||||
logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasGame := gameMem > 0 && len(gameProcs) > 0
|
||||
enteringGame := hasGame && !gameMode
|
||||
if enteringGame {
|
||||
gameMode = true
|
||||
}
|
||||
if !hasGame && gameMode {
|
||||
restore(list)
|
||||
if len(gameSaved) == 0 {
|
||||
gameMode = false
|
||||
logf("GAME MODE OFF")
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasGame {
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
_, iGame := gameProcs[list[i].Key]
|
||||
_, jGame := gameProcs[list[j].Key]
|
||||
return iGame && !jGame
|
||||
})
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
p := &list[i]
|
||||
_, isBlocked := blocked[p.Key]
|
||||
_, isGame := gameProcs[p.Key]
|
||||
target := desiredPriority(p.RSS, mem, p.Prio, hasGame, isGame)
|
||||
action := "GAME"
|
||||
if !hasGame && target == PriorityClassHigh {
|
||||
action = "PROMOTE"
|
||||
} else if !hasGame {
|
||||
action = "DEMOTE"
|
||||
}
|
||||
|
||||
if target == 0 || target == p.Prio || isBlocked {
|
||||
continue
|
||||
}
|
||||
if *dryRun {
|
||||
logf("[DRY-RUN] %s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
|
||||
continue
|
||||
}
|
||||
if err := setPrio(p.Handle, p.PID, target); err != nil {
|
||||
blocked[p.Key] = p.Name
|
||||
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, err)
|
||||
continue
|
||||
}
|
||||
if hasGame && !isGame && target == PriorityClassIdle {
|
||||
if _, saved := gameSaved[p.Key]; !saved {
|
||||
gameSaved[p.Key] = savedPrio{name: p.Name, prio: p.Prio, handle: p.Handle}
|
||||
p.Keep = true
|
||||
}
|
||||
}
|
||||
logf("%s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
|
||||
p.Prio = target
|
||||
}
|
||||
if enteringGame {
|
||||
logf("GAME MODE ON")
|
||||
}
|
||||
if err == nil {
|
||||
err = runApplication(o)
|
||||
}
|
||||
|
||||
scan()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
logf("received shutdown signal")
|
||||
return
|
||||
case <-ticker.C:
|
||||
scan()
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "autoPriority: %v\n", err)
|
||||
showError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user