It can be pushed as a scriptblock to remote servers using PS Remoting or PSexec.exe.
Enjoy!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Cleanup-windows-installer.ps1 | |
http://www.bryanvine.com/2015/06/powershell-script-cleaning-up.html | |
Bryan Vine | |
6/22/2015 | |
www.bryanvine.com | |
This script uses Heath Stewart's VB script to identify which files need to be saved and then removes everything else. | |
Can be run locally or pushed to remove servers with PSRemoting or PSexec. | |
It does take a bit to run, but can save you 3-5gb on average. | |
Requires to be ran as administrator. | |
#> | |
#Heath Stewart's VB script from | |
# http://blogs.msdn.com/b/heaths/archive/2007/01/31/how-to-safely-delete-orphaned-patches.aspx | |
$VBSFile = @" | |
'' Identify which patches are registered on the system, and to which | |
'' products those patches are installed. | |
'' | |
'' Copyright (C) Microsoft Corporation. All rights reserved. | |
'' | |
'' THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY | |
'' KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | |
'' IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A | |
'' PARTICULAR PURPOSE. | |
'Option Explicit | |
Dim msi : Set msi = CreateObject("WindowsInstaller.Installer") | |
'Output CSV header | |
WScript.Echo "The data format is ProductCode, PatchCode, PatchLocation" | |
Set objFSO = CreateObject("Scripting.FileSystemObject") | |
Set objFile = objFSO.CreateTextFile("output.txt", True) | |
objFile.WriteLine "ProductCode, PatchCode, PatchLocation" | |
objFile.WriteLine "" | |
' Enumerate all products | |
Dim products : Set products = msi.Products | |
Dim productCode | |
For Each productCode in products | |
' For each product, enumerate its applied patches | |
Dim patches : Set patches = msi.Patches(productCode) | |
Dim patchCode | |
For Each patchCode in patches | |
' Get the local patch location | |
Dim location : location = msi.PatchInfo(patchCode, "LocalPackage") | |
objFile.WriteLine productCode & ", " & patchCode & ", " & location | |
Next | |
Next | |
WScript.Echo "Data written to output.txt, these are the registered objects and SHOULD be kept!" | |
"@ | |
$VBSFile | Set-Content .\WiMsps.vbs | |
cscript .\WiMsps.vbs | |
$savelist = Import-Csv .\output.txt | |
$filelocation = $savelist | select -ExpandProperty PatchLocation | |
#First pass to remove exact file names | |
dir C:\windows\Installer -file | ForEach-Object{ | |
$fullname = $_.FullName | |
if($filelocation | Where-Object{$_ -like "*$fullname*"}){ | |
"Keeping $fullname" | |
} | |
else{ | |
Remove-Item $fullname -Force -Verbose | |
} | |
} | |
#second pass to match product and patch codes | |
dir C:\windows\Installer -Directory | ForEach-Object{ | |
$fullname = $_.name | |
if($savelist | Where-Object{$_.ProductCode -like "*$fullname*" -or $_.PatchCode -like "*$fullname*" }){ | |
"Keeping $fullname" | |
} | |
else{ | |
Remove-Item $_.fullname -Force -Verbose -Recurse | |
} | |
} |
1 comment:
This is great; thank-you for sharing.
FYI: To avoid mixing VBS and PS, I've created a PS version of the VBS script. Hope this is useful to yourself & your readers:
[code]
clear-host
function Get-WindowsInstallerPatchInfo {
[CmdletBinding()]
Param ()
$msi = New-Object -ComObject 'WindowsInstaller.Installer'
#using trick from: https://p0w3rsh3ll.wordpress.com/2012/01/10/working-with-the-windowsinstaller-installer-object/
$msi | Add-Member -Name 'InvokeMethod' -MemberType ScriptMethod -Value {
$type = $this.GetType();
$index = $args.Count -1 ;
$methodargs=$args[1..$index]
$type.invokeMember($args[0],[System.Reflection.BindingFlags]::InvokeMethod,$null,$this,$methodargs)
}
$msi | Add-Member -Name 'GetProperty' -MemberType ScriptMethod -Value {
$type = $this.gettype();
$index = $args.count -1 ;
$methodargs=$args[1..$index]
$type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$methodargs)
}
$products = $msi.GetProperty('Products')
if ($products) {
Write-Verbose "Product Count: $($products.Count)"
foreach ($productCode in $products) {
Write-Verbose "Product: $productCode"
$patches = $msi.GetProperty('Patches',$productCode)
if ($patches) {
foreach ($patchCode in $patches) {
Write-Verbose "Patch: $patchCode"
$location = $msi.GetProperty('PatchInfo', $patchCode, 'LocalPackage')
Write-Verbose "Location: $location"
if ($location) {
(New-Object -TypeName 'PSObject' -Property @{
ProductCode = $productCode
PatchCode = $patchCode
Location = $location
})
}
}
}
}
}
}
[PSObject[]]$patchLocationInfo = Get-WindowsInstallerPatchInfo -Verbose
$patchLocationInfo | format-table -autofit #optional: include this line to see what we gathered with the above script in table form
[string[]]$filelocation = $patchLocationInfo | select-object -expandproperty location
#continue with Bryan's script from line 68.
[/code]
Post a Comment