· 6 years ago · Mar 26, 2019, 11:14 PM
1<#
2.SYNOPSIS
3 Uninstall a package when the original source media is unavailable locally.
4
5 .DESCRIPTION
6 A new location will be added to SOURCELIST so that Windows Installer can locate the media. The script uses MSI files
7 located in the current directory or Path and will set an additional SOURCELIST for each before attempting to uninstall.
8
9.PARAMETER Path
10 [Optional] This is the folder that will contain the Windows Installer source.
11 [Default] The current directory
12
13.PARAMETER LogPath
14 [Optional] This is the folder that will contain the execution log files. It is assumed that the folder exists.
15 [Default] C:\Logs
16
17.NOTES
18 Author: Mark Allen
19 Created: 31/05/2017
20 References: n/a
21
22.EXAMPLE
23 .\Uninstall-WindowsInstallerSource.ps1
24 Add the current directory to the package SOURCELIST and execute the uninstall.
25
26.EXAMPLE
27 .\Uninstall-WindowsInstallerSource.ps1 -Path C:\Source
28 Add the directory defined by Path to the package SOURCELIST and execute the uninstall.
29
30.EXAMPLE
31 .\Uninstall-WindowsInstallerSource.ps1 -LogPath C:\Temp
32 Add the current directory to the package SOURCELIST and execute the uninstall with a log file in C:\Temp.
33#>
34
35[CmdletBinding( SupportsShouldProcess = $False, ConfirmImpact = "None", DefaultParameterSetName = "" ) ]
36param(
37 [Parameter(Mandatory=$False)]
38 [ValidateScript({Test-Path $(Split-Path $_) -PathType 'Container'})]
39 [string]$Path = $pwd,
40 [Parameter(Mandatory=$false)]
41 [ValidateScript({Test-Path $(Split-Path $_) -PathType 'Container'})]
42 [string]$LogPath = "C:\Logs\"
43)
44
45$ScriptName = $MyInvocation.MyCommand.Name
46$LOGFILENAME = $ScriptName + ".log"
47$LOGFILE = $LOGPATH + $LOGFILENAME
48
49function Get-MSIProperties {
50 <#
51 .SYNOPSIS
52 Return the properties from an MSI
53 .DESCRIPTION
54 Return and array of the properties from an MSI
55 .PARAMETER Msi
56 The MSI file to checked
57 .EXAMPLE
58 Get-MSIProperties -Msi Setup.msi
59 #>
60 param (
61 [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="MSI Database Filename",ValueFromPipeline=$true)]
62 [ValidateScript({Test-Path $_ -PathType 'Leaf'})]
63 $Msi
64 )
65 $msiOpenDatabaseModeReadOnly = 0
66 $msiOpenDatabaseModeTransact = 1
67
68 # Create an empty hashtable to store properties in
69 $MsiProperties = @{}
70
71 $windowsInstaller = New-Object -ComObject windowsInstaller.Installer
72
73 $database = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $windowsInstaller, @($Msi, $msiOpenDatabaseModeReadOnly))
74
75 $query = "SELECT Property, Value FROM Property"
76 $propView = $database.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $database, ($query))
77 $propView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $propView, $null) | Out-Null
78 $propRecord = $propView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $propView, $null)
79
80 while ($propRecord -ne $null)
81 {
82 $col1 = $propRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $propRecord, 1)
83 $col2 = $propRecord.GetType().InvokeMember("StringData", "GetProperty", $null, $propRecord, 2)
84
85 # Add property and value to hash table
86 $MsiProperties[$col1] = $col2
87
88 #fetch the next record
89 $propRecord = $propView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $propView, $null)
90 }
91
92 $propView.GetType().InvokeMember("Close", "InvokeMethod", $null, $propView, $null) | Out-Null
93 $propView = $null
94 $propRecord = $null
95 $database = $null
96
97 # Return the hash table
98 return $MsiProperties
99}
100
101Function Add-SourceList
102{
103 <#
104 .SYNOPSIS
105 Add a location to sourcelist for an MSI
106 .DESCRIPTION
107 Add a location to sourcelist for an MSI
108 .PARAMETER File
109 The full path to the MSI file to be added to the sourcelsit
110 .EXAMPLE
111 Add-SourceList -File C:\Sources\Setup.msi
112 #>
113 [CmdletBinding()]
114 param(
115 [Parameter(Mandatory=$true)]
116 [ValidateScript({Test-Path $_ -PathType 'Leaf'})]
117 [string]$File
118 )
119 Begin
120 {
121 ## Get the name of this function and write header
122 [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
123 LogWrite "Starting ${CmdletName} with params: $PSBoundParameters"
124 }
125 Process
126 {
127 $MSIPath = Split-Path $File
128 $MsiProperties = Get-MSIProperties -Msi $File
129
130 # check that the MSI is installed
131 $CompactedGuid = Convert-GuidToCompressedGuid($MsiProperties.ProductCode)
132 if(!(Test-Path "HKLM:\SOFTWARE\Classes\Installer\Products\$CompactedGuid")) { LogWrite "$($MsiProperties.ProductName) is not installed."; Return $false }
133
134 $windowsInstaller = New-Object -ComObject WindowsInstaller.Installer
135
136 $codeInvokeMethod = {
137 $type = $this.gettype();
138 $index = $args.count - 1;
139 $methodargs = $args[1..$index]
140 $type.invokeMember($args[0], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this, $methodargs)
141 }
142 $windowsInstaller = $windowsInstaller | Add-Member -MemberType ScriptMethod -Value $codeInvokeMethod -Name InvokeMethod -PassThru
143
144 # the following two values must already exist on the loocal machine
145 try {
146 $windowsInstaller.InvokeMethod('AddSource', "$($MsiProperties.ProductCode)", '', "$MSIPath")
147 }
148 catch
149 {
150 LogWrite "Failed to invoke the Windows Installer COM object. `n$_.Exception.Message `n$_.Exception.ItemName `nSource: ${CmdletName}"
151 Continue
152 }
153 }
154 End
155 {
156 LogWrite "${CmdletName} completed."
157 Return $true
158 }
159}
160
161function Convert-GuidToCompressedGuid
162{
163 <#
164 .SYNOPSIS
165 This converts a GUID to a compressed GUID also known as a product code.
166 .DESCRIPTION
167 This function will typically be used to figure out the product code
168 that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products'
169 registry path to a MSI installer GUID.
170 .EXAMPLE
171 Convert-GuidToCompressedGuid -Guid '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}'
172
173 This example would output the compressed GUID '2820F6C7DCD308A459CABB92E828C144'
174 .PARAMETER Guid
175 The GUID you'd like to convert.
176 #>
177 [CmdletBinding()]
178 [OutputType()]
179 param (
180 [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory)]
181 [string]$Guid
182 )
183 begin {
184 if($Guid.Length -ne 38) {LogWrite "$Guid is not a valid GUID"; Return $null}
185 $Guid = $Guid.Replace('-', '').Replace('{', '').Replace('}', '')
186 }
187 process {
188 try {
189 $Groups = @(
190 $Guid.Substring(0, 8).ToCharArray(),
191 $Guid.Substring(8, 4).ToCharArray(),
192 $Guid.Substring(12, 4).ToCharArray(),
193 $Guid.Substring(16, 16).ToCharArray()
194 )
195 $Groups[0..2] | ForEach-Object {
196 [array]::Reverse($_)
197 }
198 $CompressedGuid = ($Groups[0..2] | ForEach-Object { $_ -join '' }) -join ''
199
200 $chararr = $Groups[3]
201 for ($i = 0; $i -lt $chararr.count; $i++) {
202 if (($i % 2) -eq 0) {
203 $CompressedGuid += ($chararr[$i+1] + $chararr[$i]) -join ''
204 }
205 }
206 $CompressedGuid
207 } catch {
208 LogWrite $_.Exception.Message
209 }
210 }
211}
212
213Function Remove-ApplicationByGUID
214{
215 <#
216 .SYNOPSIS
217 Removes an apllication based on the GUID.
218 .DESCRIPTION
219 This function will typically be used to figure out the product code
220 that matches up with the product code stored in the 'SOFTWARE\Classes\Installer\Products'
221 registry path to a MSI installer GUID.
222 .EXAMPLE
223 Convert-GuidToCompressedGuid -Guid '{7C6F0282-3DCD-4A80-95AC-BB298E821C44}'
224
225 This example would output the compressed GUID '2820F6C7DCD308A459CABB92E828C144'
226 .PARAMETER Guid
227 The GUID you'd like to convert.
228 .NOTES
229 Required functions: LogWrite, ValidGuid, Test-RegistryValue, Get-RegistryValue
230 #>
231 param (
232 [parameter(Mandatory=$true)]
233 [ValidateNotNullOrEmpty()]
234 [ValidateScript({$_.length -eq 38})]
235 [string]
236 $Guid,
237 [parameter(Mandatory=$false)]
238 [string]
239 $Command = $null
240 )
241
242 # break out of the function if a valid GUID is not provided
243 if(!(ValidGuid $Guid)) { LogWrite "$Guid is not a valid GUID."; Return }
244
245 # set the uninstall lof file path
246 $strUninstLogFile = $LOGPATH + $LOGFILEPRE + $Guid + '_Uninstall.log'
247
248 # convert the GUID to the compacted version for registry checking
249 $CompactedGuid = Convert-GuidToCompressedGuid($Guid)
250
251 # create an array of registry paths to search
252 # the compacted GUID should be last as a fallback only
253 $RegistryPaths = @(
254 "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$Guid",
255 "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$Guid",
256 "HKLM:\SOFTWARE\Classes\Installer\Products\$CompactedGuid"
257 )
258 ForEach ($RegistryPath in $RegistryPaths) {
259 LogWrite "Checking the registry for key $RegistryPath"
260 if(!(Test-Path($RegistryPath))) { LogWrite "$RegistryPath not found."; Continue }
261 LogWrite "$RegistryPath found."
262 $strUninstRegKey = $RegistryPath
263 Break
264 }
265
266 if(Test-Path($strUninstRegKey))
267 {
268 LogWrite "$Guid found in the registry."
269 if($Command)
270 {
271 LogWrite "A custom uninstall command has been provided - ""$Command"" "
272 $strUninstCmd = $Command
273 } elseif(Test-RegistryValue $strUninstRegKey UninstallString) {
274 LogWrite "The uninstall string defined in the regsitry will be used."
275 $strUninstCmd = Get-RegistryValue $strUninstRegKey UninstallString
276 } else {
277 LogWrite "No uninstall string found in the registry. The default msiexec.exe command will be used."
278 $strUninstCmd = 'msiexec.exe /x ' + $Guid + ' /l*v ' + $strUninstLogFile + ' /qn'
279 }
280 if($strUninstCmd.ToLower().Contains("msiexec"))
281 {
282 if(!($strUninstCmd.ToLower().Contains("/l"))) { $strUninstCmd = "$strUninstCmd /l*v $strUninstLogFile" }
283 if(!($strUninstCmd.ToLower().Contains("/q"))) { $strUninstCmd = "$strUninstCmd /qn" }
284 }
285 LogWrite "Executing command: $strUninstCmd"
286 CMD /C "`"$strUninstCmd`""
287 LogWrite "Command returned code: $LastExitCode"
288 $ExitReason = "Success"
289 switch ($LastExitCode)
290 {
291 0 { $ExitReason = "Success" }
292 1707 { $ExitReason = "No reboot required" }
293 3010 { $ExitReason = "Soft reboot required" }
294 1641 { $ExitReason = "Hard reboot required" }
295 1618 { $ExitReason = "Fast retry" }
296 default { LogWrite "Failed to execute command ""$strUninstCmd"" 'n Error: $LastExitCode - " + $error[0].exception.message; Exit $LastExitCode }
297 }
298 LogWrite "Successfully executed command ""$strUninstCmd"" : $LastExitCode - $ExitReason "
299 } else {
300 LogWrite "$Guid not found in the uninstall registry."
301 }
302}
303
304Function Get-RegistryValue {
305 param (
306 $key,
307 $value
308 )
309
310 (Get-ItemProperty -Path $key -Name $value).$value
311}
312
313Function Test-RegistryValue {
314 param (
315 $key,
316 $value
317 )
318
319 $data = Get-ItemProperty -Path $key -Name $value -ErrorAction SilentlyContinue
320
321 if ($data) {
322 $true
323 }
324 else {
325 $false
326 }
327}
328
329Function ValidGuid ($guid_string)
330{
331 if($guid_string.length -eq 38)
332 {
333 return $true
334 } else {
335 return $false
336 }
337}
338
339Function LogWrite
340{
341 Param ([string]$logstring)
342
343 Add-content $LOGFILE -value $logstring
344}
345
346# append to or create the log file
347if(Test-Path $LOGFILE)
348{
349 "`r`n***** Script Execution *****" | Add-Content $LOGFILE
350 Get-Date -Format F | Add-Content $LOGFILE
351}
352else
353{
354 if (!(Test-Path -path $LOGPATH)){New-Item $LOGPATH -Type Directory}
355 Get-Date -Format F | Set-Content $LOGFILE
356}
357
358$Installers = @((Get-ChildItem -Path $Path | Where-Object {$_.name -like "*.msi"} | Select-Object -Unique).Name)
359# check that an MSI was found
360if($Installers.Count -eq 0) { LogWrite "No MSI found in $Path"; Exit 3 }
361
362ForEach ($Installer in $Installers)
363{
364 $ProductCode = (Get-MSIProperties -Msi $Path\$Installer).ProductCode
365 if($ProductCode -eq '') { Continue }
366 Add-SourceList -File "$Path\$Installer" # full path
367 Remove-ApplicationByGUID -Guid $ProductCode
368}
369
370Exit 0