Files
autoPriority/main.go
T

636 lines
16 KiB
Go

//go:build windows
package main
import (
"flag"
"fmt"
"math"
"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 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)
}
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) (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)
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 {
return nil, fmt.Errorf("Process32First failed: %w", e)
}
var out []procInfo
for {
name := syscall.UTF16ToString(pe.ExeFile[:])
if name != "" {
out = append(out, procInfo{PID: pe.PID, 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
}
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)
}
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, a ...any) {
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
fmt.Fprintf(logFile, format+"\n", a...)
}
logf("autoPriority started (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 {
logf("warning: could not set own priority to IDLE: %v", err)
} else {
logf("own priority set to IDLE")
}
}
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
}
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() {
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")
}
}
scan()
for {
select {
case <-stop:
logf("received shutdown signal")
return
case <-ticker.C:
scan()
}
}
}