-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathClean-System.ps1
More file actions
358 lines (311 loc) · 15.4 KB
/
Clean-System.ps1
File metadata and controls
358 lines (311 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
<#
leprechaun@huginn.ovh
.SYNOPSIS
Reclaims disk space on Windows: removes orphaned .msi/.msp packages from the
Windows Installer store, plus WinSxS and system cache cleanup. AGGRESSIVE mode:
no exclusions (Adobe included). Inspired by the free PatchCleaner utility.
.DESCRIPTION
Installer-store cleanup, based on the well-documented technique for safely
deleting unused packages from C:\Windows\Installer:
1) Enumerate C:\Windows\Installer\*.msp and *.msi (non-recursive).
2) Determine which packages are still in use by querying installed products
and applied patches through the Windows Installer automation interface
(Products/ProductInfo(LocalPackage) + Patches/PatchInfo(LocalPackage)),
executed via cscript for reliability. The package paths are read back and
reduced to their file names.
3) An installer-store file is an ORPHAN when its file name is not referenced
by any in-use package (case-insensitive comparison).
If the in-use set cannot be determined, the step aborts and deletes nothing
(no broad registry guesswork).
No exclude filters are applied, so Adobe/Acrobat orphans are removed too.
.NOTES
Run as Administrator (the script auto-elevates). Requires cscript.exe (always present).
Deletion under C:\Windows\Installer is IRREVERSIBLE: set $DryRun = $true to simulate.
#>
# ============================== CONFIGURATION FLAGS =============================
$DryRun = $false # $true = simulation only: nothing is deleted/modified
$CleanWinSxS = $true
$WinSxSResetBase = $true
$CleanWindowsUpdateCache = $true
$CleanTempFiles = $true
$CleanDeliveryOptimization = $true
# ===============================================================================
# -------------------------------- Utilities ----------------------------------
function Test-Admin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal($id)).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-Admin)) {
Write-Host "Administrative privileges required. Relaunching elevated..." -ForegroundColor Yellow
$argList = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
Start-Process -FilePath 'powershell.exe' -ArgumentList $argList -Verb RunAs
return
}
function Format-Size {
param([long]$Bytes)
if ($Bytes -ge 1GB) { '{0:N2} GB' -f ($Bytes / 1GB) }
elseif ($Bytes -ge 1MB) { '{0:N2} MB' -f ($Bytes / 1MB) }
elseif ($Bytes -ge 1KB) { '{0:N2} KB' -f ($Bytes / 1KB) }
else { "$Bytes B" }
}
function Write-Section { param($Text) Write-Host "`n=== $Text ===" -ForegroundColor Cyan }
function Get-SystemFreeSpace {
$drive = (Get-Item $env:WINDIR).PSDrive.Name
(Get-PSDrive -Name $drive).Free
}
# Sum the size of FILES only (folders have no Length). Returns 0 on empty input.
function Get-FolderSize {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return 0 }
$s = (Get-ChildItem -LiteralPath $Path -Recurse -File -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
if ($s) { [long]$s } else { 0 }
}
# --------------------------- Installer-store cleanup -------------------------
# Helper VBScript: enumerates installed products and applied patches via the
# Windows Installer automation interface and writes their cached package paths.
# Output files are created relative to the process WorkingDirectory.
$Script:WMIProductsVbs = @'
On Error Resume Next
Dim msi : Set msi = CreateObject("WindowsInstaller.Installer")
Dim objFileErrors : Set objFileErrors = Nothing
Set fso = CreateObject("Scripting.FileSystemObject")
Set objFileProducts = fso.CreateTextFile("products.txt", True)
Set objFilePatches = fso.CreateTextFile("patches.txt", True)
If fso.FileExists("errors.txt") Then fso.DeleteFile "errors.txt"
Dim products : Set products = msi.Products
Dim product, productLocation, productName, location, patches, patchCode
For Each product In products
productLocation = "" : productName = ""
productLocation = msi.ProductInfo(product, "LocalPackage") : CheckError
productName = msi.ProductInfo(product, "ProductName") : CheckError
If (productLocation <> "") Then
objFileProducts.WriteLine product & "||| [" & productName & "]||| " & productLocation
Set patches = msi.Patches(product)
For Each patchCode In patches
location = ""
location = msi.PatchInfo(patchCode, "LocalPackage")
objFilePatches.WriteLine product & "||| " & patchCode & "||| " & location
Next
End If
Next
objFileProducts.Close()
objFilePatches.Close()
Sub CheckError
If Err = 0 Then Exit Sub
Dim message : message = Err.Source & " " & Hex(Err) & ": " & Err.Description
If objFileErrors Is Nothing Then Set objFileErrors = fso.CreateTextFile("errors.txt", True)
objFileErrors.WriteLine Now & " - " & message
Err.Clear
End Sub
'@
function Get-MsiRequiredFiles {
# Returns a HashSet (OrdinalIgnoreCase) of the FILE NAMES referenced by products
# and patches, obtained by running the helper VBScript through cscript.
# Returns $null on failure (errors.txt / no output).
$set = New-Object 'System.Collections.Generic.HashSet[string]' (
[StringComparer]::OrdinalIgnoreCase)
$cscript = Join-Path $env:WINDIR 'System32\cscript.exe'
if (-not (Test-Path $cscript)) {
Write-Host "cscript.exe not found." -ForegroundColor Red; return $null
}
$work = Join-Path $env:TEMP ("pc_" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work -Force | Out-Null
$vbsPath = Join-Path $work 'WMIProducts.vbs'
$prodTxt = Join-Path $work 'products.txt'
$patchTxt = Join-Path $work 'patches.txt'
$errTxt = Join-Path $work 'errors.txt'
# CreateTextFile/cscript use ANSI: save the VBS as Default (ANSI) encoding.
Set-Content -LiteralPath $vbsPath -Value $Script:WMIProductsVbs -Encoding Default
# Run the VBScript: ProcessStartInfo with WorkingDirectory set to the work
# folder, relative VBS name, hidden window, wait for exit.
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $cscript
$psi.Arguments = '//B //Nologo WMIProducts.vbs'
$psi.WorkingDirectory = $work
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$psi.UseShellExecute = $true # like PatchCleaner (no redirection)
try {
$p = [System.Diagnostics.Process]::Start($psi)
$p.WaitForExit()
$p.Close()
} catch {
Write-Host "Failed to start cscript: $($_.Exception.Message)" -ForegroundColor Red
Remove-Item -LiteralPath $work -Recurse -Force -EA SilentlyContinue
return $null
}
# errors.txt is FATAL only if products.txt does not exist (broken VBScript
# engine). If products.txt exists, per-product errors (e.g. 80004005 on
# ProductInfo) are tolerated: those products are simply skipped.
if (-not (Test-Path $prodTxt)) {
Write-Host "products.txt was not generated: unable to determine products." -ForegroundColor Red
if (Test-Path $errTxt) {
Get-Content -LiteralPath $errTxt -EA SilentlyContinue | ForEach-Object { Write-Host " $_" }
}
Remove-Item -LiteralPath $work -Recurse -Force -EA SilentlyContinue
return $null
}
if (Test-Path $errTxt) {
$nerr = (Get-Content -LiteralPath $errTxt -EA SilentlyContinue | Measure-Object).Count
Write-Host ("Warning: {0} product(s) skipped due to MSI errors." -f $nerr) `
-ForegroundColor DarkYellow
}
# Each line is "...||| ...||| <LocalPackage>": split on "||| " and take the last field.
foreach ($file in @($prodTxt, $patchTxt)) {
if (Test-Path $file) {
Get-Content -LiteralPath $file -EA SilentlyContinue | ForEach-Object {
if ($_ -and $_.Trim()) {
$parts = $_ -split '\|\|\| '
$loc = $parts[$parts.Length - 1].Trim()
if ($loc) { [void]$set.Add([System.IO.Path]::GetFileName($loc)) }
}
}
}
}
Remove-Item -LiteralPath $work -Recurse -Force -EA SilentlyContinue
,$set
}
# Deletes an orphan: clears ReadOnly/Hidden attributes and, on an access-denied
# error, takes ownership + grants Everyone Full Control, then retries the delete.
function Remove-OrphanFile {
param([string]$Path)
try { [System.IO.File]::SetAttributes($Path, [System.IO.FileAttributes]::Normal) } catch {}
try {
Remove-Item -LiteralPath $Path -Force -ErrorAction Stop
return $true
} catch {
& takeown.exe /F "$Path" 2>$null | Out-Null
& icacls.exe "$Path" /grant '*S-1-1-0:F' /C 2>$null | Out-Null # *S-1-1-0 = Everyone
try {
Remove-Item -LiteralPath $Path -Force -ErrorAction Stop
return $true
} catch {
Write-Host "Could not delete $Path : $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
}
function Invoke-PatchCleaner {
Write-Section "Installer store: orphaned files in C:\Windows\Installer"
$installerDir = Join-Path $env:WINDIR 'Installer'
if (-not (Test-Path $installerDir)) { Write-Host "Folder $installerDir not found."; return }
$required = Get-MsiRequiredFiles
if ($null -eq $required -or $required.Count -eq 0) {
Write-Host "Unable to determine required files (VBS): aborting (nothing deleted)." `
-ForegroundColor Red
return
}
Write-Host ("Required files (products + applied patches): {0}" -f $required.Count)
# All *.msi and *.msp in the store root (hidden files included).
$allFiles = Get-ChildItem -LiteralPath $installerDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in '.msi', '.msp' }
# Orphan = file name not present among the required (case-insensitive).
$orphans = $allFiles | Where-Object { -not $required.Contains($_.Name) }
$totalSize = ($orphans | Measure-Object -Property Length -Sum).Sum
if (-not $totalSize) { $totalSize = 0 }
Write-Host ("Total files in store: {0}" -f $allFiles.Count)
Write-Host ("ORPHANED files to delete: {0} ({1})" -f $orphans.Count, (Format-Size $totalSize)) `
-ForegroundColor Yellow
$deleted = 0; $freed = 0
foreach ($f in $orphans) {
if ($DryRun) {
Write-Host "[DRYRUN] Would delete: $($f.FullName) ($(Format-Size $f.Length))"
continue
}
try {
$size = $f.Length
if (Remove-OrphanFile -Path $f.FullName) { $deleted++; $freed += $size }
} catch {
Write-Host "Could not delete $($f.FullName): $($_.Exception.Message)" -ForegroundColor Red
}
}
if (-not $DryRun) {
Write-Host ("Deleted {0} file(s), freed {1}." -f $deleted, (Format-Size $freed)) `
-ForegroundColor Green
}
}
# ------------------------------- WinSxS --------------------------------------
function Invoke-WinSxSCleanup {
Write-Section "WinSxS: component store cleanup (DISM)"
$dismArgs = @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
if ($WinSxSResetBase) { $dismArgs += '/ResetBase' }
Write-Host ("Command: dism.exe {0}" -f ($dismArgs -join ' '))
if ($DryRun) { Write-Host "[DRYRUN] DISM not executed."; return }
& dism.exe @dismArgs
$code = $LASTEXITCODE
# /ResetBase sometimes fails (e.g. 1168 "Element not found"): retry plain
# StartComponentCleanup, which generally succeeds.
if ($code -ne 0 -and $WinSxSResetBase) {
Write-Host ("DISM with /ResetBase failed (exit {0}); retrying without /ResetBase..." -f $code) `
-ForegroundColor Yellow
& dism.exe '/Online' '/Cleanup-Image' '/StartComponentCleanup'
$code = $LASTEXITCODE
}
Write-Host ("DISM finished (exit code {0})." -f $code) -ForegroundColor Green
}
# --------------------- Windows Update cache ----------------------------------
function Invoke-WUCacheCleanup {
Write-Section "Windows Update cache (SoftwareDistribution\Download)"
$path = Join-Path $env:WINDIR 'SoftwareDistribution\Download'
$services = 'wuauserv', 'bits'
if (-not (Test-Path $path)) { Write-Host "$path not present."; return }
$before = Get-FolderSize $path
if ($DryRun) { Write-Host "[DRYRUN] Would empty $path ($(Format-Size $before))"; return }
foreach ($s in $services) { Stop-Service -Name $s -Force -EA SilentlyContinue }
Get-ChildItem -LiteralPath $path -Force -EA SilentlyContinue |
Remove-Item -Recurse -Force -EA SilentlyContinue
foreach ($s in $services) { Start-Service -Name $s -EA SilentlyContinue }
Write-Host ("Freed approximately {0}." -f (Format-Size ($before - (Get-FolderSize $path)))) -ForegroundColor Green
}
# --------------------------- Temporary files ---------------------------------
function Invoke-TempCleanup {
Write-Section "Temporary files (%TEMP% + C:\Windows\Temp)"
$targets = @($env:TEMP, (Join-Path $env:WINDIR 'Temp')) | Select-Object -Unique
foreach ($t in $targets) {
if (-not (Test-Path $t)) { continue }
$before = Get-FolderSize $t
if ($DryRun) { Write-Host "[DRYRUN] Would empty $t ($(Format-Size $before))"; continue }
Get-ChildItem -LiteralPath $t -Force -EA SilentlyContinue |
Remove-Item -Recurse -Force -EA SilentlyContinue
Write-Host ("{0}: freed approximately {1}." -f $t, (Format-Size ($before - (Get-FolderSize $t)))) `
-ForegroundColor Green
}
}
# ----------------------- Delivery Optimization -------------------------------
function Invoke-DOCleanup {
Write-Section "Delivery Optimization cache"
if ($DryRun) { Write-Host "[DRYRUN] Would run Delete-DeliveryOptimizationCache."; return }
if (Get-Command Delete-DeliveryOptimizationCache -EA SilentlyContinue) {
try {
Delete-DeliveryOptimizationCache -Force -ErrorAction Stop
Write-Host "Delivery Optimization cache emptied." -ForegroundColor Green
} catch {
Write-Host "Delivery Optimization error: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
Write-Host "Delete-DeliveryOptimizationCache cmdlet not available." -ForegroundColor Yellow
}
}
# --------------------------------- Main --------------------------------------
$mode = if ($DryRun) { 'SIMULATION (DryRun)' } else { 'LIVE RUN' }
Write-Host "Starting cleanup - $mode" -ForegroundColor Magenta
$freeBefore = Get-SystemFreeSpace
Write-Host ("Initial free space: {0}" -f (Format-Size $freeBefore)) -ForegroundColor Magenta
Invoke-PatchCleaner
if ($CleanWinSxS) { Invoke-WinSxSCleanup }
if ($CleanWindowsUpdateCache) { Invoke-WUCacheCleanup }
if ($CleanTempFiles) { Invoke-TempCleanup }
if ($CleanDeliveryOptimization) { Invoke-DOCleanup }
$freeAfter = Get-SystemFreeSpace
$recovered = $freeAfter - $freeBefore
Write-Section "Summary"
Write-Host ("Initial free space: {0}" -f (Format-Size $freeBefore))
Write-Host ("Final free space: {0}" -f (Format-Size $freeAfter))
if ($recovered -ge 0) {
Write-Host ("Space recovered: {0}" -f (Format-Size $recovered)) -ForegroundColor Green
} else {
Write-Host ("Net change: -{0} (usage increased during execution)" `
-f (Format-Size ([math]::Abs($recovered)))) -ForegroundColor Yellow
}
Write-Section "Done"