diff --git a/CHANGELOG.md b/CHANGELOG.md index 008d6a5..03b74fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.2.0] - 2026-09-14 + +### Added +- **Desktop application** (`WinSenior.Gui.ps1`, WPF โ€” ships with Windows, nothing to + install) with pages for cleanup, optimization, troubleshooting, undo & restore, schedule + and about. *Scan* fills a per-task "Would free" column from the engine's JSON report; + optimization rows show their live applied state; troubleshooting shows OK/Warn/Fail with + detail and lets you tick the problems to fix. Engine output streams into a resizable log + panel with cancel and save. Selections and options persist in + `%ProgramData%\WinSenior\gui-settings.json`. The app launches the engine scripts with + parameters โ€” it never re-implements deletion. +- **One-command launcher** `WinSenior.cmd`: unblocks the scripts, elevates once (UAC), + starts the app without a console. `WinSenior.cmd console` opens the arrow-key menu; + `WinSenior.ps1 -Gui` opens the app from PowerShell. +- **Cleanup engine: `-DeferLocked`** schedules locked/in-use files for deletion at the next + reboot (`MoveFileEx` + `MOVEFILE_DELAY_UNTIL_REBOOT`) instead of counting them as errors; + the summary and JSON report carry a `TotalDeferred` counter. +- **Cleanup engine: partial-folder accounting** โ€” when a folder is only partly deletable + the bytes that did go away are still counted; age-filtered runs prune the empty folder + skeletons they leave behind. +- **Cleanup coverage 63 โ†’ 97:** Visual Studio, Python tools (uv/poetry/pipx/pyenv), JVM + wrappers, .NET SDK, Node toolchain (node-gyp/electron/bun/deno/Cypress), GitHub Desktop, + Composer, Docker Desktop/WSL logs, ML & browser-automation caches (off by default); + Telegram, WhatsApp, Zoom/Skype/Signal/Viber, a generic Electron/Chromium cache sweep, + media players, UWP temp state, notification cache, GPU vendor apps; game launcher logs, + engine caches (Unreal/Unity/Godot), per-game shader caches on every disk; service-account + temps, setup/upgrade/servicing logs, Windows misc caches, BITS queue, DNS/ARP/NetBIOS + flush, silent Store reset, standby-memory purge, Search index rebuild (off); diagnostics + & telemetry caches, third-party app logs, installer leftovers, empty-folder pruning; + hibernation off and old shadow-copy pruning (Dangerous, off). +- Automation hooks for smoke tests: `WINSENIOR_GUI_AUTOCLOSE`, `WINSENIOR_GUI_AUTORUN`, + `WINSENIOR_GUI_SCREENSHOT`. New Pester file `tests/WinSenior.Gui.Tests.ps1` loads the + embedded XAML into a real WPF window and checks every wired control exists. + +### Fixed +- The console menu's status line called an undefined `Test-Admin`; it now uses + `Test-AdminPrivileges` from the shared library. +- Cleanup engine banner printed a hard-coded `v6.0`; it now reads `Get-WinSeniorVersion`. + ## [6.1.0] - 2026-06-24 ### Added diff --git a/Cleanup-Windows-Senior.ps1 b/Cleanup-Windows-Senior.ps1 index 4aef162..f76d044 100644 --- a/Cleanup-Windows-Senior.ps1 +++ b/Cleanup-Windows-Senior.ps1 @@ -15,7 +15,7 @@ .NOTES Author : denfry (https://github.com/denfry/WindowsCleaner) - Version : 6.1.0 + Version : 6.2.0 Requires: PowerShell 5.1+ (Windows). Administrator rights for most tasks. .EXAMPLE @@ -82,6 +82,11 @@ param( # Only delete files older than N days (0 = no age filter). Per-task minimums still apply. [int]$MaxAgeDays = 0, + # Files that are locked/in use are scheduled for deletion at next reboot + # (MoveFileEx MOVEFILE_DELAY_UNTIL_REBOOT) instead of being counted as errors. + [Alias('dl')] + [switch]$DeferLocked, + [string]$LogPath = "$env:TEMP\WindowsCleanup.log", # Optional path for a machine-readable JSON report @@ -102,6 +107,7 @@ $script:Stats = New-Object System.Collections.Generic.List[object] $script:TotalBytes = [int64]0 $script:TotalFiles = 0 $script:TotalErrors = 0 +$script:TotalDeferred = 0 $script:RestorePointMade = $false # -DryRun is a friendly alias for -WhatIf. Setting the preference here makes it @@ -214,6 +220,54 @@ function Expand-TaskPath { $out } +# ===================================================================== +# CORE: LOCKED-FILE HANDLING (delete-on-reboot via MoveFileEx) +# ===================================================================== +function Initialize-DeferredDelete { + if ('WinSenior.Native' -as [type]) { return } + Add-Type -Namespace WinSenior -Name Native -MemberDefinition @' +[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] +public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags); +'@ -ErrorAction SilentlyContinue +} + +# Schedule a locked file (or every file under a locked folder) for deletion at +# the next reboot. Returns the number of entries queued. +function Register-DeferredDelete { + param([string]$FullPath) + Initialize-DeferredDelete + if (-not ('WinSenior.Native' -as [type])) { return 0 } + $queued = 0 + $targets = if (Test-Path -LiteralPath $FullPath -PathType Container) { + # Files first (deepest first), then the directories themselves. + @(Get-ChildItem -LiteralPath $FullPath -Recurse -Force -ErrorAction SilentlyContinue | + Sort-Object { $_.FullName.Length } -Descending | ForEach-Object FullName) + @($FullPath) + } else { @($FullPath) } + foreach ($t in $targets) { + if (-not (Test-Path -LiteralPath $t)) { continue } # already gone + if ([WinSenior.Native]::MoveFileEx($t, $null, 4)) { $queued++ } # 4 = MOVEFILE_DELAY_UNTIL_REBOOT + } + $queued +} + +# After an age-filtered pass, remove sub-folders that were left empty. +function Remove-EmptyDirectory { + param([string]$Root) + if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return 0 } + $removed = 0 + $dirs = Get-ChildItem -LiteralPath $Root -Directory -Recurse -Force -ErrorAction SilentlyContinue | + Sort-Object { $_.FullName.Length } -Descending + foreach ($d in $dirs) { + if (-not (Test-SafeToDelete $d.FullName)) { continue } + $any = Get-ChildItem -LiteralPath $d.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $any) { + Remove-Item -LiteralPath $d.FullName -Force -ErrorAction SilentlyContinue + if (-not (Test-Path -LiteralPath $d.FullName)) { $removed++ } + } + } + $removed +} + # ===================================================================== # CORE: PATH CLEANUP (honest accounting + real ShouldProcess) # ===================================================================== @@ -225,7 +279,7 @@ function Invoke-PathCleanup { [string]$Description = 'items' ) - $files = 0; [int64]$bytes = 0; $errors = 0 + $files = 0; [int64]$bytes = 0; $errors = 0; $deferred = 0 $cutoff = if ($AgeDays -gt 0) { (Get-Date).AddDays(-$AgeDays) } else { $null } foreach ($spec in $Path) { @@ -251,6 +305,16 @@ function Invoke-PathCleanup { if (-not (Test-Path -LiteralPath $full)) { $files += $count; $bytes += $size } } catch { + # Partial success inside a folder still counts: measure what is left. + if (Test-Path -LiteralPath $full) { + $left = Get-ItemSize (Get-Item -LiteralPath $full -Force -ErrorAction SilentlyContinue) + if ($left -lt $size) { $bytes += ($size - $left) } + if ($DeferLocked -and (Register-DeferredDelete $full) -gt 0) { + $deferred++ + Write-CleanupLog " deferred to reboot: $full" 'Debug' + continue + } + } $errors++ Write-CleanupLog " $full : $($_.Exception.Message)" 'Debug' } @@ -260,9 +324,14 @@ function Invoke-PathCleanup { $files += $count; $bytes += $size } } + + # Age-filtered passes leave empty folder skeletons behind - tidy them. + if ($cutoff -and -not (Test-WhatIfMode) -and ($spec -match '[\*\?]')) { + [void](Remove-EmptyDirectory -Root $container) + } } - [pscustomobject]@{ Files = $files; Bytes = $bytes; Errors = $errors } + [pscustomobject]@{ Files = $files; Bytes = $bytes; Errors = $errors; Deferred = $deferred } } # Stop a set of services, run a body, then restart whatever was running. @@ -435,6 +504,55 @@ function Get-CleanupTaskRegistry { New-CleanupTask ps-modulecache 'PowerShell module analysis cache' DevTools Safe -Paths @( '\AppData\Local\Microsoft\Windows\PowerShell\ModuleAnalysisCache', '\AppData\Local\Microsoft\Windows\PowerShell\StartupProfileData-*') + New-CleanupTask vs 'Visual Studio component & IntelliSense caches' DevTools Safe -Paths @( + '\AppData\Local\Microsoft\VisualStudio\*\ComponentModelCache\*', + '\AppData\Local\Microsoft\VSApplicationInsights\*', + '\AppData\Local\Microsoft\VSCommon\*\SQM\*', + '\AppData\Local\Temp\VSFeedbackIntelliCodeLogs\*', + '\AppData\Local\Temp\VSRemoteControl\*', + '\AppData\Local\Microsoft\Team Foundation\*\Cache\*') + New-CleanupTask python-tools 'Python tool caches (uv / poetry / pipx / pyenv)' DevTools Safe -Paths @( + '\AppData\Local\uv\cache\*', + '\AppData\Local\pypoetry\Cache\*', + '\.cache\pypoetry\*', + '\AppData\Local\pipx\.cache\*', + '\.pyenv\pyenv-win\install_cache\*') + New-CleanupTask java-tools 'JVM build caches (Maven/Gradle wrapper dists, sbt boot, kotlin daemon)' DevTools Safe -Paths @( + '\.m2\wrapper\dists\*', + '\.gradle\daemon\*\*.log', + '\.gradle\wrapper\dists\*\*\*.zip', + '\.sbt\boot\*', + '\AppData\Local\kotlin\daemon\*') + New-CleanupTask dotnet-tools '.NET SDK temp & telemetry caches' DevTools Safe -Paths @( + '\AppData\Local\Temp\.net\*', + '\.dotnet\TelemetryStorageService\*', + '\.dotnet\sdk-advertising\*') + New-CleanupTask node-tools 'Node toolchain caches (node-gyp / electron / bun / deno / Cypress)' DevTools Safe -Paths @( + '\AppData\Local\node-gyp\Cache\*', + '\AppData\Local\electron\Cache\*', + '\AppData\Local\electron-builder\Cache\*', + '\.bun\install\cache\*', + '\AppData\Local\deno\deps\*', + '\AppData\Local\deno\npm\*', + '\AppData\Local\Cypress\Cache\*') + New-CleanupTask git-tools 'GitHub Desktop logs & cache' DevTools Safe -Paths @( + '\AppData\Roaming\GitHub Desktop\logs\*', + '\AppData\Roaming\GitHub Desktop\Cache\*', + '\AppData\Roaming\GitHub Desktop\GPUCache\*') + New-CleanupTask composer-php 'PHP Composer cache' DevTools Safe -Paths @( + '\AppData\Local\Composer\files\*', + '\AppData\Local\Composer\repo\*') + New-CleanupTask ml-caches 'ML model & browser-automation caches (HuggingFace / torch / Playwright / Puppeteer)' DevTools Moderate -DefaultOn $false -Paths @( + '\.cache\huggingface\hub\*', + '\.cache\torch\hub\*', + '\.cache\ms-playwright\*', + '\AppData\Local\ms-playwright\*', + '\.cache\puppeteer\*') + New-CleanupTask docker-logs 'Docker Desktop / WSL logs & caches' DevTools Safe -Paths @( + '\AppData\Local\Docker\log\*', + '\AppData\Roaming\Docker Desktop\Cache\*', + '\AppData\Roaming\Docker Desktop\GPUCache\*', + '\AppData\Local\Temp\wsl-*') # ---------------- Apps / messengers (Safe) ---------------- New-CleanupTask appcache 'Windows app cache' Apps Safe -Paths @( @@ -471,6 +589,65 @@ function Get-CleanupTaskRegistry { '\AppData\Local\Adobe\CameraRaw\Cache\*') New-CleanupTask rdp-cache 'Remote Desktop client bitmap cache' Apps Safe -Paths @( '\AppData\Local\Microsoft\Terminal Server Client\Cache\*') + New-CleanupTask telegram 'Telegram Desktop media cache' Apps Safe -Paths @( + '\AppData\Roaming\Telegram Desktop\tdata\user_data\cache\*', + '\AppData\Roaming\Telegram Desktop\tdata\user_data\media_cache\*', + '\AppData\Roaming\Telegram Desktop\tdata\emoji\*', + '\AppData\Local\Packages\TelegramMessengerLLP.TelegramDesktop_*\LocalCache\Roaming\Telegram Desktop UWP\tdata\user_data\cache\*') + New-CleanupTask whatsapp 'WhatsApp Desktop cache' Apps Safe -Paths @( + '\AppData\Local\Packages\5319275A.WhatsAppDesktop_*\LocalCache\*', + '\AppData\Roaming\WhatsApp\Cache\*', + '\AppData\Roaming\WhatsApp\Code Cache\*', + '\AppData\Roaming\WhatsApp\GPUCache\*') + New-CleanupTask messengers 'Zoom / Skype / Signal / Viber caches & logs' Apps Safe -Paths @( + '\AppData\Roaming\Zoom\logs\*', + '\AppData\Roaming\Zoom\data\Cache\*', + '\AppData\Roaming\Microsoft\Skype for Desktop\Cache\*', + '\AppData\Roaming\Microsoft\Skype for Desktop\Code Cache\*', + '\AppData\Roaming\Microsoft\Skype for Desktop\logs\*', + '\AppData\Roaming\Signal\logs\*', + '\AppData\Roaming\Signal\Cache\*', + '\AppData\Roaming\ViberPC\*\Cache\*') + # Moderate: catches every Chromium/Electron app, not just the ones listed by name. + New-CleanupTask electron-generic 'Generic Electron/Chromium app caches (any app in AppData)' Apps Moderate -Paths @( + '\AppData\Roaming\*\Cache\*', + '\AppData\Roaming\*\Code Cache\*', + '\AppData\Roaming\*\GPUCache\*', + '\AppData\Roaming\*\DawnCache\*', + '\AppData\Roaming\*\DawnGraphiteCache\*', + '\AppData\Roaming\*\DawnWebGPUCache\*', + '\AppData\Roaming\*\Service Worker\CacheStorage\*', + '\AppData\Local\*\GPUCache\*', + '\AppData\Local\*\DawnCache\*') + New-CleanupTask media-players 'Media player caches (VLC / iTunes / Windows Media Player / Plex)' Apps Safe -Paths @( + '\AppData\Roaming\vlc\art\*', + '\AppData\Local\Microsoft\Media Player\Art Cache\*', + '\AppData\Local\Microsoft\Media Player\Transcoded Files Cache\*', + '\AppData\Local\Plex Media Server\Cache\*', + '\AppData\Local\Plex\Cache\*') + New-CleanupTask uwp-caches 'Store app (UWP) temp state & local caches' Apps Safe -Paths @( + '\AppData\Local\Packages\*\TempState\*', + '\AppData\Local\Packages\*\LocalCache\Local\Microsoft\Windows\INetCache\*', + '\AppData\Local\Packages\*\AC\Microsoft\Internet Explorer\DOMStore\*', + '\AppData\Local\Packages\Microsoft.Windows.Photos_*\LocalState\PhotosAppCache\*', + '\AppData\Local\Packages\Microsoft.WindowsStore_*\LocalCache\*', + '\AppData\Local\Packages\Microsoft.Windows.ContentDeliveryManager_*\LocalCache\*') + New-CleanupTask notifications 'Notification image cache & Explorer side caches' Apps Safe -Paths @( + '\AppData\Local\Microsoft\Windows\Notifications\wpnidm\*', + '\AppData\Local\Microsoft\Windows\Caches\*', + '\AppData\Local\Microsoft\Windows\SchCache\*', + '\AppData\Local\Microsoft\Windows\WebCache.old\*') + New-CleanupTask gpu-apps 'GPU vendor app caches & telemetry (NVIDIA App / GeForce Experience / AMD / Intel)' Apps Safe -Paths @( + '\AppData\Local\NVIDIA Corporation\NVIDIA App\CefCache\*', + '\AppData\Local\NVIDIA Corporation\NVIDIA GeForce Experience\CefCache\*', + '\AppData\Local\NVIDIA Corporation\NvTelemetry\*', + '%ProgramData%\NVIDIA Corporation\NvTelemetry\*', + '%ProgramData%\NVIDIA Corporation\Downloader\*', + '\AppData\Local\AMD\CN\Cache\*', + '\AppData\Local\AMD\DxCache\*', + '\AppData\Local\AMD\GLCache\*', + '\AppData\Local\AMD\VkCache\*', + '\AppData\Local\Intel\ShaderCache\*') # ---------------- Games (launcher caches, Safe) ---------------- New-CleanupTask game-caches 'Game launcher caches (Steam/Epic/Battle.net/GOG)' Games Safe -Paths @( @@ -481,6 +658,32 @@ function Get-CleanupTaskRegistry { '\AppData\Local\Battle.net\Cache\*', '%ProgramData%\Battle.net\Agent\data\cache\*', '\AppData\Local\GOG.com\Galaxy\webcache\*') + New-CleanupTask game-logs 'Game launcher logs & crash dumps (Steam/Epic/EA/Ubisoft/Riot/Xbox)' Games Safe -Paths @( + '%ProgramFiles(x86)%\Steam\logs\*', + '%ProgramFiles(x86)%\Steam\dumps\*', + '\AppData\Local\EpicGamesLauncher\Saved\Logs\*', + '\AppData\Local\EpicGamesLauncher\Saved\Crashes\*', + '\AppData\Local\Electronic Arts\EA Desktop\Logs\*', + '\AppData\Local\Electronic Arts\EA Desktop\cache\*', + '\AppData\Local\Ubisoft Game Launcher\logs\*', + '\AppData\Local\Ubisoft Game Launcher\cache\*', + '\AppData\Local\Riot Games\Riot Client\Logs\*', + '%ProgramData%\Riot Games\Logs\*', + '\AppData\Local\Packages\Microsoft.GamingApp_*\LocalCache\*', + '\AppData\Local\Packages\Microsoft.XboxGamingOverlay_*\TempState\*') + New-CleanupTask game-engines 'Game engine caches (Unreal DDC / Unity / Godot)' Games Moderate -Paths @( + '\AppData\Local\UnrealEngine\Common\DerivedDataCache\*', + '\AppData\Local\UnrealEngine\*\Saved\Logs\*', + '\AppData\Local\UnrealEngine\*\Saved\Crashes\*', + '\AppData\Local\Unity\cache\*', + '\AppData\LocalLow\Unity\Caches\*', + '\AppData\Roaming\Godot\shader_cache\*', + '\AppData\Roaming\Godot\logs\*') + New-CleanupTask game-shaders 'Per-game shader caches on every disk (Steam libraries / Unity games)' Games Aggressive -Paths @( + 'SteamLibrary\steamapps\shadercache\*', + 'Games\Steam\steamapps\shadercache\*', + 'Steam\steamapps\shadercache\*', + '\AppData\LocalLow\*\*\ShaderCache\*') # ---------------- System (Safe / Moderate / Aggressive) ---------------- New-CleanupTask temp-user 'User temp files' System Safe -Paths @( @@ -512,6 +715,88 @@ function Get-CleanupTaskRegistry { New-CleanupTask deliveryopt 'Delivery Optimization cache' System Safe -Paths @( '%WINDIR%\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization\*', '%ProgramData%\Microsoft\Windows\DeliveryOptimization\*') + New-CleanupTask svc-temp 'Service-account temp folders (LocalService / NetworkService / system profile)' System Safe -Paths @( + '%WINDIR%\ServiceProfiles\LocalService\AppData\Local\Temp\*', + '%WINDIR%\ServiceProfiles\NetworkService\AppData\Local\Temp\*', + '%WINDIR%\System32\config\systemprofile\AppData\Local\Temp\*', + '%WINDIR%\SysWOW64\config\systemprofile\AppData\Local\Temp\*', + '%WINDIR%\ServiceProfiles\LocalService\AppData\Local\Microsoft\Windows\INetCache\*', + '%WINDIR%\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\INetCache\*', + '%WINDIR%\System32\config\systemprofile\AppData\Local\Microsoft\Windows\INetCache\*', + '%SystemDrive%\Users\Default\AppData\Local\Temp\*') + New-CleanupTask upgrade-logs 'Windows setup / upgrade / servicing logs (Panther, CBS, USO, WindowsUpdate.log)' System Safe -Paths @( + '%WINDIR%\Panther\*.log', + '%WINDIR%\Panther\*.xml', + '%WINDIR%\Panther\UnattendGC\*', + '%WINDIR%\Logs\CBS\*.log', + '%WINDIR%\Logs\CBS\*.cab', + '%WINDIR%\Logs\MoSetup\*', + '%WINDIR%\Logs\WindowsUpdate\*', + '%WINDIR%\Logs\NetSetup\*', + '%WINDIR%\Logs\waasmedic\*', + '%WINDIR%\Logs\SIH\*', + '%ProgramData%\USOShared\Logs\*', + '%WINDIR%\WindowsUpdate.log', + '%WINDIR%\setupact.log', + '%WINDIR%\setuperr.log', + '%WINDIR%\PFRO.log') + New-CleanupTask win-misc 'Windows misc caches (Offline Web Pages, Defender scan cache, Search temp, PerfLogs)' System Safe -Paths @( + '%WINDIR%\Offline Web Pages\*', + '%ProgramData%\Microsoft\Windows Defender\Scans\mpcache-*', + '%ProgramData%\Microsoft\Windows Defender\Scans\MetaStore\*', + '%ProgramData%\Microsoft\Windows Defender\Scans\FilesStash\*', + '%ProgramData%\Microsoft\Windows\Caches\*', + '%ProgramData%\Microsoft\Search\Data\Temp\*', + '%SystemDrive%\PerfLogs\*') + New-CleanupTask bits-cache 'BITS transfer queue (stuck / partial background downloads)' System Moderate -StopServices @('BITS') -Paths @( + '%ProgramData%\Microsoft\Network\Downloader\*.tmp', + '%ProgramData%\Microsoft\Network\Downloader\qmgr*.dat', + '%ProgramData%\Microsoft\Network\Downloader\qmgr.db', + '%ProgramData%\Microsoft\Network\Downloader\qmgr.jfm') + New-CleanupTask dns-flush 'Flush DNS resolver, ARP & NetBIOS caches' System Safe -Action { + Invoke-NativeStep 'ipconfig /flushdns' { & ipconfig.exe /flushdns *>$null } | Out-Null + Invoke-NativeStep 'arp -d *' { & arp.exe -d * *>$null } | Out-Null + Invoke-NativeStep 'nbtstat -R' { & nbtstat.exe -R *>$null } | Out-Null + $null + } + New-CleanupTask store-reset 'Reset Microsoft Store cache (WSReset, silent)' System Safe -Action { + $exe = Join-Path $env:WINDIR 'System32\WSReset.exe' + if (-not (Test-Path $exe)) { return $null } + Invoke-NativeStep 'WSReset.exe -i' { + $p = Start-Process -FilePath $exe -ArgumentList '-i' -WindowStyle Hidden -PassThru + if (-not $p.WaitForExit(60000)) { $p.Kill() } + } | Out-Null + $null + } + New-CleanupTask memory-standby 'Purge standby memory list & trim working sets (frees RAM, not disk)' System Safe -Action { + # Same documented NtSetSystemInformation(SystemMemoryListInformation) call RAMMap uses. + if (Test-WhatIfMode) { Write-CleanupLog '[WhatIf] would purge standby memory list' 'WhatIf'; return $null } + if (-not ('WinSenior.Memory' -as [type])) { + Add-Type -Namespace WinSenior -Name Memory -MemberDefinition @' +[DllImport("ntdll.dll")] public static extern int NtSetSystemInformation(int cls, ref int info, int len); +[DllImport("advapi32.dll", SetLastError=true)] public static extern bool OpenProcessToken(IntPtr h, int acc, out IntPtr tok); +[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] public static extern bool LookupPrivilegeValue(string s, string n, out long luid); +[DllImport("advapi32.dll", SetLastError=true)] public static extern bool AdjustTokenPrivileges(IntPtr tok, bool dis, ref TOKP np, int len, IntPtr prev, IntPtr ret); +[StructLayout(LayoutKind.Sequential)] public struct TOKP { public int Count; public long Luid; public int Attr; } +public static bool Enable(string name) { + IntPtr tok; if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle, 0x28, out tok)) return false; + TOKP tp; tp.Count = 1; tp.Attr = 2; if (!LookupPrivilegeValue(null, name, out tp.Luid)) return false; + return AdjustTokenPrivileges(tok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); +} +'@ -ErrorAction SilentlyContinue + } + if (-not ('WinSenior.Memory' -as [type])) { return $null } + [void][WinSenior.Memory]::Enable('SeProfileSingleProcessPrivilege') + [void][WinSenior.Memory]::Enable('SeIncreaseQuotaPrivilege') + $before = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory * 1KB + foreach ($cmd in 4, 2) { # 4 = purge standby list, 2 = empty working sets + $v = [int]$cmd + [void][WinSenior.Memory]::NtSetSystemInformation(80, [ref]$v, 4) + } + $after = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory * 1KB + Write-CleanupLog (" RAM freed: {0}" -f (Format-FileSize ([Math]::Max([int64]0, [int64]($after - $before))))) 'Success' + $null + } New-CleanupTask recent 'Recent items & jump lists' System Moderate -DefaultOn $true -Paths @( '\AppData\Roaming\Microsoft\Windows\Recent\*') New-CleanupTask fontcache 'Font cache' System Moderate -StopServices @('FontCache') -Paths @( @@ -519,6 +804,27 @@ function Get-CleanupTaskRegistry { New-CleanupTask winlogs 'Windows log files' System Moderate -Paths @('%WINDIR%\Logs\*') New-CleanupTask prefetch 'Prefetch (rebuilt by Windows)' System Aggressive -Paths @( '%WINDIR%\Prefetch\*') + New-CleanupTask search-index 'Rebuild Windows Search index (deletes Windows.edb, re-indexes in background)' System Aggressive -DefaultOn $false -Action { + $db = @("$env:ProgramData\Microsoft\Search\Data\Applications\Windows\Windows.edb", + "$env:ProgramData\Microsoft\Search\Data\Applications\Windows\Windows.db") + $existing = @($db | Where-Object { Test-Path -LiteralPath $_ }) + if (-not $existing) { Write-CleanupLog ' no search index database found' 'Debug'; return $null } + $size = ($existing | ForEach-Object { (Get-Item -LiteralPath $_ -Force).Length } | Measure-Object -Sum).Sum + if (Test-WhatIfMode) { + Write-CleanupLog "[WhatIf] would delete search index ($(Format-FileSize $size)) and trigger a rebuild" 'WhatIf' + return [pscustomobject]@{ Files = $existing.Count; Bytes = $size; Errors = 0 } + } + Use-StoppedService -Name 'WSearch' -Body { + $ok = 0; $err = 0 + foreach ($f in $existing) { + Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue + if (Test-Path -LiteralPath $f) { $err++ } else { $ok++ } + } + # SetupCompletedSuccessfully=0 makes the indexer rebuild from scratch on next start. + Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Search' -Name SetupCompletedSuccessfully -Value 0 -Type DWord -ErrorAction SilentlyContinue + [pscustomobject]@{ Files = $ok; Bytes = $(if ($err) { 0 } else { $size }); Errors = $err } + } + } New-CleanupTask old-drivers 'Remove superseded driver packages (pnputil)' System Dangerous -Action { # Enumeration is read-only, but loading the DISM module (triggered by Get-Command # or Get-WindowsDriver) runs Set-Alias under the GLOBAL WhatIf preference. Toggle @@ -590,6 +896,57 @@ function Get-CleanupTaskRegistry { '%ProgramData%\Microsoft\Windows Defender\Scans\History\Results\*') New-CleanupTask livekernel 'Live kernel crash dumps (driver/GPU TDR)' Logs Safe -Paths @( '%WINDIR%\LiveKernelReports\*.dmp') + New-CleanupTask diag-telemetry 'Diagnostics & telemetry caches (Diagnosis, ETL traces, SleepStudy, WDI)' Logs Moderate -StopServices @('DiagTrack') -Paths @( + '%ProgramData%\Microsoft\Diagnosis\ETLLogs\*', + '%ProgramData%\Microsoft\Diagnosis\DownloadedSettings\*', + '%ProgramData%\Microsoft\Diagnosis\*.rbs', + '%WINDIR%\System32\LogFiles\WMI\*.etl', + '%WINDIR%\System32\LogFiles\WMI\RtBackup\*', + '%WINDIR%\System32\SleepStudy\*.etl', + '%WINDIR%\System32\WDI\LogFiles\*', + '%ProgramData%\Microsoft\Windows\Power\*.etl', + '\AppData\Local\Microsoft\Windows\PowerShell\CommandAnalysis\*') + New-CleanupTask app-logs 'Third-party app logs (Adobe, Autodesk, Corsair, Logitech, Razer, Intel, PowerToys, Terminal)' Logs Safe -Paths @( + '%ProgramData%\Adobe\*\Logs\*', + '\AppData\Local\Adobe\*\Logs\*', + '\AppData\Roaming\Adobe\*\Logs\*', + '%ProgramData%\Autodesk\*\Logs\*', + '\AppData\Local\Autodesk\*\Logs\*', + '\AppData\Roaming\Corsair\CUE\logs\*', + '\AppData\Local\LGHUB\logs\*', + '\AppData\Local\Razer\Synapse3\Log\*', + '%ProgramData%\Razer\Synapse3\Logs\*', + '%ProgramData%\Intel\*\Logs\*', + '\AppData\Local\Microsoft\PowerToys\Logs\*', + '\AppData\Local\Packages\Microsoft.WindowsTerminal_*\LocalState\*.log', + '\AppData\Local\Temp\*.log', + '\AppData\Local\Temp\*.etl', + '\AppData\Local\Temp\*.dmp') + New-CleanupTask installer-leftovers 'Installer leftovers (MSI temp, Squirrel/NSIS/Inno temp, Chrome/Edge updater downloads)' Logs Safe -Paths @( + '%WINDIR%\Installer\*.tmp', + '\AppData\Local\SquirrelTemp\*', + '\AppData\Local\Temp\nsis*', + '\AppData\Local\Temp\7z*', + '\AppData\Local\Temp\is-*.tmp', + '\AppData\Local\Temp\{*}', + '\AppData\Local\Google\Update\Download\*', + '\AppData\Local\Google\Update\Install\*', + '\AppData\Local\Microsoft\EdgeUpdate\Download\*', + '\AppData\Local\Microsoft\EdgeUpdate\Install\*', + '%ProgramFiles(x86)%\Google\Update\Download\*', + '%ProgramFiles(x86)%\Microsoft\EdgeUpdate\Download\*') + New-CleanupTask empty-dirs 'Remove empty folders left behind in temp locations' Logs Safe -Action { + $roots = Expand-TaskPath @('\AppData\Local\Temp', '%WINDIR%\Temp') + $n = 0 + foreach ($r in $roots) { + if (-not (Test-Path -LiteralPath $r)) { continue } + if (Test-WhatIfMode) { + $n += @(Get-ChildItem -LiteralPath $r -Directory -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1) }).Count + } else { $n += Remove-EmptyDirectory -Root $r } + } + [pscustomobject]@{ Files = $n; Bytes = 0; Errors = 0 } + } New-CleanupTask srum-db 'Network/app usage telemetry DB (SRUM)' Logs Moderate ` -StopServices @('DPS') -Paths @('%WINDIR%\System32\sru\*') New-CleanupTask eventtranscript 'Diagnostic telemetry database (EventTranscript)' Logs Moderate ` @@ -650,6 +1007,33 @@ function Get-CleanupTaskRegistry { $total } + New-CleanupTask hiberfil 'Disable hibernation & delete hiberfil.sys (also disables Fast Startup)' Updates Dangerous -DefaultOn $false -Action { + $f = "$env:SystemDrive\hiberfil.sys" + if (-not (Test-Path -LiteralPath $f)) { Write-CleanupLog ' hibernation already off' 'Debug'; return $null } + $size = (Get-Item -LiteralPath $f -Force).Length + if (Test-WhatIfMode) { + Write-CleanupLog "[WhatIf] would run powercfg /hibernate off ($(Format-FileSize $size))" 'WhatIf' + return [pscustomobject]@{ Files = 1; Bytes = $size; Errors = 0 } + } + $ok = Invoke-NativeStep 'powercfg /hibernate off' { & powercfg.exe /hibernate off *>$null } + [pscustomobject]@{ Files = 1; Bytes = $(if ($ok) { $size } else { 0 }); Errors = $(if ($ok) { 0 } else { 1 }) } + } + New-CleanupTask shadow-old 'Delete all but the newest restore point / shadow copy (vssadmin)' Updates Dangerous -DefaultOn $false -Action { + $shadows = @(Get-CimInstance Win32_ShadowCopy -ErrorAction SilentlyContinue | Sort-Object InstallDate) + if ($shadows.Count -le 1) { Write-CleanupLog ' nothing to prune' 'Debug'; return $null } + $old = @($shadows | Select-Object -First ($shadows.Count - 1)) + if (Test-WhatIfMode) { + Write-CleanupLog "[WhatIf] would delete $($old.Count) older shadow copies" 'WhatIf' + return [pscustomobject]@{ Files = $old.Count; Bytes = 0; Errors = 0 } + } + $n = 0 + foreach ($sc in $old) { + & vssadmin.exe delete shadows /shadow=$($sc.ID) /quiet *>$null + if ($LASTEXITCODE -eq 0) { $n++ } + } + [pscustomobject]@{ Files = $n; Bytes = 0; Errors = ($old.Count - $n) } + } + # ---------------- Optimization (slow; skipped by -SkipOptimization) ---------------- New-CleanupTask dism-analyze 'Analyze component store (report only)' Optimization Safe -DefaultOn $true -Action { if (Test-WhatIfMode) { Write-CleanupLog '[WhatIf] would run DISM /AnalyzeComponentStore' 'WhatIf'; return $null } @@ -737,9 +1121,11 @@ function Invoke-CleanupTask { $script:TotalBytes += [int64]$result.Bytes $script:TotalFiles += [int]$result.Files $script:TotalErrors += [int]$result.Errors + $def = if ($result.PSObject.Properties.Name -contains 'Deferred') { [int]$result.Deferred } else { 0 } + $script:TotalDeferred += $def $script:Stats.Add([pscustomobject]@{ - Task = $Task.Id; Category = $Task.Category - Files = [int]$result.Files; Bytes = [int64]$result.Bytes; Errors = [int]$result.Errors + Task = $Task.Id; Name = $Task.Name; Category = $Task.Category; Risk = $Task.Risk + Files = [int]$result.Files; Bytes = [int64]$result.Bytes; Errors = [int]$result.Errors; Deferred = $def }) if ($result.Bytes -gt 0 -or $result.Files -gt 0) { $verb = if (Test-WhatIfMode) { 'would free' } else { 'freed' } @@ -771,7 +1157,11 @@ function Show-CleanupSummary { Write-CleanupLog '' 'Info' Write-CleanupLog ("{0}: {1} ({2} items)" -f $verb, (Format-FileSize $script:TotalBytes), $script:TotalFiles) 'Success' if ($script:TotalErrors -gt 0) { - Write-CleanupLog "Errors (locked/in-use items): $script:TotalErrors" 'Warning' + $hint = if ($DeferLocked) { '' } else { ' (use -DeferLocked to remove them at next reboot)' } + Write-CleanupLog "Errors (locked/in-use items): $script:TotalErrors$hint" 'Warning' + } + if ($script:TotalDeferred -gt 0) { + Write-CleanupLog "Scheduled for deletion at next reboot: $script:TotalDeferred item(s)" 'Info' } Write-CleanupLog ("Duration: {0:N1}s Log: {1}" -f $dur.TotalSeconds, $LogPath) 'Info' } @@ -784,6 +1174,7 @@ function Write-CleanupReport { TotalFreed = (Format-FileSize $script:TotalBytes) TotalFiles = $script:TotalFiles TotalErrors = $script:TotalErrors + TotalDeferred = $script:TotalDeferred } ` -Items $script:Stats ` -LogAction { param($m, $l) Write-CleanupLog $m $l } @@ -823,6 +1214,7 @@ SELECTION -Drives Local disks for drive-level cleanup, e.g. -Drives C,D (default: all local disks) -SkipOptimization,-so Skip the slow SFC/DISM category -MaxAgeDays Only delete files older than n days + -DeferLocked, -dl Schedule locked/in-use files for deletion at next reboot SAFETY -WhatIf / -DryRun,-dr Preview only, change nothing (real ShouldProcess) @@ -849,7 +1241,7 @@ EXAMPLES function Start-WindowsCleanup { $modeText = if (Test-WhatIfMode) { 'DryRun' } else { 'Live' } $scopeText = if ($CurrentUserOnly) { 'current user' } else { 'all users' } - Write-CleanupLog 'Windows System Cleaner v6.0' 'Step' + Write-CleanupLog "Windows System Cleaner v$(Get-WinSeniorVersion)" 'Step' Write-CleanupLog ("PowerShell {0} | Mode: {1} | Scope: {2}" -f $PSVersionTable.PSVersion, $modeText, $scopeText) 'Info' if (-not (Test-AdminPrivileges)) { diff --git a/Optimize-Windows-Senior.ps1 b/Optimize-Windows-Senior.ps1 index f4d8d3e..a3c9ebf 100644 --- a/Optimize-Windows-Senior.ps1 +++ b/Optimize-Windows-Senior.ps1 @@ -17,7 +17,7 @@ .NOTES Author : denfry (https://github.com/denfry/WindowsCleaner) - Version : 6.1.0 + Version : 6.2.0 Requires: PowerShell 5.1+ (Windows). Administrator rights. .EXAMPLE diff --git a/README.md b/README.md index eb50856..1411d60 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Windows System Cleaner and Optimizer ๐Ÿงน [![CI](https://github.com/denfry/WindowsCleaner/actions/workflows/ci.yml/badge.svg)](https://github.com/denfry/WindowsCleaner/actions/workflows/ci.yml) -[![Version](https://img.shields.io/badge/version-6.1.0-blue.svg)](https://github.com/denfry/WindowsCleaner) +[![Version](https://img.shields.io/badge/version-6.2.0-blue.svg)](https://github.com/denfry/WindowsCleaner) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![PowerShell](https://img.shields.io/badge/powershell-5.1%2B%20%7C%207%2B-blue.svg)](https://learn.microsoft.com/powershell/) [![Platform](https://img.shields.io/badge/platform-Windows%2010%20%7C%2011-blue.svg)](https://www.microsoft.com/windows/) @@ -10,7 +10,7 @@ > a small engine resolves what to run, reclaims disk space, and deletes through > PowerShell's `ShouldProcess` โ€” so **`-WhatIf` is real**, not a parallel code path. -It cleans 63 targets across browsers, developer tools, apps, games, system caches, every +It cleans 97 targets across browsers, developer tools, apps, games, system caches, every local disk, logs, Windows Update, and driver leftovers โ€” all from one declarative registry with a real dry-run mode and a hard safety guard. A companion optimization engine applies 49 reversible system tweaks, a troubleshooting engine scans for 25 common problems and @@ -23,7 +23,8 @@ safety net. Works on desktops, laptops, and Windows Server, and runs unattended ## Contents -- [The menu โ€” one command](#one-command--the-menu) +- [Desktop app โ€” one command](#one-command--the-desktop-app) +- [The console menu](#the-console-menu) - [Highlights](#highlights) - [Quick start](#quick-start) - [Parameters](#parameters) @@ -37,9 +38,45 @@ safety net. Works on desktops, laptops, and Windows Server, and runs unattended - [Common issues (FAQ)](#common-issues-faq) - [License](#license) -## One command โ€” the menu +## One command โ€” the desktop app -`WinSenior.ps1` is the single entry point. Run it (it self-elevates) and an interactive +Clone, run one file, done. Nothing is downloaded or installed: the window is WPF, which +ships with every Windows 10/11. + +```powershell +git clone https://github.com/denfry/WindowsCleaner +cd WindowsCleaner +.\WinSenior.cmd # or double-click it in Explorer +``` + +`WinSenior.cmd` removes the Mark-of-the-Web from the scripts (zip downloads), asks for +Administrator once (UAC) and opens **Windows Senior** without a console window. +`.\WinSenior.ps1 -Gui` does the same from PowerShell; `.\WinSenior.cmd console` opens +the arrow-key menu instead. + +The app has six pages: + +- **Disk cleanup** โ€” every task with its category and risk badge. *Scan* runs the real + `-WhatIf` dry run and fills a "Would free" column per task; *Clean now* runs the checked + tasks. Options: current user only, restore point, delete locked files at reboot, skip + SFC/DISM, minimum file age. Dangerous tasks are never pre-checked and ask for confirmation. +- **Optimize** โ€” all tweaks with their live *applied / not applied* state; preview or apply, + restore point first, every applied tweak is backed up. +- **Troubleshoot** โ€” read-only scan with OK / Warn / Fail per check and the detail text; + tick the problems to repair and press *Fix selected*, or auto-fix everything. +- **Undo & restore** โ€” list of optimization backup manifests (undo newest or a chosen one), + create a System Restore point, open System Restore, open log folders. +- **Schedule** โ€” install/remove the recurring Task Scheduler jobs and see their next run. +- **About** โ€” versions, counts, paths. + +Engine output streams into the log panel at the bottom (resizable, saveable, cancellable). +The app never re-implements deletion: it launches the same engine scripts with parameters, +so `-WhatIf`, the safety guard, restore points and undo are exactly the ones documented below. +Selections and options persist in `%ProgramData%\WinSenior\gui-settings.json`. + +## The console menu + +`WinSenior.ps1` is the console entry point. Run it (it self-elevates) and an interactive menu opens with detailed screens for cleanup, optimization, troubleshooting, undo, a restore point and a task/tweak/check listing. It drives all three engines, so every action keeps real `-WhatIf`, the safety guard and per-tweak undo. @@ -63,7 +100,12 @@ characters (ASCII `+ - |` borders instead). - **Real restore point.** `Checkpoint-Computer` actually creates a System Restore point (and clears the 24-hour throttle first) โ€” non-interactive, safe for automation. - **Honest accounting.** Reclaimed bytes are summed from items that were actually - removed, not estimated and not counted from log lines. + removed, not estimated and not counted from log lines. Partially-deleted folders count + only what actually went away. +- **Locked files don't win.** `-DeferLocked` queues anything in use for deletion at the + next reboot through `MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT)` โ€” the same mechanism + Windows Installer uses โ€” instead of reporting an error. Age-filtered runs also prune the + empty folder skeletons they leave behind. - **A safety guard that can't be argued with.** `Test-SafeToDelete` refuses to operate on drive roots, `%WINDIR%`, `%USERPROFILE%`, `C:\Users`, `System32`, or any path shallower than two levels โ€” defends against a bad registry entry or an unexpanded variable. @@ -115,6 +157,7 @@ Run as Administrator. PowerShell 5.1+ (7+ recommended). | `-NoRestorePoint` | `-nrp` | Skip the restore point that is otherwise created first | | `-SkipOptimization` | `-so` | Skip the slow SFC / DISM category | | `-MaxAgeDays ` | | Only delete files older than n days | +| `-DeferLocked` | `-dl` | Locked/in-use files are scheduled for deletion at next reboot (MoveFileEx) instead of counted as errors | | `-LogPath ` | | Text log (default `%TEMP%\WindowsCleanup.log`) | | `-ReportPath ` | | Write a machine-readable JSON report | | `-ListTasks` | | Print the task registry and exit | @@ -139,17 +182,35 @@ task on regardless of tier. - **Browsers** โ€” Chrome, Edge, Firefox, Opera, Yandex, Brave (all profiles, cache / code cache / GPU cache / service-worker cache). - **DevTools** โ€” npm, pip, Yarn, NuGet http cache, Gradle, VS Code, JetBrains - (caches/logs/temp), Nuitka build cache, Go build cache, Dart/Flutter Pub cache, - package-manager caches (winget, Chocolatey, Scoop, conda, cargo, Go module cache), - `pnpm store prune`, and `docker system prune` (each skipped if the tool isn't installed). -- **Apps** โ€” Windows app/UWP caches, Microsoft Teams (classic + new), Discord, Slack, - Spotify, Office document/web cache, OneDrive logs, Adobe media & Camera Raw cache. -- **Games** โ€” launcher caches for Steam (shader/http/html), Epic, Battle.net, GOG. -- **System** โ€” user/Windows temp, WinINet cache, thumbnail & icon cache, GPU/D3D shader - cache, GPU driver installer leftovers (NVIDIA/AMD), WebCache, Delivery Optimization, - recent items, font cache, Windows logs, prefetch, and removal of superseded driver - packages via pnputil (Dangerous; boot-critical and in-use drivers are never touched). -- **Logs** additions โ€” setup logs (Panther/setupapi) and Defender scan history. + (caches/logs/temp), Visual Studio (ComponentModelCache, IntelliCode logs, telemetry), + Nuitka, Go build cache, Dart/Flutter Pub cache, package-manager caches (winget, Chocolatey, + Scoop, conda, cargo, Go module cache), Python tools (uv, poetry, pipx, pyenv), JVM + wrappers (Maven/Gradle dists, sbt boot, kotlin daemon), .NET SDK temp & telemetry, Node + toolchain (node-gyp, electron, bun, deno, Cypress), GitHub Desktop, Composer, Docker + Desktop/WSL logs, `pnpm store prune`, `docker system prune`, and โ€” off by default โ€” ML model + & browser-automation caches (HuggingFace, torch, Playwright, Puppeteer). +- **Apps** โ€” Windows app/UWP caches & temp state, Microsoft Teams (classic + new), Discord, + Slack, Spotify, Telegram, WhatsApp, Zoom/Skype/Signal/Viber, a generic sweep of every + Electron/Chromium app cache under AppData (Moderate), media players (VLC, WMP, Plex), Office + document/web cache, OneDrive logs, Adobe media & Camera Raw cache, RDP bitmap cache, + notification image cache, GPU vendor apps (NVIDIA App / GeForce Experience / AMD / Intel). +- **Games** โ€” launcher caches for Steam (shader/http/html), Epic, Battle.net, GOG; launcher + logs & crash dumps (Steam, Epic, EA, Ubisoft, Riot, Xbox); engine caches (Unreal DDC, Unity, + Godot); per-game shader caches on every disk (Aggressive). +- **System** โ€” user/Windows temp, service-account temps (LocalService / NetworkService / + system profile), WinINet cache, thumbnail & icon cache, GPU/D3D shader cache, GPU driver + installer leftovers (NVIDIA/AMD), WebCache, Delivery Optimization, BITS transfer queue, + setup/upgrade/servicing logs (Panther, CBS, USO, WindowsUpdate.log), Offline Web Pages, + Defender scan cache, PerfLogs, recent items, font cache, Windows logs, prefetch, DNS/ARP/ + NetBIOS flush, silent Store cache reset, standby-memory purge (frees RAM), Windows Search + index rebuild (Aggressive, off by default), and removal of superseded driver packages via + pnputil (Dangerous; boot-critical and in-use drivers are never touched). +- **Logs** additions โ€” setup logs (Panther/setupapi), Defender scan history, diagnostics & + telemetry caches (Diagnosis, ETL traces, SleepStudy, WDI), third-party app logs (Adobe, + Autodesk, Corsair, Logitech, Razer, Intel, PowerToys, Terminal), installer leftovers (MSI + temp, Squirrel/NSIS/Inno, Chrome/Edge updater downloads), empty-folder pruning in temp. +- **Updates** additions (Dangerous, off by default) โ€” disable hibernation & delete + `hiberfil.sys`; delete all but the newest restore point / shadow copy. - **Disks** โ€” drive-level cleanup across **every local disk** (C:, D:, E: โ€ฆ): per-drive `Temp`/`tmp` scratch folders and CHKDSK `FOUND.*` fragments. Recycle Bins on all drives are emptied by the Logs task. Restrict with `-Drives C,D`. @@ -286,7 +347,7 @@ Every engine's `-ReportPath` writes the same envelope, so one parser reads them ```json { - "Tool": "WinSenior", "Version": "6.1.0", "Engine": "Cleanup", + "Tool": "WinSenior", "Version": "6.2.0", "Engine": "Cleanup", "Host": "PC01", "Timestamp": "2026-06-24T03:00:11", "Mode": "Live", "RestorePoint": true, "DurationSec": 42.3, "Summary": { "TotalFreed": "1.20 GB", "TotalFiles": 8123, "TotalErrors": 2 }, diff --git a/Repair-Windows-Senior.ps1 b/Repair-Windows-Senior.ps1 index 2761b5c..29b8bc1 100644 --- a/Repair-Windows-Senior.ps1 +++ b/Repair-Windows-Senior.ps1 @@ -15,7 +15,7 @@ .NOTES Author : denfry (https://github.com/denfry/WindowsCleaner) - Version : 6.1.0 + Version : 6.2.0 Requires: PowerShell 5.1+ (Windows). Administrator rights. .EXAMPLE diff --git a/WinSenior.Common.ps1 b/WinSenior.Common.ps1 index ed11a99..d9a106b 100644 --- a/WinSenior.Common.ps1 +++ b/WinSenior.Common.ps1 @@ -14,7 +14,7 @@ .NOTES Author : denfry (https://github.com/denfry/WindowsCleaner) - Version : 6.1.0 + Version : 6.2.0 #> # ===================================================================== @@ -115,7 +115,7 @@ function New-WinSeniorRestorePoint { # DurationSec; engine-specific counters go in Summary, the per-unit list # in Items. No-op without -ReportPath. # ===================================================================== -function Get-WinSeniorVersion { '6.1.0' } +function Get-WinSeniorVersion { '6.2.0' } function Write-WinSeniorReport { param( diff --git a/WinSenior.Gui.ps1 b/WinSenior.Gui.ps1 new file mode 100644 index 0000000..6acb0d0 --- /dev/null +++ b/WinSenior.Gui.ps1 @@ -0,0 +1,1017 @@ +<# +.SYNOPSIS + Windows Senior - desktop (WPF) application for the cleanup, optimization and + troubleshooting engines. + +.DESCRIPTION + A native Windows window built on WPF (ships with every Windows 10/11, nothing to + install). It drives the three engines exactly like the console menu does - by + launching them with parameters - so every action keeps the engines' real -WhatIf, + the safety guard, restore points and per-tweak undo. Engine output streams into + the log panel; JSON reports feed the per-task size and health columns. + + Normally started through WinSenior.cmd (double-click) or WinSenior.ps1 -Gui; + both elevate first. Run directly with -NoElevate to skip the UAC prompt. + +.NOTES + Author : denfry (https://github.com/denfry/WindowsCleaner) + Requires: PowerShell 5.1+ (Windows), .NET Framework 4.x (built in). +#> + +#Requires -Version 5.1 + +[CmdletBinding()] +param( + [switch]$NoElevate +) + +$ErrorActionPreference = 'Stop' +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { } + +# ===================================================================== +# LOCATE ENGINES + ELEVATE +# ===================================================================== +$script:Root = $PSScriptRoot +$script:CommonScript = Join-Path $script:Root 'WinSenior.Common.ps1' +$script:CleanupScript = Join-Path $script:Root 'Cleanup-Windows-Senior.ps1' +$script:OptimizeScript = Join-Path $script:Root 'Optimize-Windows-Senior.ps1' +$script:RepairScript = Join-Path $script:Root 'Repair-Windows-Senior.ps1' +$script:MenuScript = Join-Path $script:Root 'WinSenior.ps1' +$script:ScheduleScript = Join-Path $script:Root 'WinSenior.Schedule.ps1' + +foreach ($s in @($script:CommonScript, $script:CleanupScript, $script:OptimizeScript, $script:RepairScript, $script:ScheduleScript)) { + if (-not (Test-Path $s)) { + [System.Windows.MessageBox]::Show("Engine not found:`n$s`n`nKeep WinSenior.Gui.ps1 next to the engine scripts.", 'Windows Senior', 'OK', 'Error') | Out-Null + exit 1 + } +} + +. $script:CommonScript + +# Which host are we? Children run on the same one so behaviour matches. +$script:HostExe = if ($PSVersionTable.PSEdition -eq 'Core') { Join-Path $PSHOME 'pwsh.exe' } else { Join-Path $PSHOME 'powershell.exe' } +if (-not (Test-Path $script:HostExe)) { $script:HostExe = 'powershell.exe' } + +if (-not (Test-AdminPrivileges) -and -not $NoElevate) { + try { + Start-Process -FilePath $script:HostExe -Verb RunAs -WindowStyle Hidden -ArgumentList @( + '-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-WindowStyle', 'Hidden', '-File', "`"$PSCommandPath`"") + exit 0 + } + catch { + exit 1 + } +} + +# Hide the console window that hosts us (when launched from a console). +try { + Add-Type -Namespace WinSenior -Name ConsoleWin -MemberDefinition @' +[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); +[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); +'@ -ErrorAction SilentlyContinue + $h = [WinSenior.ConsoleWin]::GetConsoleWindow() + if ($h -ne [IntPtr]::Zero) { [void][WinSenior.ConsoleWin]::ShowWindow($h, 0) } +} catch { } + +Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Xaml + +# Load the engines as libraries: registries, applied-state checks, selection defaults. +. $script:CleanupScript +. $script:OptimizeScript +. $script:RepairScript +. $script:ScheduleScript + +# ===================================================================== +# ROW MODEL (INotifyPropertyChanged so the grids update live) +# ===================================================================== +if (-not ('WinSenior.Row' -as [type])) { + Add-Type -TypeDefinition @' +using System.ComponentModel; +namespace WinSenior { + public class Row : INotifyPropertyChanged { + string _id="", _name="", _group="", _risk="", _size="", _state="", _detail="", _explain=""; + bool _selected, _enabled = true; long _bytes; + public event PropertyChangedEventHandler PropertyChanged; + void On(string n){ var h = PropertyChanged; if (h != null) h(this, new PropertyChangedEventArgs(n)); } + public string Id { get { return _id; } set { _id = value; On("Id"); } } + public string Name { get { return _name; } set { _name = value; On("Name"); } } + public string Group { get { return _group; } set { _group = value; On("Group"); } } + public string Risk { get { return _risk; } set { _risk = value; On("Risk"); } } + public string Size { get { return _size; } set { _size = value; On("Size"); } } + public string State { get { return _state; } set { _state = value; On("State"); } } + public string Detail { get { return _detail; } set { _detail = value; On("Detail"); } } + public string Explain { get { return _explain; } set { _explain = value; On("Explain"); } } + public bool Selected{ get { return _selected; }set { _selected = value; On("Selected"); } } + public bool Enabled { get { return _enabled; } set { _enabled = value; On("Enabled"); } } + public long Bytes { get { return _bytes; } set { _bytes = value; On("Bytes"); } } + } +} +'@ +} + +# ===================================================================== +# SETTINGS (persisted selections & options) +# ===================================================================== +$script:SettingsDir = Join-Path $env:ProgramData 'WinSenior' +try { + if (-not (Test-Path $script:SettingsDir)) { New-Item -ItemType Directory -Path $script:SettingsDir -Force -ErrorAction Stop | Out-Null } + [System.IO.File]::WriteAllText((Join-Path $script:SettingsDir '.write-test'), 'ok'); Remove-Item (Join-Path $script:SettingsDir '.write-test') -Force +} catch { + # Not writable (running without admin via -NoElevate): keep settings per user instead. + $script:SettingsDir = Join-Path $env:LOCALAPPDATA 'WinSenior' +} +$script:SettingsFile = Join-Path $script:SettingsDir 'gui-settings.json' +$script:LogDir = Join-Path $script:SettingsDir 'logs' +foreach ($d in $script:SettingsDir, $script:LogDir) { if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } } + +function Get-GuiSettings { + $def = [ordered]@{ + CleanOn = $null; OptOn = $null + CurrentUserOnly = $false; RestorePoint = $true; DeferLocked = $true + SkipOptimization = $false; MaxAgeDays = 0 + } + if (Test-Path $script:SettingsFile) { + try { + $j = Get-Content $script:SettingsFile -Raw -Encoding UTF8 | ConvertFrom-Json + foreach ($p in $j.PSObject.Properties) { $def[$p.Name] = $p.Value } + } catch { } + } + $def +} + +function Save-GuiSettings { + try { + $o = [ordered]@{ + CleanOn = @($script:CleanRows | Where-Object Selected | ForEach-Object Id) + OptOn = @($script:OptRows | Where-Object Selected | ForEach-Object Id) + CurrentUserOnly = [bool]$W.ChkCurrentUser.IsChecked + RestorePoint = [bool]$W.ChkRestore.IsChecked + DeferLocked = [bool]$W.ChkDefer.IsChecked + SkipOptimization = [bool]$W.ChkSkipOpt.IsChecked + MaxAgeDays = [int]$W.TxtMaxAge.Text + } + ($o | ConvertTo-Json -Depth 4) | Set-Content -Path $script:SettingsFile -Encoding UTF8 + } catch { } +} + +# ===================================================================== +# XAML +# ===================================================================== +$xaml = @' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +