diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e6607e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe +*.log diff --git a/README.md b/README.md index 9229581..a43269b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,38 @@ # autoPriority -A program that automatically sets a high priority for game processes \ No newline at end of file +Automatically sets high CPU priority for memory-heavy processes. + +## Build + +``` +# Standard build (with console window) +go build -o autopriority.exe . + +# Background build (no console, minimal size) +go build -ldflags="-H windowsgui -s -w" -o autopriority.exe . +``` + +## Usage + +``` +autopriority [flags] +``` + +| Flag | Default | Description | +|-------------|---------------|----------------------------------| +| `-mem` | 536870912 | Memory threshold in bytes | +| `-interval` | 1m0s | Scan interval (min 10s) | +| `-dry-run` | false | Log only, don't change priority | +| `-log` | true | Write `autopriority.log` file | + +## Auto-start + +Place a shortcut to `autopriority.exe` in: + +``` +shell:startup +``` + +## Dependencies + +None. Uses only Windows API (kernel32, psapi) via raw syscall. diff --git a/go.mod b/go.mod index b9c8282..7b79382 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,3 @@ -module autoPriority +module autopriority -go 1.19 - -require ( - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/postfinance/single v0.0.2 // indirect - github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/tklauser/go-sysconf v0.3.11 // indirect - github.com/tklauser/numcpus v0.6.0 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.4.0 -) +go 1.24 diff --git a/go.sum b/go.sum deleted file mode 100644 index 4664a37..0000000 --- a/go.sum +++ /dev/null @@ -1,22 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/postfinance/single v0.0.2 h1:YLjLxxeGDnsW93oK4CxxOFSVOOiBi1OyoK4ZTl5biJw= -github.com/postfinance/single v0.0.2/go.mod h1:OYWUsdMIZK9eQyZYpAsMHN+j6+jXJ6RFUNqIWH0oC5U= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..85c0378 --- /dev/null +++ b/main.go @@ -0,0 +1,262 @@ +//go:build windows + +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "syscall" + "time" + "unsafe" +) + +var ( + k32 = syscall.NewLazyDLL("kernel32.dll") + ps = syscall.NewLazyDLL("psapi.dll") + + pCreateSnap = k32.NewProc("CreateToolhelp32Snapshot") + pProcess32First = k32.NewProc("Process32FirstW") + pProcess32Next = k32.NewProc("Process32NextW") + pGetProcMemInfo = ps.NewProc("GetProcessMemoryInfo") + pSetPriority = k32.NewProc("SetPriorityClass") + pGetPriority = k32.NewProc("GetPriorityClass") + pOpenProcess = k32.NewProc("OpenProcess") + pCloseHandle = k32.NewProc("CloseHandle") + pCreateMutex = k32.NewProc("CreateMutexW") + pMessageBox = k32.NewProc("MessageBoxW") + pGetLastError = k32.NewProc("GetLastError") +) + +const ( + SNAPPROCESS = 0x00000002 + SET_INFO = 0x0200 + QUERY_INFO = 0x0400 + IDLE_PRIO = 0x00000040 + NORMAL_PRIO = 0x00000020 + HIGH_PRIO = 0x00000080 + EXISTING = 1832 +) + +type pe32 struct { + Size uint32 + CntUsage uint32 + PID uint32 + HeapID uintptr + ModuleID uint32 + Threads uint32 + ParentPID uint32 + PrioClass int32 + Flags uint32 + ExeFile [260]uint16 +} + +type pmc struct { + CBM uint32 + PageFaults uint32 + PeakWS uintptr + WorkingSet uintptr + QuotaPeakPG uintptr + QuotaPG uintptr + QuotaPeakPM uintptr + QuotaPM uintptr + PeakPF uintptr + Pagefile uintptr + Reserved [2]uintptr +} + +type procInfo struct { + PID uint32 + Name string +} + +func wide(s string) *uint16 { + return syscall.StringToUTF16Ptr(s) +} + +func closeH(h syscall.Handle) { + _, _, _ = pCloseHandle.Call(uintptr(h)) +} + +func openProc(pid uint32, acc uint32) syscall.Handle { + r, _, _ := pOpenProcess.Call(uintptr(acc), 0, uintptr(pid)) + return syscall.Handle(r) +} + +func setPrio(pid uint32, cls uint32) bool { + h := openProc(pid, SET_INFO) + if h == 0 { + return false + } + defer closeH(h) + r, _, _ := pSetPriority.Call(uintptr(h), uintptr(cls)) + return r != 0 +} + +func curPrio(pid uint32) uint32 { + h := openProc(pid, QUERY_INFO) + if h == 0 { + return 0 + } + defer closeH(h) + r, _, _ := pGetPriority.Call(uintptr(h)) + return uint32(r) +} + +func rss(pid uint32) uint64 { + h := openProc(pid, QUERY_INFO) + if h == 0 { + return 0 + } + defer closeH(h) + + var m pmc + r, _, _ := pGetProcMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m))) + if r == 0 { + return 0 + } + return uint64(m.WorkingSet) +} + +func allProcs() []procInfo { + var out []procInfo + pe := pe32{Size: uint32(unsafe.Sizeof(pe32{}))} + snap, _, _ := pCreateSnap.Call(SNAPPROCESS, 0) + if snap == 0 { + return out + } + defer closeH(syscall.Handle(snap)) + + r, _, _ := pProcess32First.Call(snap, uintptr(unsafe.Pointer(&pe))) + if r != 0 { + for { + out = append(out, procInfo{ + PID: pe.PID, + Name: syscall.UTF16ToString(pe.ExeFile[:]), + }) + r, _, _ := pProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe))) + if r == 0 { + break + } + } + } + return out +} + +func prioName(c uint32) string { + switch c { + case IDLE_PRIO: + return "IDLE" + case NORMAL_PRIO: + return "NORMAL" + case HIGH_PRIO: + return "HIGH" + default: + return fmt.Sprintf("0x%X", c) + } +} + +func main() { + mem := flag.Uint64("mem", 512*1024*1024, "memory threshold in bytes") + interval := flag.Duration("interval", time.Minute, "scan interval") + dryRun := flag.Bool("dry-run", false, "log only, do not change priorities") + doLog := flag.Bool("log", true, "write log file next to executable") + flag.Parse() + + if *interval < 10*time.Second { + *interval = 10 * time.Second + } + + var logFile *os.File + if *doLog { + exePath, _ := os.Executable() + f, err := os.OpenFile(filepath.Join(filepath.Dir(exePath), "autopriority.log"), + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err == nil { + logFile = f + defer logFile.Close() + } + } + + ts := func() string { + return time.Now().Format("02.01.2006 15:04:05") + } + + logf := func(format string, a ...any) { + if logFile == nil { + return + } + fmt.Fprintf(logFile, "[%s] ", ts()) + fmt.Fprintf(logFile, format+"\n", a...) + } + + logf("autoPriority started (mem=%d bytes, interval=%s, dry-run=%v)", *mem, *interval, *dryRun) + + // Single instance + mh, _, _ := pCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(wide("Global\\autopriority_v1")))) + lastErr, _, _ := pGetLastError.Call() + if lastErr == uintptr(EXISTING) { + closeH(syscall.Handle(mh)) + pMessageBox.Call(0, + uintptr(unsafe.Pointer(wide("autoPriority is already running."))), + uintptr(unsafe.Pointer(wide("autoPriority"))), + 0x30, + ) + os.Exit(1) + } + + // Lower own priority + pSetPriority.Call(uintptr(syscall.Handle(^uintptr(0))), IDLE_PRIO) + logf("own priority set to IDLE") + + // promoted: PID → name (for restore logging) + promoted := make(map[uint32]string) + + // Restore on exit + defer func() { + for pid, name := range promoted { + if setPrio(pid, NORMAL_PRIO) { + logf("RESTORE %s (PID %d) → NORMAL", name, pid) + } + } + logf("autoPriority stopped") + }() + + myPID := uint32(os.Getpid()) + ticker := time.NewTicker(*interval) + defer ticker.Stop() + + scan := func() { + for _, p := range allProcs() { + if p.PID == myPID || p.PID == 0 { + continue + } + if _, ok := promoted[p.PID]; ok { + continue + } + if rss(p.PID) < *mem { + continue + } + + orig := curPrio(p.PID) + + if *dryRun { + logf("[DRY-RUN] %s (PID %d) RSS=%dMB, priority=%s — would set HIGH", + p.Name, p.PID, rss(p.PID)/1024/1024, prioName(orig)) + continue + } + + if setPrio(p.PID, HIGH_PRIO) { + promoted[p.PID] = p.Name + logf("PROMOTE %s (PID %d) RSS=%dMB, %s → HIGH", + p.Name, p.PID, rss(p.PID)/1024/1024, prioName(orig)) + } + } + } + + scan() + for range ticker.C { + scan() + } +} diff --git a/main_windows.go b/main_windows.go deleted file mode 100644 index d96af06..0000000 --- a/main_windows.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/signal" - "time" - - "github.com/postfinance/single" - "github.com/shirou/gopsutil/process" - "golang.org/x/sys/windows" -) - -const ( - memThreshold = 512 * 1024 * 1024 - selfPriority = windows.IDLE_PRIORITY_CLASS - targetPriority = windows.HIGH_PRIORITY_CLASS -) - -func setPriority(pid int32, class uint32) error { - h, err := windows.OpenProcess(windows.PROCESS_SET_INFORMATION, false, uint32(pid)) - if err != nil { - return fmt.Errorf("open process %d: %w", pid, err) - } - defer windows.CloseHandle(h) - return windows.SetPriorityClass(h, class) -} - -func main() { - lock, err := single.New("autoPriority") - if err != nil { - log.Fatalf("create lock: %v", err) - } - if err := lock.Lock(); err != nil { - windows.MessageBox(0, - windows.StringToUTF16Ptr("The program is already running.\nYou can close it via the Task Manager."), - windows.StringToUTF16Ptr("autoPriority"), - windows.MB_OK|windows.MB_ICONWARNING, - ) - os.Exit(1) - } - defer lock.Unlock() - - if err := setPriority(int32(os.Getpid()), selfPriority); err != nil { - log.Printf("set self priority: %v", err) - } - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt) - - tk := time.NewTicker(time.Minute) - defer tk.Stop() - - for { - select { - case <-sig: - log.Println("exit") - return - case <-tk.C: - procs, err := process.Processes() - if err != nil { - log.Printf("list processes: %v", err) - continue - } - for _, p := range procs { - m, err := p.MemoryInfo() - if err != nil { - continue - } - if m.RSS >= memThreshold { - if err := setPriority(p.Pid, targetPriority); err != nil { - log.Printf("PID %d: %v", p.Pid, err) - } - } - } - } - } -}