Skip to content

Instantly share code, notes, and snippets.

@markhallen
Created March 21, 2019 12:02
Show Gist options
  • Save markhallen/347642690d27228004d742f4edc99b05 to your computer and use it in GitHub Desktop.
Save markhallen/347642690d27228004d742f4edc99b05 to your computer and use it in GitHub Desktop.
<#
.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