Last active
November 26, 2025 15:30
-
-
Save Jason-Clark-FG/50ed92dd46b08ccd6c06124d99c15c00 to your computer and use it in GitHub Desktop.
An attempt to merge the modified msi removal with the forced removal/cleanup from https://gist.github.com/KGHague/2c562ee88492c1c0c0eac1b3ae0fecd8 and https://gist.github.com/broestls/f872872a00acee2fca02017160840624 with the ability to run remotely
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
| <# | |
| .SYNOPSIS | |
| Uninstalls and removes VMware Tools from remote Windows systems. | |
| .DESCRIPTION | |
| This script uses PowerShell remoting to execute VMware Tools cleanup on one or more | |
| remote computers. It first attempts to uninstall VMware Tools using the MSI installer | |
| method, then performs comprehensive cleanup by removing registry entries, filesystem | |
| folders, services, and devices. | |
| WARNING: If running this script on a system that has been migrated to a different | |
| hypervisor (e.g., Proxmox with VirtIO drivers), removing VMware storage drivers may | |
| cause boot failures. In such cases: | |
| - Ensure VirtIO drivers (or equivalent) are installed BEFORE running this script | |
| - If boot issues occur, change the disk controller type to IDE or SATA in the | |
| hypervisor settings, boot the system, then reinstall the appropriate drivers | |
| - Consider taking a snapshot before running this script if on a virtualized system | |
| .PARAMETER ComputerName | |
| One or more computer names or IP addresses to target. Accepts pipeline input. | |
| .PARAMETER Credential | |
| PSCredential object for authenticating to remote computers. If not specified, uses current credentials. | |
| .PARAMETER Force | |
| Bypass the confirmation prompt and proceed with uninstall and cleanup automatically on all targets. | |
| .PARAMETER Reboot | |
| Reboot the remote systems after cleanup completes. If -Force is not specified, prompts for confirmation per computer. | |
| .PARAMETER ThrottleLimit | |
| Maximum number of computers to process in parallel. Default is 5. | |
| .EXAMPLE | |
| .\Cleanup-VMwareToolsRemote.ps1 -ComputerName SERVER01 | |
| Prompts for confirmation before cleaning VMware Tools from SERVER01. | |
| .EXAMPLE | |
| .\Cleanup-VMwareToolsRemote.ps1 -ComputerName SERVER01,SERVER02,SERVER03 -Force | |
| Cleans VMware Tools from multiple servers without prompting. | |
| .EXAMPLE | |
| Get-Content servers.txt | .\Cleanup-VMwareToolsRemote.ps1 -Credential (Get-Credential) -Force -Reboot | |
| Reads server names from a file, cleans VMware Tools, and reboots automatically. | |
| .EXAMPLE | |
| .\Cleanup-VMwareToolsRemote.ps1 -ComputerName SERVER01 -ThrottleLimit 10 | |
| Process up to 10 computers in parallel. | |
| .NOTES | |
| This script combines techniques from two sources: | |
| - MSI uninstaller method: https://gist.github.com/KGHague/2c562ee88492c1c0c0eac1b3ae0fecd8 | |
| - Brute-force cleanup method: https://gist.github.com/broestls/f872872a00acee2fca02017160840624 | |
| Requirements: | |
| - PowerShell Remoting must be enabled on target computers | |
| - User must have administrative privileges on target computers | |
| - WinRM service must be running on target computers | |
| .LINK | |
| https://gist.github.com/KGHague/2c562ee88492c1c0c0eac1b3ae0fecd8 | |
| .LINK | |
| https://gist.github.com/broestls/f872872a00acee2fca02017160840624 | |
| #> | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] | |
| [Alias('CN','Name','PSComputerName')] | |
| [string[]]$ComputerName, | |
| [Parameter(Mandatory=$false)] | |
| [System.Management.Automation.PSCredential] | |
| [System.Management.Automation.Credential()] | |
| $Credential, | |
| [Parameter(Mandatory=$false)] | |
| [switch]$Force, | |
| [Parameter(Mandatory=$false)] | |
| [switch]$Reboot, | |
| [Parameter(Mandatory=$false)] | |
| [int]$ThrottleLimit = 5 | |
| ) | |
| Begin { | |
| #region Script Setup | |
| # Initialize script timer | |
| $scriptStartTime = Get-Date | |
| Write-Host "=== VMware Tools Remote Cleanup Started ===" -ForegroundColor Cyan | |
| Write-Host "Script started at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan | |
| Write-Host "Throttle limit: $ThrottleLimit concurrent computers`n" -ForegroundColor Gray | |
| # Collect all computer names for batch processing | |
| $allComputers = @() | |
| #endregion | |
| #region Cleanup ScriptBlock | |
| # This scriptblock will be executed on each remote computer | |
| $cleanupScriptBlock = { | |
| param( | |
| [bool]$ForceCleanup, | |
| [bool]$RebootAfter | |
| ) | |
| # Helper function for remote execution | |
| function Get-VMwareToolsInstallerID { | |
| foreach ($item in $(Get-ChildItem Registry::HKEY_CLASSES_ROOT\Installer\Products)) { | |
| if ($item.GetValue('ProductName') -eq 'VMware Tools') { | |
| return @{ | |
| reg_id = $item.PSChildName; | |
| msi_id = [Regex]::Match($item.GetValue('ProductIcon'), '(?<={)(.*?)(?=})') | Select-Object -ExpandProperty Value | |
| } | |
| } | |
| } | |
| } | |
| $result = @{ | |
| ComputerName = $env:COMPUTERNAME | |
| Success = $false | |
| Message = "" | |
| VMwareToolsFound = $false | |
| UninstallSuccess = $false | |
| CleanupSuccess = $false | |
| } | |
| try { | |
| #region Gather VMware Tools Information | |
| $vmware_tools_ids = Get-VMwareToolsInstallerID | |
| if ($vmware_tools_ids) { | |
| $result.VMwareToolsFound = $true | |
| Write-Host " VMware Tools installer IDs found" -ForegroundColor Green | |
| } | |
| else { | |
| Write-Host " VMware Tools installer IDs not found in registry" -ForegroundColor Yellow | |
| } | |
| #endregion | |
| #region Step 1: MSI-based Uninstallation | |
| Write-Host " Attempting MSI-based uninstallation..." -ForegroundColor Cyan | |
| if ($vmware_tools_ids) { | |
| $installer = New-Object -ComObject WindowsInstaller.Installer | |
| $localPackage = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\$($vmware_tools_ids.reg_id)\InstallProperties" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LocalPackage | |
| if ($localPackage) { | |
| Write-Host " VMware Tools MSI found: $localPackage" -ForegroundColor Yellow | |
| # Open and modify the MSI database | |
| $database = $installer.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $installer, @("${localPackage}", 2)) | |
| $query = "DELETE FROM CustomAction WHERE Action='VM_LogStart' OR Action='VM_CheckRequirements'" | |
| $view = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $database, @($query)) | |
| $view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null) | |
| $view.GetType().InvokeMember("Close", "InvokeMethod", $null, $view, $null) | |
| [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($view) | |
| $database.GetType().InvokeMember("Commit", "InvokeMethod", $null, $database, $null) | |
| [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($database) | |
| Write-Host " MSI database modified successfully" -ForegroundColor Green | |
| # Uninstall | |
| Write-Host " Uninstalling VMware Tools via MSI..." -ForegroundColor Yellow | |
| Start-Process msiexec.exe -ArgumentList "/x `"${localPackage}`" /qn /norestart" -Wait -NoNewWindow | |
| Write-Host " MSI uninstallation completed" -ForegroundColor Green | |
| $result.UninstallSuccess = $true | |
| } | |
| } | |
| #endregion | |
| #region Step 2: Comprehensive Cleanup | |
| Write-Host " Performing comprehensive cleanup..." -ForegroundColor Cyan | |
| $reg_targets = @( | |
| "Registry::HKEY_CLASSES_ROOT\Installer\Features\", | |
| "Registry::HKEY_CLASSES_ROOT\Installer\Products\", | |
| "HKLM:\SOFTWARE\Classes\Installer\Features\", | |
| "HKLM:\SOFTWARE\Classes\Installer\Products\", | |
| "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\" | |
| ) | |
| $VMware_Tools_Directory = "${env:SystemDrive}\Program Files\VMware" | |
| $VMware_Common_Directory = "${env:SystemDrive}\Program Files\Common Files\VMware" | |
| $VMware_Startmenu_Entry = "${env:SystemDrive}\ProgramData\Microsoft\Windows\Start Menu\Programs\VMware\" | |
| $VMware_ProgramData_Directory = "${env:SystemDrive}\ProgramData\VMware" | |
| $targets = @() | |
| if ($vmware_tools_ids) { | |
| foreach ($item in $reg_targets) { | |
| $targets += $item + $vmware_tools_ids.reg_id | |
| } | |
| $targets += "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{$($vmware_tools_ids.msi_id)}" | |
| } | |
| if ([Environment]::OSVersion.Version.Major -lt 10) { | |
| $targets += "HKCR:\CLSID\{D86ADE52-C4D9-4B98-AA0D-9B0C7F1EBBC8}" | |
| $targets += "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{9709436B-5A41-4946-8BE7-2AA433CAF108}" | |
| $targets += "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{FE2F6A2C-196E-4210-9C04-2B1BC21F07EF}" | |
| } | |
| if (Test-Path "HKLM:\SOFTWARE\VMware, Inc.") { | |
| $targets += "HKLM:\SOFTWARE\VMware, Inc." | |
| } | |
| if (Test-Path "HKLM:\SOFTWARE\WOW6432Node\VMware, Inc.") { | |
| $targets += "HKLM:\SOFTWARE\WOW6432Node\VMware, Inc." | |
| } | |
| # Add the VMware User Process run key value | |
| $runKeyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" | |
| if (Test-Path $runKeyPath) { | |
| $runKey = Get-ItemProperty -Path $runKeyPath -ErrorAction SilentlyContinue | |
| if ($runKey."VMware User Process") { | |
| $targets += "$runKeyPath|VMware User Process" | |
| } | |
| } | |
| if (Test-Path $VMware_Tools_Directory) { $targets += $VMware_Tools_Directory } | |
| if (Test-Path $VMware_Common_Directory) { $targets += $VMware_Common_Directory } | |
| if (Test-Path $VMware_Startmenu_Entry) { $targets += $VMware_Startmenu_Entry } | |
| if (Test-Path $VMware_ProgramData_Directory) { $targets += $VMware_ProgramData_Directory } | |
| $services = @(Get-Service -DisplayName "VMware*" -ErrorAction SilentlyContinue) | |
| $services += @(Get-Service -DisplayName "GISvc" -ErrorAction SilentlyContinue) | |
| $vmwareDevices = Get-PnpDevice | Where-Object { $_.FriendlyName -like "*VMware*" } | |
| if (!$targets -and !$services) { | |
| Write-Host " No cleanup targets found" -ForegroundColor Yellow | |
| $result.Message = "No VMware components found to clean up" | |
| } | |
| else { | |
| $global:ErrorActionPreference = 'SilentlyContinue' | |
| # Unregister vmStatsProvider.dll | |
| $vmStatsProvider = "c:\Program Files\VMware\VMware Tools\vmStatsProvider\win64\vmStatsProvider.dll" | |
| if (Test-Path $vmStatsProvider) { | |
| Write-Host " Unregistering vmStatsProvider.dll..." -ForegroundColor Yellow | |
| Regsvr32 /s /u $vmStatsProvider | |
| } | |
| # Stop and remove VMware services | |
| Write-Host " Stopping and removing VMware services..." -ForegroundColor Yellow | |
| $services | Stop-Service -Confirm:$false -ErrorAction SilentlyContinue | |
| if (Get-Command Remove-Service -ErrorAction SilentlyContinue) { | |
| $services | Remove-Service -Confirm:$false -ErrorAction SilentlyContinue | |
| } | |
| else { | |
| foreach ($s in $services) { | |
| sc.exe DELETE $($s.Name) | Out-Null | |
| } | |
| } | |
| # Stop dependent services | |
| Write-Host " Stopping dependent services temporarily..." -ForegroundColor Yellow | |
| $dep = Get-Service -Name "EventLog" -DependentServices | Select-Object -Property Name | |
| Stop-Service -Name "EventLog" -Force -ErrorAction SilentlyContinue | |
| Stop-Service -Name "wmiApSrv" -Force -ErrorAction SilentlyContinue | |
| $dep += Get-Service -Name "winmgmt" -DependentServices | Select-Object -Property Name | |
| Stop-Service -Name "winmgmt" -Force -ErrorAction SilentlyContinue | |
| Start-Sleep -Seconds 5 | |
| # Remove registry keys, values, and filesystem folders | |
| Write-Host " Removing registry keys, values, and filesystem folders..." -ForegroundColor Yellow | |
| foreach ($item in $targets) { | |
| if ($item -match '^(.+)\|(.+)$') { | |
| $regPath = $Matches[1] | |
| $valueName = $Matches[2] | |
| if (Test-Path $regPath) { | |
| Remove-ItemProperty -Path $regPath -Name $valueName -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| elseif (Test-Path $item) { | |
| Get-Childitem -Path $item -Recurse | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue | |
| Remove-Item -Path $item -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| # Restart dependent services | |
| Write-Host " Restarting dependent services..." -ForegroundColor Yellow | |
| Start-Service -Name "EventLog" -ErrorAction SilentlyContinue | |
| Start-Service -Name "wmiApSrv" -ErrorAction SilentlyContinue | |
| Start-Service -Name "winmgmt" -ErrorAction SilentlyContinue | |
| foreach ($service in $dep) { | |
| Start-Service $service.Name -ErrorAction SilentlyContinue | |
| } | |
| # Remove VMware devices | |
| if ($vmwareDevices.Count -gt 0) { | |
| Write-Host " Removing VMware devices..." -ForegroundColor Yellow | |
| foreach ($device in $vmwareDevices) { | |
| pnputil /remove-device $device.InstanceId 2>&1 | Out-Null | |
| } | |
| } | |
| # Remove VMware driver packages | |
| Write-Host " Removing VMware driver packages..." -ForegroundColor Yellow | |
| $pnpOutput = pnputil /enum-drivers | |
| $vmwareDrivers = @() | |
| for ($i = 0; $i -lt $pnpOutput.Count; $i++) { | |
| if ($pnpOutput[$i] -match "Published Name\s*:\s*(oem\d+\.inf)") { | |
| $oemInf = $Matches[1] | |
| $driverBlock = $pnpOutput[$i..($i+5)] -join " " | |
| if ($driverBlock -match "VMware") { | |
| $vmwareDrivers += $oemInf | |
| } | |
| } | |
| } | |
| if ($vmwareDrivers.Count -gt 0) { | |
| Write-Host " Found $($vmwareDrivers.Count) VMware driver package(s) in driver store" -ForegroundColor Yellow | |
| foreach ($driver in $vmwareDrivers) { | |
| pnputil /delete-driver $driver /uninstall /force 2>&1 | Out-Null | |
| } | |
| } | |
| Start-Sleep -Seconds 2 | |
| Write-Host " Cleanup completed successfully" -ForegroundColor Green | |
| $result.CleanupSuccess = $true | |
| $result.Message = "VMware Tools cleanup completed successfully" | |
| } | |
| #endregion | |
| #region Handle Reboot | |
| if ($RebootAfter) { | |
| Write-Host " Initiating system reboot..." -ForegroundColor Yellow | |
| Restart-Computer -Force | |
| $result.Message += " (Rebooting)" | |
| } | |
| #endregion | |
| $result.Success = $true | |
| } | |
| catch { | |
| $result.Success = $false | |
| $result.Message = "Error: $($_.Exception.Message)" | |
| Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red | |
| } | |
| # Explicitly output the result object to ensure it's returned through the remoting pipeline | |
| Write-Output $result | |
| } | |
| #endregion | |
| } | |
| Process { | |
| # Collect all computer names from pipeline | |
| $allComputers += $ComputerName | |
| } | |
| End { | |
| #region Execute Remote Cleanup | |
| if ($allComputers.Count -eq 0) { | |
| Write-Host "No computers specified for cleanup." -ForegroundColor Red | |
| return | |
| } | |
| Write-Host "Targets: $($allComputers -join ', ')" -ForegroundColor Cyan | |
| # Test connectivity first | |
| Write-Host "`n=== Testing Connectivity ===" -ForegroundColor Cyan | |
| $reachableComputers = @() | |
| $unreachableComputers = @() | |
| foreach ($computer in $allComputers) { | |
| # Check if this is the local computer | |
| $isLocal = ($computer -eq 'localhost') -or | |
| ($computer -eq '127.0.0.1') -or | |
| ($computer -eq $env:COMPUTERNAME) -or | |
| ($computer -eq "$env:COMPUTERNAME.$env:USERDNSDOMAIN") | |
| Write-Host "Testing $computer..." -NoNewline | |
| if ($isLocal) { | |
| Write-Host " OK (Local)" -ForegroundColor Cyan | |
| $reachableComputers += $computer | |
| } | |
| elseif (Test-WSMan -ComputerName $computer -ErrorAction SilentlyContinue) { | |
| Write-Host " OK" -ForegroundColor Green | |
| $reachableComputers += $computer | |
| } | |
| else { | |
| Write-Host " FAILED" -ForegroundColor Red | |
| $unreachableComputers += $computer | |
| } | |
| } | |
| if ($unreachableComputers.Count -gt 0) { | |
| Write-Host "`nUnreachable computers:" -ForegroundColor Red | |
| $unreachableComputers | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } | |
| } | |
| if ($reachableComputers.Count -eq 0) { | |
| Write-Host "`nNo reachable computers found. Exiting." -ForegroundColor Red | |
| return | |
| } | |
| Write-Host "`n=== Starting Cleanup on $($reachableComputers.Count) Computer(s) ===" -ForegroundColor Cyan | |
| # Separate local and remote computers | |
| $localComputers = @() | |
| $remoteComputers = @() | |
| [System.Collections.ArrayList]$results = @() | |
| foreach ($computer in $reachableComputers) { | |
| $isLocal = ($computer -eq 'localhost') -or | |
| ($computer -eq '127.0.0.1') -or | |
| ($computer -eq $env:COMPUTERNAME) -or | |
| ($computer -eq "$env:COMPUTERNAME.$env:USERDNSDOMAIN") | |
| if ($isLocal) { | |
| $localComputers += $computer | |
| } | |
| else { | |
| $remoteComputers += $computer | |
| } | |
| } | |
| # Execute on local computer(s) directly (without remoting) | |
| if ($localComputers.Count -gt 0) { | |
| Write-Host "Executing locally on: $($localComputers -join ', ')" -ForegroundColor Cyan | |
| foreach ($computer in $localComputers) { | |
| $localResult = & $cleanupScriptBlock -ForceCleanup $Force.IsPresent -RebootAfter $Reboot.IsPresent | |
| [void]$results.Add($localResult) | |
| } | |
| } | |
| # Execute on remote computer(s) via Invoke-Command | |
| if ($remoteComputers.Count -gt 0) { | |
| Write-Host "Executing remotely on: $($remoteComputers -join ', ')" -ForegroundColor Cyan | |
| # Build Invoke-Command parameters | |
| $invokeParams = @{ | |
| ComputerName = $remoteComputers | |
| ScriptBlock = $cleanupScriptBlock | |
| ArgumentList = @($Force.IsPresent, $Reboot.IsPresent) | |
| ThrottleLimit = $ThrottleLimit | |
| ErrorAction = 'Continue' | |
| ErrorVariable = 'remoteErrors' | |
| } | |
| if ($Credential) { | |
| $invokeParams.Credential = $Credential | |
| } | |
| # Execute cleanup on remote computers in parallel | |
| $remoteResults = Invoke-Command @invokeParams | |
| if ($remoteResults) { | |
| foreach ($r in $remoteResults) { | |
| [void]$results.Add($r) | |
| } | |
| } | |
| # Display any errors that occurred during remote execution | |
| if ($remoteErrors -and $remoteErrors.Count -gt 0) { | |
| Write-Host "`nRemoting Errors Detected:" -ForegroundColor Red | |
| foreach ($err in $remoteErrors) { | |
| Write-Host " $err" -ForegroundColor Red | |
| } | |
| } | |
| } | |
| #endregion | |
| #region Display Results | |
| Write-Host "`n=== Cleanup Results ===" -ForegroundColor Cyan | |
| if (!$results -or $results.Count -eq 0) { | |
| Write-Host "No results returned from remote execution." -ForegroundColor Red | |
| Write-Host "This may indicate an issue with PowerShell remoting or the scriptblock execution." -ForegroundColor Yellow | |
| Write-Host "`nTroubleshooting tips:" -ForegroundColor Yellow | |
| Write-Host " 1. Ensure PowerShell remoting is enabled: Enable-PSRemoting -Force" -ForegroundColor Gray | |
| Write-Host " 2. Check if WinRM service is running: Get-Service WinRM" -ForegroundColor Gray | |
| Write-Host " 3. Verify you have administrative privileges on the target computer(s)" -ForegroundColor Gray | |
| return | |
| } | |
| foreach ($result in $results) { | |
| Write-Host "`n$($result.ComputerName):" -ForegroundColor Cyan | |
| Write-Host " VMware Tools Found: $($result.VMwareToolsFound)" -ForegroundColor $(if ($result.VMwareToolsFound) { "Yellow" } else { "Gray" }) | |
| Write-Host " Uninstall Success: $($result.UninstallSuccess)" -ForegroundColor $(if ($result.UninstallSuccess) { "Green" } else { "Gray" }) | |
| Write-Host " Cleanup Success: $($result.CleanupSuccess)" -ForegroundColor $(if ($result.CleanupSuccess) { "Green" } else { "Gray" }) | |
| Write-Host " Status: $($result.Message)" -ForegroundColor $(if ($result.Success) { "Green" } else { "Red" }) | |
| } | |
| #endregion | |
| #region Finalize | |
| $scriptDuration = (Get-Date) - $scriptStartTime | |
| # Calculate statistics | |
| $successCount = 0 | |
| $failCount = 0 | |
| foreach ($r in $results) { | |
| if ($r.Success -eq $true) { | |
| $successCount++ | |
| } | |
| else { | |
| $failCount++ | |
| } | |
| } | |
| Write-Host "`n========================================" -ForegroundColor Cyan | |
| Write-Host "Total script execution time: $($scriptDuration.TotalSeconds.ToString('F2')) seconds ($($scriptDuration.ToString('mm\:ss')))" -ForegroundColor Cyan | |
| Write-Host "Script completed at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan | |
| Write-Host "Total computers processed: $($results.Count)" -ForegroundColor Cyan | |
| Write-Host "Successful: $successCount" -ForegroundColor Green | |
| Write-Host "Failed: $failCount" -ForegroundColor Red | |
| Write-Host "========================================" -ForegroundColor Cyan | |
| #endregion | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment