UninstallHelper.psm1
<#
.SYNOPSIS Return Uninstall properties from the registry .DESCRIPTION Return Uninstall properties from the registry .PARAMETER Name The name to search for, supports wildcards .EXAMPLE Get-UninstallEntry '*7-Zip*' Returns any entry where the DisplayName matches 7-Zip #> function Get-UninstallEntry { param( [Parameter( Mandatory = $true, Position = 1 )] [string] $Name ) Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $Name } } <# .SYNOPSYS Attempts to run an MSI uninstall with no output .DESCRIPTION Attempts to run an MSI uninstall with no output .PARAMETER QuietUninstallString The QuiteUninstallString from Get-UninstallEntry, will also fallback to UninstallString. Supports pipeline input. .EXAMPLE Get-UninstallEntry '*7-Zip*' | Invoke-MsiQuietUninstall #> function Invoke-MsiQuietUninstall { [CmdletBinding()] param( [Parameter( Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true )] [Alias( 'UninstallString' )] [string] $QuietUninstallString ) process { $FilePath, [string[]]$ArgumentList = __ExpandCommandLine $QuietUninstallString $Command = Get-Command $FilePath -ErrorAction SilentlyContinue if ( -not $Command -or $Command.Name -ne 'msiexec.exe' ) { Write-Warning "Uninstall command is not supported: $FilePath" return } [System.Collections.ArrayList]$SwitchParams = @() [System.Collections.ArrayList]$NamedParams = @() for ( $i = 0; $i -lt $ArgumentList.Count; $i ++ ) { # if argument is a bare /I or /X next param is always a path if ( $ArgumentList[$i] -eq '/I' -or $ArgumentList[$i] -eq '/X' ) { Write-Verbose "Replacing /I with /X" $SwitchParams.Add( '/X' ) > $null $i ++ Write-Verbose "Adding path: $($ArgumentList[$i])" $SwitchParams.Add( $ArgumentList[$i] ) > $null continue } # if argument is a /I or /X followed by any string we just change to an /X if ( $ArgumentList[$i] -like '/[IX]*' ) { Write-Verbose "Replacing /I with /X" $SwitchParams.Add( $ArgumentList[$i].Replace( '/I', '/X' ) ) > $null continue } # if argument is a /L followed by any string we just include it if ( $ArgumentList[$i] -match '^/(log|l[iwearucmopvx+!\*]+)' ) { Write-Verbose "Including logging directive: $($ArgumentList[$i])" $SwitchParams.Add( $ArgumentList[$i] ) > $null # if next param does not start with / or have an "=" we append it right after the switch if ( $ArgumentList[$i+1][0] -ne '/' -and $ArgumentList[$i+1].IndexOf('=') -eq -1 ) { $i ++ Write-Verbose "Adding path: $($ArgumentList[$i])" $SwitchParams.Add( $ArgumentList[$i] ) > $null } continue } # if argument is a quiet switch we skip if ( $ArgumentList[$i] -eq '/quiet' -or $ArgumentList[$i] -eq '/passive' -or $ArgumentList[$i] -like '/q[nbrf]*' ) { Write-Verbose "Skipping quiet directive: $($ArgumentList[$i])" continue } # if argument is a restart switch we skip if ( $ArgumentList[$i] -like '/[npf]*restart' ) { Write-Verbose "Skipping restart directive: $($ArgumentList[$i])" continue } # otherwise any other switches we add it if ( $ArgumentList[$i] -like '/*' ) { Write-Verbose "Adding unknown switch parameter: $($ArgumentList[$i])" $SwitchParams.Add( $ArgumentList[$i] ) > $null # if next param does not start with / or have an "=" we append it right after the switch if ( $ArgumentList[$i+1][0] -ne '/' -and $ArgumentList[$i+1].IndexOf('=') -eq -1 ) { $i ++ Write-Verbose "Adding path: $($ArgumentList[$i])" $SwitchParams.Add( $ArgumentList[$i] ) > $null } continue } else { Write-Verbose "Processing named param: $($ArgumentList[$i])" # in all other cases we add to $NamedParams $NamedParams.Add( $ArgumentList[$i] ) > $null } } Write-Verbose "Adding /qn /norestart" $SwitchParams.Add( '/qn' ) > $null $SwitchParams.Add( '/norestart' ) > $null [string[]]$ArgumentList = $SwitchParams + $NamedParams return __InvokeUninstallCommand -FilePath 'msiexec.exe' -ArgumentList $ArgumentList -Timeout $Timeout } } <# .SYNOPSYS Attempts to run an EXE uninstall with no output .DESCRIPTION Attempts to run an EXE uninstall with no output .PARAMETER QuietUninstallString The QuiteUninstallString from Get-UninstallEntry. Supports pipeline input. .PARAMETER UninstallString The UninstallString from Get-UninstallEntry. Fall back support for pipeline input if QuietUninstallString is not provided. Typically you also need to supply -SilentParams. .PARAMETER SilentParams The parameters to supply to the uninstaller to make it silent. .PARAMETER ReplaceParams Replace the existing parameters with the values in -SilentParams vs adding them. .EXAMPLE Get-UninstallEntry '*7-Zip*' | Invoke-ExeQuietUninstall -SilentParams '/S' #> function Invoke-ExeQuiteUninstall { [CmdletBinding( DefaultParameterSetName = 'QuietUninstallString' )] param( [Parameter( ParameterSetName = 'QuietUninstallString', Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true )] [string] $QuietUninstallString, [Parameter( ParameterSetName = 'UninstallString', Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true )] [string] $UninstallString, [Parameter( ParameterSetName = 'UninstallString' )] [string[]] $SilentParams, [Parameter( ParameterSetName = 'UninstallString' )] [switch] $ReplaceParams, [int] $Timeout = 900 ) process { if ( $PSCmdlet.ParameterSetName -eq 'QuietUninstallString' ) { Write-Verbose "Attempting Quiet Uninstall: $QuietUninstallString" $FilePath, [string[]]$ArgumentList = __ExpandCommandLine $QuietUninstallString # regular uninstall } else { Write-Verbose "Attempting Uninstall: $UninstallString" $FilePath, [string[]]$ArgumentList = __ExpandCommandLine $UninstallString if ( $SilentParams.Count -gt 0 ) { if ( $ReplaceParams ) { $ArgumentList = $SilentParams } else { $ArgumentList = $ArgumentList + $SilentParams } } } return __InvokeUninstallCommand -FilePath $FilePath -ArgumentList $ArgumentList -Timeout $Timeout } } <# .SYNOPSIS Retrieves properties from MSI installer file .DESCRIPTION Retrieves properties from MSI installer file #> function Get-MsiFileProperties { [CmdletBinding()] param( [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $true )] [System.IO.FileInfo[]] $Path ) begin { $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer $Query = 'SELECT * FROM Property' } process { $Path | ForEach-Object { try { $Properties = @{ Name = $_.Name Path = $_.FullName } $MsiDatabase = $WindowsInstaller.GetType().InvokeMember( 'OpenDatabase', 'InvokeMethod', $null, $WindowsInstaller, @( $_.FullName, 0 ) ) $OpenView = $MSIDatabase.GetType().InvokeMember( 'OpenView', 'InvokeMethod', $null, $MSIDatabase, $Query ) $OpenView.GetType().InvokeMember( 'Execute', 'InvokeMethod', $null, $OpenView, $null ) while ( $Record = $OpenView.GetType().InvokeMember( 'Fetch', 'InvokeMethod', $null, $OpenView, $null ) ) { $Key = $Record.GetType().InvokeMember( 'StringData', 'GetProperty', $null, $Record, 1 ) $Value = $Record.GetType().InvokeMember( 'StringData', 'GetProperty', $null, $Record, 2 ) $Properties[$Key] = $Value } $MSIDatabase.GetType().InvokeMember( 'Commit', 'InvokeMethod', $null, $MSIDatabase, $null ) $OpenView.GetType().InvokeMember( 'Close', 'InvokeMethod', $null, $OpenView, $null ) New-Object -TypeName PSObject -Property $Properties } catch { Write-Warning $_.Exception.Message } } } end { [System.Runtime.Interopservices.Marshal]::ReleaseComObject( $WindowsInstaller ) > $null [System.GC]::Collect() } } function __ExpandCommandLine { param( [Parameter( Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true )] [string] $UninstallString ) $UninstallString -replace '(?<!`)([\(\)\[\]{}@&$;])', '`$1' | ForEach-Object { Invoke-Expression "& {`$args} $_" } } function __InvokeUninstallCommand { param( [Parameter( Mandatory = $true, Position = 1 )] [string] $FilePath, [string[]] $ArgumentList, [int] $Timeout = 900 ) $UninstallProcess = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -WindowStyle Hidden -PassThru try { $UninstallProcess | Wait-Process -Timeout $Timeout -ErrorAction SilentlyContinue -ErrorVariable UninstallTimeout if ( $UninstallTimeout ) { $UninstallProcess | Stop-Process -Force Write-Warning "Cancelled uninstall due to timeout after $Timeout seconds" } } finally { if ( -not $UninstallProcess.HasExited ) { $UninstallProcess | Stop-Process -Force Write-Warning "Cancelled uninstall due to user termination" } } return $UninstallProcess.ExitCode } |