WindowsInstaller.psm1
Set-StrictMode -Version Latest function Get-MsiexecInstallString { [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$InstallerFilePath, [Parameter()] [AllowNull()] [string]$MstFilePath, [Parameter()] [AllowNull()] [string]$MspFilePath, [Parameter()] [AllowNull()] [string]$ExtraSwitches, [Parameter()] [AllowNull()] [string]$LogFilePath ) begin { $ErrorActionPreference = 'Stop' } process { try { ## We're creating common msiexec switches here. /i specifies I want to run an install, /qn ## says I want that install to be quiet (no prompts) and n means no UI so no progress bars $InstallArgs = @() $InstallArgs += "/i `"$InstallerFilePath`" /qn" if ($MstFilePath) { $InstallArgs += "TRANSFORMS=`"$MstFilePath`"" } if ($MspFilePath) { $InstallArgs += "PATCH=`"$MspFilePath`"" } if ($ExtraSwitches) { $InstallArgs += $ExtraSwitches } ## Once we've added all of the custom syntax elements we'll then add a few more default ## switches. REBOOT=ReallySuppress prevents the computer from rebooting if it exists with an ## exit code of 3010, ALLUSERS=1 means that we'd like to make this software for all users ## on the machine and /Lvx* is the most verbose way to specify a log file path and to log as ## much information as possible. if (-not $PSBoundParameters.ContainsKey('LogFilePath')) { $LogFilePath = "$(Get-SystemTempFolderPath)\$($InstallerFilePath | Split-Path -Leaf).log" } $InstallArgs += "REBOOT=ReallySuppress ALLUSERS=1 /Lvx* `"$LogFilePath`"" $InstallArgs = $InstallArgs -join ' ' $InstallArgs } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } function Uninstall-ViaMsizap { <# .SYNOPSIS This function runs the MSIzap utility to forcefully remove and cleanup MSI-installed software .DESCRIPTION This function runs msizap to remove software. .EXAMPLE Uninstall-ViaMsizap -MsizapFilePath C:\msizap.exe -Guid {XXXX-XXX-XXX} This example would attempt to remove the software registered with the GUID {XXXX-XXX-XXX}. .PARAMETER MsizapFilePath The file path where the msizap utility exists. This can be a local or UNC path. .PARAMETER Guid The GUID of the registered software you'd like removed .PARAMETER Params Non-default params you'd like passed to msizap. By default, "TWG!" is used to remove in all user profiles. This typically doesn't need to be changed. .PARAMETER LogFilePath The file path to where msizap will generate output #> [OutputType()] [CmdletBinding()] param ( [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')] [Parameter(Mandatory = $true)] [string]$Guid, [string]$Params = 'TWG!', [ValidateScript({ Test-Path $_ -PathType 'Leaf' })] [string]$MsizapFilePath = "C:\MyDeployment\msizap.exe", [Parameter()] [string]$LogFilePath = "$(Get-SystemTempFolderPath)\msizap.log" ) process { try { Write-Log -Message "-Starting the process `"$MsiZapFilePath $Params $Guid`"..." $NewProcess = Start-Process $MsiZapFilePath -ArgumentList "$Params $Guid" -Wait -NoNewWindow -PassThru -RedirectStandardOutput $LogFilePath Wait-MyProcess -ProcessId $NewProcess.Id } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } function Uninstall-WindowsInstallerPackage { <# .SYNOPSIS This function runs an uninstall for a Windows Installer package .PARAMETER Name The software title of the Windows installer package you'd like to uninstall. Use either the Name param or the Guid param to find the Windows installer package. .PARAMETER MsiExecSwitches Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default, it already uses "/x GUID /qn". You can specify any additional parameters here. .PARAMETER Guid The GUID of the Windows Installer package #> [OutputType()] [CmdletBinding(DefaultParameterSetName = 'Guid')] param ( [Parameter(ParameterSetName = 'Name')] [string]$Name, [Parameter(ParameterSetName = 'Guid')] [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')] [string]$Guid, [string]$MsiExecSwitches ) process { try { $Params = @{ } if ($Name) { Write-Log -Message "Attempting to uninstall Windows Installer using name '$Name'..." $params.Name = $Name } elseif ($Guid) { Write-Log -Message "Attempting to uninstall Windows Installer using GUID '$Guid'..." $params.Guid = $Guid } if ($PSBoundParameters.ContainsKey('MsiExecSwitches')) { $params.MsiExecSwitches = $MsiExecSwitches } if (Uninstall-WindowsInstallerPackageWithMsiexec @params) { Write-Log -Message 'Successfull uninstall.' } else { throw "Failed to uninstall." } } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } function Uninstall-WindowsInstallerPackageWithMsiexec { <# .SYNOPSIS This function runs an uninstall for a Windows Installer package using msiexec.exe /x .PARAMETER Name The software title of the Windows installer package you'd like to uninstall. Use either the Name param or the Guid param to find the Windows installer package. .PARAMETER Guid The GUID of the Windows Installer package .PARAMETER MsiExecSwitches Specify a string of switches you'd like msiexec.exe to run when it attempts to uninstall the software. By default, it already uses "/x GUID /qn". You can specify any additional parameters here. #> [OutputType()] [CmdletBinding(DefaultParameterSetName = 'Guid')] param ( [Parameter(ParameterSetName = 'Name')] [string]$Name, [Parameter(ParameterSetName = 'Guid')] [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')] [string]$Guid, [string]$MsiExecSwitches ) process { try { if ($Name) { Write-Log -Message "Attempting to uninstall Windows Installer with msiexec.exe using name '$Name'..." $Params = @{ 'Name' = $Name } $software = Get-InstalledSoftware @Params if (-not $software) { throw 'Name specified for uninstall but could not find GUID to remove' } else { ## Sometimes multiple instances are returned. 1 having no GUID and 1 having a GUID. ## Cisco AnyConnect is an example where if the one with the GUID is removed both are removed. $Guid = $software | Where-Object { $_.GUID } if (-not $Guid) { throw 'Required GUID could not be found for software' } else { $Guid = $Guid.GUID Write-Log -Message "Using GUID [$Guid] for the uninstall" } } } $switches = @("/x `"$Guid`"") if ($PSBoundParameters.ContainsKey('MsiExecSwitches')) { $switches += $MsiExecSwitches } $switches += @('REBOOT=ReallySuppress', '/qn') $switchString = $switches -join ' ' Write-Log -Message "Initiating msiexec.exe with arguments [$($switchString)]" $Process = Start-Process 'msiexec.exe' -ArgumentList $switchString -PassThru -Wait -NoNewWindow Wait-WindowsInstaller Test-Process $Process if (-not (Test-InstalledSoftware -Guid $Guid)) { Write-Log -Message 'Successfully uninstalled MSI package with msiexec.exe' } else { throw 'Failed to uninstall MSI package with msiexec.exe' } } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } function Uninstall-WindowsInstallerPackageWithMsiModule { <# .SYNOPSIS This function runs an uninstall for a Windows Installer package using the Windows Installer Powershell module https://psmsi.codeplex.com .PARAMETER Name The software title of the Windows installer package you'd like to uninstall. Use either the Name param or the Guid param to find the Windows installer package. .PARAMETER Guid The GUID of the Windows Installer package #> [OutputType()] [CmdletBinding(DefaultParameterSetName = 'Guid')] param ( [Parameter(ParameterSetName = 'Name')] [string]$Name, [Parameter(ParameterSetName = 'Guid')] [ValidatePattern('\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b')] [string]$Guid ) process { try { if (-not (Get-Module -ListAvailable -Name 'MSI')) { throw 'Required MSI module is not available' } if (((Get-OperatingSystem) -notmatch 'XP') -and ((Get-OperatingSystem) -notmatch 'Server')) { $UninstallParams = @{ 'Log' = $script:LogFilePath 'Chain' = $true 'Force' = $true 'ErrorAction' = 'SilentlyContinue' 'Properties' = 'REBOOT=ReallySuppress' } if ($Name) { $MsiProductParams = @{ 'Name' = $Name } } elseif ($Guid) { $MsiProductParams = @{ 'ProductCode' = $Guid } } Get-MSIProductInfo @MsiProductParams | Uninstall-MsiProduct @UninstallParams if (-not (Test-InstalledSoftware @MsiProductParams)) { Write-Log -Message "Successfully uninstalled MSI package '$Name' with MSI module" } else { throw "Failed to uninstall MSI package '$Name' with MSI module" } } } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } function Wait-WindowsInstaller { <# .SYNOPSIS This function should be called immediately after the Uninstall-WindowsInstallerPackage function. This is a specific process waiting function especially for msiexec.exe. It was built because the Wait-MyProcess function will sometimes not work with msiexec installs/uninstalls. This is because msiexec.exe creates a process tree which does not necessarily mean child processes. Using this function will ensure your script always wait for the msiexec.exe process you kicked off to complete before continuing. #> [OutputType()] [CmdletBinding()] param () process { try { Write-Log -Message 'Looking for any msiexec.exe processes...' $MsiexecProcesses = Get-WmiObject -Class Win32_Process -Filter "Name = 'msiexec.exe'" | Where-Object { $_.CommandLine -ne 'C:\Windows\system32\msiexec.exe /V' } if ($MsiExecProcesses) { Write-Log -Message "Found '$($MsiexecProcesses.Count)' Windows installer processes. Waiting..." ## Wait for each msiexec.exe process to finish before proceeding foreach ($Process in $MsiexecProcesses) { Wait-MyProcess -ProcessId $Process.ProcessId } ## When all msiexec.exe processes finish, recursively call this function again to ensure no ## other installs have begun. Wait-WindowsInstaller } else { Write-Log -Message 'No Windows installer processes found' } } catch { Write-Log -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' $PSCmdlet.ThrowTerminatingError($_) } } } |