diff --git a/README.md b/README.md index e3ada11..024e1c4 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ This is a simple Windows application that runs in the system tray and allows you to clean the standby memory list. The application requests administrator privileges upon startup and provides a menu in the tray to perform memory cleaning. ## Features -- Runs in the system tray. +- Cleans RAM - Cleans the standby memory list when its size exceeds 65% of free memory. -- Requests administrator privileges on startup. - Displays memory usage statistics when hovering over the tray icon. -- Uses an embedded icon for the tray. +- Runs in the system tray. ## Requirements +- Requests administrator privileges on startup. - Windows operating system. - Go 1.16 or later. diff --git a/cmd/main.go b/cmd/main.go index 642acb8..e048641 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,28 +3,64 @@ package main import ( - "clean-standby-list/internal/elevation" - "clean-standby-list/internal/memory" "clean-standby-list/internal/tray" + "clean-standby-list/internal/windows_api" "runtime" + "time" "github.com/getlantern/systray" ) +var lastCleanup time.Time +var stopChan = make(chan struct{}) +const ( + percentThreshold = 65 +) + func main() { // Request admin rights if not already granted - if !elevation.IsRunAsAdmin() { - elevation.RunMeElevated() + if !windowsapi.IsRunAsAdmin() { + windowsapi.RunAsAdmin() return } - onReady := func() { - tray.OnReady(memory.EmptyStandbyList, memory.CheckAndCleanStandbyList) - } + go autoCleanStandbyList(stopChan) - systray.Run(onReady, onExit) + systray.Run(tray.OnReady, onExit) } func onExit() { + close(stopChan) runtime.GC() } + +// autoCleanStandbyList periodically cleans the standby list to free up RAM. +// It runs in a loop until the stopChan is closed. +// The function checks the percentage of standby list usage against the threshold. +// If the percentage is above the threshold and enough time has passed since the last cleanup, +// it calls the CleanStandbyList function to clean the standby list. +// It also updates the tooltip and sleeps for 1 minute before the next iteration. +// +// Parameters: +// - stopChan: A channel used to stop the function when closed. +// +// Note: The function assumes the availability of the windowsapi package. +func autoCleanStandbyList(stopChan chan struct{}) { + for { + select { + case <-stopChan: + return + default: + standbyList, freeRAM, _ := windowsapi.GetStanbyListAndFreeRAMSize() + percent := standbyList / freeRAM * 100 + + if percent > percentThreshold && time.Since(lastCleanup) > 5*time.Minute { + windowsapi.CleanStandbyList() + lastCleanup = time.Now() + } + + tray.UpdateTooltip() + time.Sleep(1 * time.Minute) + } + } +} diff --git a/internal/memory/memory.go b/internal/memory/memory.go deleted file mode 100644 index 3edc92a..0000000 --- a/internal/memory/memory.go +++ /dev/null @@ -1,100 +0,0 @@ -// internal/memory/memory.go - -package memory - -import ( - "fmt" - "log" - "time" - "unsafe" - - "clean-standby-list/internal/elevation" - "clean-standby-list/internal/tray" - - "github.com/StackExchange/wmi" - "golang.org/x/sys/windows" -) - -const ( - SystemMemoryListInformationClass = 0x0050 - MemoryPurgeStandbyList = 4 - PercentThreshold = 65 -) - -var lastCleanup time.Time - -type Win32_PerfRawData_PerfOS_Memory struct { - StandbyCacheNormalPriorityBytes uint64 - StandbyCacheReserveBytes uint64 - StandbyCacheCoreBytes uint64 - AvailableBytes uint64 -} - -// GetStandbyListInfo retrieves the size of the standby list and free memory using WMI -func GetStandbyListInfo() (standbySize, freeSize uint64, err error) { - var dst []Win32_PerfRawData_PerfOS_Memory - query := wmi.CreateQuery(&dst, "") - err = wmi.Query(query, &dst) - if err != nil { - return 0, 0, err - } - - if len(dst) > 0 { - standbySize = dst[0].StandbyCacheCoreBytes + dst[0].StandbyCacheNormalPriorityBytes + dst[0].StandbyCacheReserveBytes - freeSize = dst[0].AvailableBytes - return standbySize, freeSize, nil - } - return 0, 0, fmt.Errorf("no data returned from WMI query") -} - -func CheckAndCleanStandbyList() { - standbySize, freeSize, err := GetStandbyListInfo() - if err != nil { - log.Printf("Error getting memory info: %v\n", err) - return - } - percent := (standbySize * 100) / freeSize - log.Printf("Standby List: %d MB, Free Memory: %d MB, Percent: %d%%\n", standbySize/1024/1024, freeSize/1024/1024, percent) - - if percent > PercentThreshold && time.Since(lastCleanup) > 5*time.Minute { - log.Printf("Standby list exceeds %d%% of free memory, cleaning...\n", PercentThreshold) - if err := EmptyStandbyList(); err != nil { - log.Printf("Error cleaning standby list: %v\n", err) - } else { - log.Println("Standby list cleaned successfully") - lastCleanup = time.Now() - } - } - tray.UpdateTooltip(standbySize, freeSize, percent) -} - -func EmptyStandbyList() error { - privileges := []string{"SeProfileSingleProcessPrivilege", "SeIncreaseQuotaPrivilege", "SeDebugPrivilege"} - for _, privilege := range privileges { - if err := elevation.EnablePrivilege(privilege); err != nil { - return fmt.Errorf("failed to enable privilege %s: %v", privilege, err) - } - log.Printf("Enabled privilege: %s\n", privilege) - } - log.Println("All privileges enabled successfully") - - ntdll := windows.NewLazySystemDLL("ntdll.dll") - if err := ntdll.Load(); err != nil { - return fmt.Errorf("failed to load ntdll.dll: %v", err) - } - log.Println("ntdll.dll loaded successfully") - - procNtSetSystemInformation := ntdll.NewProc("NtSetSystemInformation") - memoryPurgeStandbyList := uint32(MemoryPurgeStandbyList) - r1, _, err := procNtSetSystemInformation.Call( - uintptr(SystemMemoryListInformationClass), - uintptr(unsafe.Pointer(&memoryPurgeStandbyList)), - unsafe.Sizeof(memoryPurgeStandbyList), - ) - if r1 != 0 { - return fmt.Errorf("NtSetSystemInformation call failed: %v", err) - } - log.Println("NtSetSystemInformation call succeeded") - - return nil -} diff --git a/internal/tray/assets/icon.ico b/internal/tray/assets/icon.ico index 765a2c2..b1c0111 100644 Binary files a/internal/tray/assets/icon.ico and b/internal/tray/assets/icon.ico differ diff --git a/internal/tray/tray.go b/internal/tray/tray.go index 973f6b8..8cc5949 100644 --- a/internal/tray/tray.go +++ b/internal/tray/tray.go @@ -6,10 +6,11 @@ package tray import ( _ "embed" "fmt" - "log" "os" "time" + windowsapi "clean-standby-list/internal/windows_api" + "github.com/getlantern/systray" ) @@ -18,43 +19,79 @@ import ( //go:embed assets/icon.ico var iconData []byte -var LastCleanup time.Time +const ( + PercentThreshold = 65 +) + +type TrayInfoStruct struct { + LastSTDCleanup time.Time + LastRAMCleanup time.Time + ErrorStr string +} + +var TrayInfo = TrayInfoStruct{ + LastSTDCleanup: time.Time{}, + LastRAMCleanup: time.Time{}, + ErrorStr: "", +} -func OnReady(emptyStandbyList func() error, checkAndCleanStandbyList func()) { +// OnReady initializes the system tray icon and menu items. +// It sets the icon, title, and adds menu items for cleaning the standby list, cleaning the RAM, and quitting the application. +// It also starts a goroutine to handle menu item clicks. +func OnReady() { systray.SetIcon(iconData) // Use the embedded icon systray.SetTitle("Memory Cleaner") - systray.SetTooltip("Right-click to clean standby list") - mClean := systray.AddMenuItem("Clean", "Clean the standby list") + mSTDClean := systray.AddMenuItem("Clean Standby List", "Clean the standby list") + mRAMClean := systray.AddMenuItem("Clean RAM", "Clean the RAM") mQuit := systray.AddMenuItem("Quit", "Exit the application") - go func() { - for { - select { - case <-mClean.ClickedCh: - if err := emptyStandbyList(); err != nil { - log.Printf("Error cleaning standby list: %v\n", err) - } else { - log.Println("Standby list cleaned successfully") - LastCleanup = time.Now() - } - case <-mQuit.ClickedCh: - systray.Quit() - os.Exit(0) - } - } - }() + go handleMenuClicks(mSTDClean, mRAMClean, mQuit) +} - // Periodically update the tooltip with memory information - go func() { - for { - checkAndCleanStandbyList() - time.Sleep(30 * time.Second) // Update every minute - } - }() +// UpdateTooltip updates the tooltip text of the system tray icon. +// It retrieves the standby list and free RAM size using the windowsapi package, +// and formats the tooltip string with the obtained values and the last cleanup timestamps. +// The formatted tooltip string is then set as the tooltip for the system tray icon. +func UpdateTooltip() { + standbyList, freeRAM, err := windowsapi.GetStanbyListAndFreeRAMSize() + if err != nil { + TrayInfo.ErrorStr = err.Error() + } + + tooltipStr := fmt.Sprintf( + "SBL: %d MB\nFreeRAM: %d MB\nSTBcln: %s\nRAMcln: %s", + standbyList/(1024*1024), + freeRAM/(1024*1024), + TrayInfo.LastSTDCleanup.Format("15:04:05"), + TrayInfo.LastRAMCleanup.Format("15:04:05"), + ) + + systray.SetTooltip(tooltipStr) } -func UpdateTooltip(standbySize, freeSize uint64, percent uint64) { - tooltip := fmt.Sprintf("Standby List: %d MB, Free Memory: %d MB, Percent: %d%%", standbySize/(1024*1024), freeSize/(1024*1024), percent) - systray.SetTooltip(tooltip) +func handleMenuClicks(mSTDClean, mRAMClean, mQuit *systray.MenuItem) { + for { + select { + case <-mRAMClean.ClickedCh: + if err := windowsapi.CleanRAM(); err != nil { + TrayInfo.ErrorStr = err.Error() + UpdateTooltip() + } else { + TrayInfo.LastRAMCleanup = time.Now() + UpdateTooltip() + } + case <-mSTDClean.ClickedCh: + if err := windowsapi.CleanStandbyList(); err != nil { + TrayInfo.ErrorStr = err.Error() + UpdateTooltip() + } else { + TrayInfo.LastSTDCleanup = time.Now() + UpdateTooltip() + } + case <-mQuit.ClickedCh: + systray.Quit() + os.Exit(0) + } + } } diff --git a/internal/windows_api/actions.go b/internal/windows_api/actions.go new file mode 100644 index 0000000..01f4284 --- /dev/null +++ b/internal/windows_api/actions.go @@ -0,0 +1,150 @@ +package windowsapi + +import ( + "fmt" + "unsafe" + + "github.com/StackExchange/wmi" + "golang.org/x/sys/windows" +) + +const ( + SystemMemoryListInformationClass = 0x0050 + MemoryPurgeStandbyList = 4 +) + +// Win32_PerfRawData_PerfOS_Memory structure for WMI queries +type Win32_PerfRawData_PerfOS_Memory struct { + StandbyCacheNormalPriorityBytes uint64 + StandbyCacheReserveBytes uint64 + StandbyCacheCoreBytes uint64 + AvailableBytes uint64 +} + +// CleanRAM cleans the system and process memory to free up RAM. +// It calls various functions to clean the system memory, process memory, and system working set. +// If any of the cleaning operations fail, it returns an error. +func CleanRAM() error { + err := cleanSystemMemory() + if err := cleanSystemMemory(); err != nil { + return fmt.Errorf("failed to clean system memory: %v", err) + } + + if err := cleanProcessMemory(); err != nil { + return fmt.Errorf("failed to clean process memory: %v", err) + } + + if err := cleanSystemWorkingSet(); err != nil { + return fmt.Errorf("failed to clean system working set: %v", err) + } + + return err +} + +// GetStanbyListAndFreeRAMSize retrieves the size of the standby list and the amount of free RAM. +// It returns the standby list size, free RAM size, and any error encountered. +func GetStanbyListAndFreeRAMSize() (uint64, uint64, error) { + standbySize, freeSize, err := getStandbyListInfo() + if err != nil { + return 0, 0, fmt.Errorf("error getting memory info: %v", err) + } + + return standbySize, freeSize, nil +} + +// CleanStandbyList purges the standby list in the system memory. +// It grants necessary privileges to the process and calls the NtSetSystemInformation function +// to perform the memory purge operation. +// Returns an error if granting privileges or calling NtSetSystemInformation fails. +func CleanStandbyList() error { + if err := GrantPrivileges(); err != nil { + return fmt.Errorf("failed to grant privileges to the process: %v", err) + } + + memoryPurgeStandbyList := uint32(MemoryPurgeStandbyList) + r1, _, err := NtSetSystemInformation.Call( + uintptr(SystemMemoryListInformationClass), + uintptr(unsafe.Pointer(&memoryPurgeStandbyList)), + unsafe.Sizeof(memoryPurgeStandbyList), + ) + if r1 != 0 { + return fmt.Errorf("NtSetSystemInformation call failed: %v", err) + } + + return nil +} + +// GetStandbyListInfo retrieves the size of the standby list and free memory using WMI +func getStandbyListInfo() (standbySize, freeSize uint64, err error) { + var dst []Win32_PerfRawData_PerfOS_Memory + query := wmi.CreateQuery(&dst, "") + err = wmi.Query(query, &dst) + if err != nil { + return 0, 0, err + } + + if len(dst) > 0 { + standbySize = dst[0].StandbyCacheCoreBytes + dst[0].StandbyCacheNormalPriorityBytes + dst[0].StandbyCacheReserveBytes + freeSize = dst[0].AvailableBytes + return standbySize, freeSize, nil + } + return 0, 0, fmt.Errorf("no data returned from WMI query") +} + +// getCurrentProcessHandle retrieves the handle of the current process +func getCurrentProcessHandle() windows.Handle { + return windows.CurrentProcess() +} + +// cleanProcessMemory sets the process working set size to minimum and maximum values +func cleanProcessMemory() error { + hProcess := getCurrentProcessHandle() + ret, _, err := ProcSetProcessWorkingSetSize.Call(uintptr(hProcess), uintptr(^uint32(0)), uintptr(^uint32(0))) + if ret == 0 { + return fmt.Errorf("failed to set process working set size: %v", err) + } + + return nil +} + +// cleanSystemWorkingSet empties the working set of the current process +func cleanSystemWorkingSet() error { + hProcess := getCurrentProcessHandle() + + ret, _, err := ProcEmptyWorkingSet.Call(uintptr(hProcess)) + if ret == 0 { + return fmt.Errorf("failed to empty working set: %v", err) + } + + return nil +} + +// cleanSystemMemory frees memory of all processes by emptying their working sets +func cleanSystemMemory() error { + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + return fmt.Errorf("failed to create snapshot: %v", err) + } + defer windows.CloseHandle(snapshot) + + var pe windows.ProcessEntry32 + pe.Size = uint32(unsafe.Sizeof(pe)) + if err := windows.Process32First(snapshot, &pe); err != nil { + return fmt.Errorf("failed to get first process: %v", err) + } + + for { + hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_SET_QUOTA, false, pe.ProcessID) + if err == nil { + ProcEmptyWorkingSet.Call(uintptr(hProcess)) + windows.CloseHandle(hProcess) + } + + err = windows.Process32Next(snapshot, &pe) + if err != nil { + break + } + } + + return nil +} diff --git a/internal/windows_api/dll_libs.go b/internal/windows_api/dll_libs.go new file mode 100644 index 0000000..9d4f695 --- /dev/null +++ b/internal/windows_api/dll_libs.go @@ -0,0 +1,13 @@ +package windowsapi + +import "syscall" + +// Windows API function definitions +var ( + ModKernel32 = syscall.NewLazyDLL("kernel32.dll") + ModPsapi = syscall.NewLazyDLL("psapi.dll") + Ntdll = syscall.NewLazyDLL("ntdll.dll") + ProcSetProcessWorkingSetSize = ModKernel32.NewProc("SetProcessWorkingSetSize") + ProcEmptyWorkingSet = ModPsapi.NewProc("EmptyWorkingSet") + NtSetSystemInformation = Ntdll.NewProc("NtSetSystemInformation") +) \ No newline at end of file diff --git a/internal/elevation/elevation.go b/internal/windows_api/privileges.go similarity index 50% rename from internal/elevation/elevation.go rename to internal/windows_api/privileges.go index e2591ff..fab55ee 100644 --- a/internal/elevation/elevation.go +++ b/internal/windows_api/privileges.go @@ -1,9 +1,8 @@ // Description: This package provides functions to elevate the current process to run as administrator. -package elevation +package windowsapi import ( "fmt" - "log" "os" "strings" "syscall" @@ -11,7 +10,12 @@ import ( "golang.org/x/sys/windows" ) -func RunMeElevated() { +// RunAsAdmin runs the current executable with administrative privileges. +// It uses the Windows API function ShellExecute to execute the executable +// with the "runas" verb, which prompts the user for consent to elevate +// the process. The function takes no arguments and returns no values. +// If an error occurs during the execution, it will be printed to the console. +func RunAsAdmin() { verb := "runas" exe, _ := os.Executable() cwd, _ := os.Getwd() @@ -30,25 +34,40 @@ func RunMeElevated() { } } -func EnablePrivilege(privilegeName string) error { - log.Printf("Attempting to enable privilege: %s\n", privilegeName) +// GrantPrivileges grants specific privileges to the current process. +// It enables the privileges specified in the `privileges` slice. +// If any privilege fails to be enabled, an error is returned. +func GrantPrivileges() error { + privileges := []string{"SeProfileSingleProcessPrivilege", "SeIncreaseQuotaPrivilege", "SeDebugPrivilege"} + for _, privilege := range privileges { + if err := enablePrivilege(privilege); err != nil { + return fmt.Errorf("failed to enable privilege %s: %v", privilege, err) + } + } + + return nil +} + +// enablePrivilege enables the specified privilege for the current process. +// It takes a privilegeName string as input and returns an error if any. +// The function uses the Windows API to lookup the privilege value, open the process token, +// adjust the token privileges, and enable the specified privilege. +// If any error occurs during the process, it returns an error with a descriptive message. +func enablePrivilege(privilegeName string) error { var luid windows.LUID err := windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr(privilegeName), &luid) if err != nil { return fmt.Errorf("LookupPrivilegeValue error: %v", err) } - log.Printf("LookupPrivilegeValue succeeded for privilege: %s\n", privilegeName) var token windows.Token processHandle := windows.CurrentProcess() - log.Println("GetCurrentProcess succeeded") err = windows.OpenProcessToken(processHandle, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) if err != nil { return fmt.Errorf("OpenProcessToken error: %v", err) } defer token.Close() - log.Println("OpenProcessToken succeeded") tp := windows.Tokenprivileges{ PrivilegeCount: 1, @@ -61,12 +80,10 @@ func EnablePrivilege(privilegeName string) error { if err != nil { return fmt.Errorf("AdjustTokenPrivileges error: %v", err) } - log.Printf("AdjustTokenPrivileges succeeded for privilege: %s\n", privilegeName) + return nil } func IsRunAsAdmin() bool { - elevated := windows.GetCurrentProcessToken().IsElevated() - fmt.Printf("Admin: %v\n", elevated) - return elevated + return windows.GetCurrentProcessToken().IsElevated() }