Monday, June 22, 2015

Powershell script: Cleaning up C:\Windows\Installer directory - the correct way

This very simple script is built off Heath Stewart's VB Script which identifies the files linked for installed products that need to be kept.  My powershell wrapper simply takes his output and automates the deleting of what's not needed.  Usage is simple: drop the script in a temp directory, and run. It will create and run Heath's script which creates another file (output.txt) which is then read back in and parsed.  You'll see what's being removed and what's being kept.

It can be pushed as a scriptblock to remote servers using PS Remoting or PSexec.exe.

Enjoy!

<#
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:

JohnLBevan said...

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]