PowerShell Script to Clean the Windows Installer Directory

Yesterday, I had a case of the Windows folder filling up the OS drive on a server.

I already have scripts to archive the event log files in c:\windows\system32\winevt, as well as c:\inetpub\logs for web servers.  So I ran those again, but found the OS drive still down to less than 5 GB in space.

Using a program called WinDirStat, I discovered that the Windows Installer directory had gotten pretty big, like over 40GB in size.

I began my quest to Google for a solution.

I saw one article talking about using DISM to clean up the winsxs directory.
 https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/clean-up-the-winsxs-folder

However, that didn't clean up the c:\windows\installer directory at all.  Also, installing the Desktop Experience Feature and using the cleanmgr.exe (Disk Clean up) utility didn't clean up the Installer directory either.

Many blogs pointed towards a tool called Patch Cleaner.
https://www.winhelponline.com/blog/windows-installer-folder-safe-cleanup-free-disk-space/
https://www.homedev.com.au/Free/PatchCleaner

The PatchCleaner tool checks Windows Installer to determine which files in the Windows Installer directory can be deleted because they are orphaned remnants of installation packages.

In my production environment, I'm not supposed to install software on my servers without going through a good deal of testing and security procedures,  so I continued to Google for something lighter weight like a PowerShell script.

I managed to find this link http://www.bryanvine.com/2015/06/powershell-script-cleaning-up.html which had a VBScript and PowerShell script mixed solution.

However, I wanted a purely PowerShell version, which I couldn't find.   As I attempted to write a purely PowerShell version, I discovered that there were some issues between PowerShell and the Windows Installer COM object.  I also delved into using the Win32_Product WMI object.  However, I was only able to figure out how to get the Product info.  I wasn't able to get the patch info.

So I finally broke down and used the Windows Installer PowerShell module found at the following GitHub link:
 https://github.com/heaths/psmsi.

You'll have to download the .msi installer from the GitHub site and install it.  Once installed, new instances of PowerShell will have the Get-MSIProductInfo and Get-MSIPatchInfo commandlets available.

The below PowerShell Script will move the orphaned windows installer files to the directory specified in the $archiveDirectory variable which I recommend you change to a folder not on the OS drive to ensure your OS drive is freed of much needed space after the script runs. You can decide whether to just compress the archiveDirectory or at some point delete the files from the archive directory at your own discretion after the script is run.

Without further introduction, here's the source code for the PowerShell script to clean up a Windows Installer directory:


#This code references PowerShell Script from here: http://www.bryanvine.com/2015/06/powershell-script-cleaning-up.html
# The version on the above web site utilizes vbscript to obtain the patches, and only accounts for .msp files. 
# This version accounts for both .msi and .msp files that are registered with the Windows Installer.
#Sample of how to use WMI to get the product information:
#$products = Get-WmiObject -Class "Win32_Product"
#$productList = $products | Select Name, Version, IdentifyingNumber, PackageCode, LocalPackage;
#Because I wasn't able to get patch info from using the WMI objects, I wound up using the Windows Installer PowerShell Module
#https://github.com/heaths/psmsi
# You can easiliy install it by downloading the .msi package from the site or using PackageManager (see the website).
#Get-MSIProductInfo and Get-MSIPatchInfo are the key commandlets used to retrieve the registered products and patches.
#
# Code written by Will Chung in June 2020 to assist with systems running low on drive space due to the Windows\Installer directory filling up. 

#USE AT YOUR OWN RISK DISCLAMIER:
#This software is provided free of charge.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

#The archive directory is where the orphaned files will be moved to. 
#Change this to a different drive if you have one with more room than the c drive.
#Also, recommend enabling compression on the directory where the files are archive to.
# (Right Click -> Properties -> General Tab, click Advanced button -> Check "Compress contents to save disk space".
#I have opted to archive rather than delete, because there is a slight chance you might need the files, and it will be easy to just copy it back to the Windows Installer
#should that ever happen.

$archiveDirectory="c:\ArchiveWindowsInstaller";
New-Item -ItemType Directory -Force -Path $archiveDirectory
if (Get-Command "Get-MsiProductInfo" -ErrorAction Stop)
{
}
else
{
    Write-Output("You must install the PowerShell MSI Module from https://github.com/heaths/psmsi.");
    return;
}
$products = [System.Collections.ArrayList](Get-MsiProductInfo | Select Name, ProductCode, PackageCode, LocalPackage)
$productList = New-Object -TypeName "System.Collections.ArrayList";
$productList.IsFixedSize

foreach ($product in $products)
{
   $productObj = New-Object -TypeName PSObject;
   $productObj | Add-Member -MemberType NoteProperty -Name Name -Value $product.Name;
   $productObj | Add-Member -MemberType NoteProperty -Name ProductCode -Value $product.ProductCode;
   $productObj | Add-Member -MemberType NoteProperty -Name PackageCode -Value $product.PackageCode;
   $productObj | Add-Member -MemberType NoteProperty -Name LocalPackage -Value $product.LocalPackage;
   $productList.Add($productObj );
   $patches=get-msipatchInfo -ProductCode $product.ProductCode;
   foreach ($patch in $patches)
   {
  
       $productObj = New-Object -TypeName PSObject;
       $productObj | Add-Member -MemberType NoteProperty -Name Name -Value $patch.Name;
       $productObj | Add-Member -MemberType NoteProperty -Name ProductCode -Value $patch.ProductCode;
       $productObj | Add-Member -MemberType NoteProperty -Name PackageCode -Value $patch.PatchCode;
       $productObj | Add-Member -MemberType NoteProperty -Name LocalPackage -Value $patch.LocalPackage;
       $productList.Add( $productObj);
     
   }

}

#First pass to remove exact file names
dir C:\windows\Installer -file | ForEach-Object{
    $fullname = $_.FullName
    $filename = $_.Name;
    $product=$null;
    $product=$productList | Where-Object{$_.LocalPackage -like "*$fullname*"};
    if($product){
        Write-Output("Keeping " +$product.Name + ", $fullname");
    }
    else{
        Write-Output("Archiving: " + $fullname + " to " + ($archiveDirectory+"\"+$filename));
        Move-Item -Path $fullname -Destination ($archiveDirectory+"\"+$filename);

    }
   
}

#second pass to match product and patch codes
dir C:\windows\Installer -Directory | ForEach-Object{

    $bkeep = $true;
    $fullname = $_.FullName;
    $dirname = $_.name
    $product = $null;
    $msiProduct = $null;
    $product= $productList | Where-Object{$_.ProductCode -like "*$dirname*" -or $_.PackageCode -like "*$dirname*" }
    if ($product)
    {
      $bKeep=$true;
     
    }
    else
    {
       $bKeep=$false;
    }

    if ($product -and $product.Count -gt 1)
    {
        $msiProduct = $product | Where-Object {$_.LocalPackage -like "*.msi" }
    }
    if($bKeep -and $msiProduct){

        Write-Output ("Keeping "+$msiProduct.Name+", " + $fullname)
    }
    elseif ($bKeep)
    {
        Write-Output ("Keeping "+$product.Name+", " + $fullname)
    }
    else{
       
        Write-Output("Archiving: "+ $fullname + " to " + ($archiveDirectory + "\" + $dirname) );
        Move-Item -Path $fullname -Destination ($archiveDirectory + "\" + $dirname)
    }

Comments

Popular posts from this blog

Getting Authentication Prompt When Accessing SharePoint Web Services

How To use ASPNET_SetReg to store encrypted data in the registry and then decrypt the data for use in your app