Created
March 21, 2019 12:02
-
-
Save markhallen/347642690d27228004d742f4edc99b05 to your computer and use it in GitHub Desktop.
This file contains 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 | |
Uninstall a package when the original source media is unavailable locally. | |
.DESCRIPTION | |
A new location will be added to SOURCELIST so that Windows Installer can locate the media. The script uses MSI files | |
located in the current directory or Path and will set an additional SOURCELIST for each before attempting to uninstall. | |
.PARAMETER Path | |
[Optional] This is the folder that will contain the Windows Installer source. | |
[Default] The current directory | |
.PARAMETER LogPath | |
[Optional] This is the folder that will contain the execution log files. It is assumed that the folder exists. | |
[Default] C:\Logs | |
.NOTES | |
Author: Mark Allen | |
Created: 31/05/2017 | |
References: n/a | |
.EXAMPLE | |
.\Uninstall-WindowsInstallerSource.ps1 | |
Add the current directory to the package SOURCELIST and execute the uninstall. | |
.EXAMPLE | |
.\Uninstall-WindowsInstallerSource.ps1 -Path C:\Source | |
Add the directory defined by Path to the package SOURCELIST and execute the uninstall. | |
.EXAMPLE | |
.\Uninstall-WindowsInstallerSource.ps1 -LogPath C:\Temp | |
Add the current directory to the package SOURCELIST and execute the uninstall with a log file in C:\Temp. | |
#> | |
[CmdletBinding( SupportsShouldProcess = $False, ConfirmImpact = "None", DefaultParameterSetName = "" ) ] | |
param( | |
[Parameter(Mandatory=$False)] | |
[ValidateScript({Test-Path $(Split-Path $_) -PathType 'Container'})] | |
[string]$Path = $pwd, | |
[Parameter(Mandatory=$false)] | |
[ValidateScript({Test-Path $(Split-Path $_) -PathType 'Container'})] | |
[string]$LogPath = "C:\Logs\" | |
) | |
$ScriptName = $MyInvocation.MyCommand.Name | |
$LOGFILENAME = $ScriptName + ".log" | |
$LOGFILE = $LOGPATH + $LOGFILENAME | |
function Get-MSIProperties { | |
<# | |
.SYNOPSIS | |
Return the properties from an MSI | |
.DESCRIPTION | |
Return and array of the properties from an MSI | |
.PARAMETER Msi | |
The MSI file to checked | |
.EXAMPLE | |
Get-MSIProperties -Msi Setup.msi | |
#> | |
param ( | |
[Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="MSI Database Filename",ValueFromPipeline=$true)] | |
[ValidateScript({Test-Path $_ -PathType 'Leaf'})] | |
$Msi | |
) | |
$msiOpenDatabaseModeReadOnly = 0 | |
$msiOpenDatabaseModeTransact = 1 | |
# Create an empty hashtable to store properties in | |
$MsiProperties = @{} | |
$windowsInstaller = New-Object -ComObject windowsInstaller.Installer | |
$database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $windowsInstaller, @($Msi, $msiOpenDatabaseModeReadOnly)) | |
$query = "SELECT Property, Value FROM Property" | |
$propView = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $database, ($query)) | |
$propView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $propView, $null) | Out-Null | |
$propRecord = $propView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $propView, $null) | |
while ($propRecord -ne $null) | |
{ | |
$col1 = $propRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $propRecord, 1) | |
$col2 = $propRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $propRecord, 2) | |
# Add property and value to hash table | |
$MsiProperties[$col1] = $col2 | |
#fetch the next record | |
$propRecord = $propView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $propView, $null) | |
} | |
$propView.GetType().InvokeMember("Close", "InvokeMethod", $null, $propView, $null) | Out-Null | |
$propView = $null | |
$propRecord = $null | |
$database = $null | |
# Return the hash table | |
return $MsiProperties | |
} | |
Function Add-SourceList | |
{ | |
<# | |
.SYNOPSIS | |
Add a location to sourcelist for an MSI | |
.DESCRIPTION | |
Add a location to sourcelist for an MSI | |
.PARAMETER File | |
The full path to the MSI file to be added to the sourcelsit | |
.EXAMPLE | |
Add-SourceList -File C:\Sources\Setup.msi | |
#> | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true)] | |
[ValidateScript({Test-Path $_ -PathType 'Leaf'})] | |
[string]$File | |
) | |
Begin | |
{ | |
## Get the name of this function and write header | |
[string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name | |
LogWrite "Starting ${CmdletName} with params: $PSBoundParameters" | |
} | |
Process | |
{ | |
$MSIPath = Split-Path $File | |
$MsiProperties = Get-MSIProperties -Msi $File | |
# check that the MSI is installed | |
$CompactedGuid = Convert-GuidToCompressedGuid($MsiProperties.ProductCode) | |
if(!(Test-Path "HKLM:\SOFTWARE\Classes\Installer\Products\$CompactedGuid")) { LogWrite "$($MsiProperties.ProductName) is not installed."; Return $false } | |
$windowsInstaller = New-Object -ComObject WindowsInstaller.Installer | |
$codeInvokeMethod = { | |
$type = $this.gettype(); | |
$index = $args.count - 1; | |
$methodargs = $args[1..$index] | |
$type.invokeMember($args[0], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this, $methodargs) | |
} | |
$windowsInstaller = $windowsInstaller | Add-Member -MemberType ScriptMethod -Value $codeInvokeMethod -Name InvokeMethod -PassThru | |
# the following two values must already exist on the loocal machine | |
try { | |
$windowsInstaller.InvokeMethod('AddSource', "$($MsiProperties.ProductCode)", '', "$MSIPath") | |
} | |
catch | |
{ | |
LogWrite "Failed to invoke the Windows Installer COM object. `n$_.Exception.Message `n$_.Exception.ItemName `nSource: ${CmdletName}" | |
Continue | |
} | |
} | |
End | |
{ | |
LogWrite "${CmdletName} completed." | |
Return $true | |
} | |
} | |
function Convert-GuidToCompressedGuid | |
{ | |
<# | |
.SYNOPSIS | |
This converts a GUID to a compressed GUID also known as a product code. | |
.DESCRIPTION | |
This function will typically be used to figure out the product code | |
that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products' | |
registry path to a MSI installer GUID. | |
.EXAMPLE | |
Convert-GuidToCompressedGuid -Guid '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}' | |
This example would output the compressed GUID '2820F6C7DCD308A459CABB92E828C144' | |
.PARAMETER Guid | |
The GUID you'd like to convert. | |
#> | |
[CmdletBinding()] | |
[OutputType()] | |
param ( | |
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory)] | |
[string]$Guid | |
) | |
begin { | |
if($Guid.Length -ne 38) {LogWrite "$Guid is not a valid GUID"; Return $null} | |
$Guid = $Guid.Replace('-', '').Replace('{', '').Replace('}', '') | |
} | |
process { | |
try { | |
$Groups = @( | |
$Guid.Substring(0, 8).ToCharArray(), | |
$Guid.Substring(8, 4).ToCharArray(), | |
$Guid.Substring(12, 4).ToCharArray(), | |
$Guid.Substring(16, 16).ToCharArray() | |
) | |
$Groups[0..2] | ForEach-Object { | |
[array]::Reverse($_) | |
} | |
$CompressedGuid = ($Groups[0..2] | ForEach-Object { $_ -join '' }) -join '' | |
$chararr = $Groups[3] | |
for ($i = 0; $i -lt $chararr.count; $i++) { | |
if (($i % 2) -eq 0) { | |
$CompressedGuid += ($chararr[$i+1] + $chararr[$i]) -join '' | |
} | |
} | |
$CompressedGuid | |
} catch { | |
LogWrite $_.Exception.Message | |
} | |
} | |
} | |
Function Remove-ApplicationByGUID | |
{ | |
<# | |
.SYNOPSIS | |
Removes an apllication based on the GUID. | |
.DESCRIPTION | |
This function will typically be used to figure out the product code | |
that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products' | |
registry path to a MSI installer GUID. | |
.EXAMPLE | |
Convert-GuidToCompressedGuid -Guid '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}' | |
This example would output the compressed GUID '2820F6C7DCD308A459CABB92E828C144' | |
.PARAMETER Guid | |
The GUID you'd like to convert. | |
.NOTES | |
Required functions: LogWrite, ValidGuid, Test-RegistryValue, Get-RegistryValue | |
#> | |
param ( | |
[parameter(Mandatory=$true)] | |
[ValidateNotNullOrEmpty()] | |
[ValidateScript({$_.length -eq 38})] | |
[string] | |
$Guid, | |
[parameter(Mandatory=$false)] | |
[string] | |
$Command = $null | |
) | |
# break out of the function if a valid GUID is not provided | |
if(!(ValidGuid $Guid)) { LogWrite "$Guid is not a valid GUID."; Return } | |
# set the uninstall lof file path | |
$strUninstLogFile = $LOGPATH + $LOGFILEPRE + $Guid + '_Uninstall.log' | |
# convert the GUID to the compacted version for registry checking | |
$CompactedGuid = Convert-GuidToCompressedGuid($Guid) | |
# create an array of registry paths to search | |
# the compacted GUID should be last as a fallback only | |
$RegistryPaths = @( | |
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$Guid", | |
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$Guid", | |
"HKLM:\SOFTWARE\Classes\Installer\Products\$CompactedGuid" | |
) | |
ForEach ($RegistryPath in $RegistryPaths) { | |
LogWrite "Checking the registry for key $RegistryPath" | |
if(!(Test-Path($RegistryPath))) { LogWrite "$RegistryPath not found."; Continue } | |
LogWrite "$RegistryPath found." | |
$strUninstRegKey = $RegistryPath | |
Break | |
} | |
if(Test-Path($strUninstRegKey)) | |
{ | |
LogWrite "$Guid found in the registry." | |
if($Command) | |
{ | |
LogWrite "A custom uninstall command has been provided - ""$Command"" " | |
$strUninstCmd = $Command | |
} elseif(Test-RegistryValue $strUninstRegKey UninstallString) { | |
LogWrite "The uninstall string defined in the regsitry will be used." | |
$strUninstCmd = Get-RegistryValue $strUninstRegKey UninstallString | |
} else { | |
LogWrite "No uninstall string found in the registry. The default msiexec.exe command will be used." | |
$strUninstCmd = 'msiexec.exe /x ' + $Guid + ' /l*v ' + $strUninstLogFile + ' /qn' | |
} | |
if($strUninstCmd.ToLower().Contains("msiexec")) | |
{ | |
if(!($strUninstCmd.ToLower().Contains("/l"))) { $strUninstCmd = "$strUninstCmd /l*v $strUninstLogFile" } | |
if(!($strUninstCmd.ToLower().Contains("/q"))) { $strUninstCmd = "$strUninstCmd /qn" } | |
} | |
LogWrite "Executing command: $strUninstCmd" | |
CMD /C "`"$strUninstCmd`"" | |
LogWrite "Command returned code: $LastExitCode" | |
$ExitReason = "Success" | |
switch ($LastExitCode) | |
{ | |
0 { $ExitReason = "Success" } | |
1707 { $ExitReason = "No reboot required" } | |
3010 { $ExitReason = "Soft reboot required" } | |
1641 { $ExitReason = "Hard reboot required" } | |
1618 { $ExitReason = "Fast retry" } | |
default { LogWrite "Failed to execute command ""$strUninstCmd"" 'n Error: $LastExitCode - " + $error[0].exception.message; Exit $LastExitCode } | |
} | |
LogWrite "Successfully executed command ""$strUninstCmd"" : $LastExitCode - $ExitReason " | |
} else { | |
LogWrite "$Guid not found in the uninstall registry." | |
} | |
} | |
Function Get-RegistryValue { | |
param ( | |
$key, | |
$value | |
) | |
(Get-ItemProperty -Path $key -Name $value).$value | |
} | |
Function Test-RegistryValue { | |
param ( | |
$key, | |
$value | |
) | |
$data = Get-ItemProperty -Path $key -Name $value -ErrorAction SilentlyContinue | |
if ($data) { | |
$true | |
} | |
else { | |
$false | |
} | |
} | |
Function ValidGuid ($guid_string) | |
{ | |
if($guid_string.length -eq 38) | |
{ | |
return $true | |
} else { | |
return $false | |
} | |
} | |
Function LogWrite | |
{ | |
Param ([string]$logstring) | |
Add-content $LOGFILE -value $logstring | |
} | |
# append to or create the log file | |
if(Test-Path $LOGFILE) | |
{ | |
"`r`n***** Script Execution *****" | Add-Content $LOGFILE | |
Get-Date -Format F | Add-Content $LOGFILE | |
} | |
else | |
{ | |
if (!(Test-Path -path $LOGPATH)){New-Item $LOGPATH -Type Directory} | |
Get-Date -Format F | Set-Content $LOGFILE | |
} | |
$Installers = @((Get-ChildItem -Path $Path | Where-Object {$_.name -like "*.msi"} | Select-Object -Unique).Name) | |
# check that an MSI was found | |
if($Installers.Count -eq 0) { LogWrite "No MSI found in $Path"; Exit 3 } | |
ForEach ($Installer in $Installers) | |
{ | |
$ProductCode = (Get-MSIProperties -Msi $Path\$Installer).ProductCode | |
if($ProductCode -eq '') { Continue } | |
Add-SourceList -File "$Path\$Installer" # full path | |
Remove-ApplicationByGUID -Guid $ProductCode | |
} | |
Exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment