#
.SYNOPSIS
PSAppDeployToolkit - This script contains the PSADT core runtime and functions using by a Deploy-Application.ps1 script.
.DESCRIPTION
The script can be called directly to dot-source the toolkit functions for testing, but it is usually called by the Deploy-Application.ps1 script.
The script can usually be updated to the latest version without impacting your per-application Deploy-Application scripts. Please check release notes before upgrading.
PSAppDeployToolkit is licensed under the GNU LGPLv3 License - (C) 2023 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham and Muhammad Mashwani).
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see .
.PARAMETER CleanupBlockedApps
Clean up the blocked applications.
This parameter is passed to the script when it is called externally, e.g. from a scheduled task or asynchronously.
.PARAMETER ShowBlockedAppDialog
Display a dialog box showing that the application execution is blocked.
This parameter is passed to the script when it is called externally, e.g. from a scheduled task or asynchronously.
.PARAMETER ReferredInstallName
Name of the referring application that invoked the script externally.
This parameter is passed to the script when it is called externally, e.g. from a scheduled task or asynchronously.
.PARAMETER ReferredInstallTitle
Title of the referring application that invoked the script externally.
This parameter is passed to the script when it is called externally, e.g. from a scheduled task or asynchronously.
.PARAMETER ReferredLogname
Logfile name of the referring application that invoked the script externally.
This parameter is passed to the script when it is called externally, e.g. from a scheduled task or asynchronously.
.PARAMETER AsyncToolkitLaunch
This parameter is passed to the script when it is being called externally, e.g. from a scheduled task or asynchronously.
.INPUTS
None
You cannot pipe objects to this script.
.OUTPUTS
None
This script does not generate any output.
.NOTES
The other parameters specified for this script that are not documented in this help section are for use only by functions in this script that call themselves by running this script again asynchronously.
.LINK
https://psappdeploytoolkit.com
#>
[CmdletBinding()]
Param (
## Script Parameters: These parameters are passed to the script when it is called externally from a scheduled task or because of an Image File Execution Options registry setting
[Switch]$ShowInstallationPrompt = $false,
[Switch]$ShowInstallationRestartPrompt = $false,
[Switch]$CleanupBlockedApps = $false,
[Switch]$ShowBlockedAppDialog = $false,
[Switch]$DisableLogging = $false,
[String]$ReferredInstallName = '',
[String]$ReferredInstallTitle = '',
[String]$ReferredLogName = '',
[String]$Title = '',
[String]$Message = '',
[String]$MessageAlignment = '',
[String]$ButtonRightText = '',
[String]$ButtonLeftText = '',
[String]$ButtonMiddleText = '',
[String]$Icon = '',
[String]$Timeout = '',
[Switch]$ExitOnTimeout = $false,
[Boolean]$MinimizeWindows = $false,
[Switch]$PersistPrompt = $false,
[Int32]$CountdownSeconds = 60,
[Int32]$CountdownNoHideSeconds = 30,
[Switch]$NoCountdown = $false,
[Switch]$AsyncToolkitLaunch = $false,
[Boolean]$TopMost = $true
)
##*=============================================
##* VARIABLE DECLARATION
##*=============================================
#region VariableDeclaration
## Variables: Toolkit Name
[String]$appDeployToolkitName = 'PSAppDeployToolkit'
[String]$appDeployMainScriptFriendlyName = 'App Deploy Toolkit Main'
## Variables: Script Info
[Version]$appDeployMainScriptVersion = [Version]'3.9.3'
[Version]$appDeployMainScriptMinimumConfigVersion = [Version]'3.9.3'
[String]$appDeployMainScriptDate = '02/05/2023'
[Hashtable]$appDeployMainScriptParameters = $PSBoundParameters
## Variables: Datetime and Culture
[DateTime]$currentDateTime = Get-Date
[String]$currentTime = Get-Date -Date $currentDateTime -UFormat '%T'
[String]$currentDate = Get-Date -Date $currentDateTime -UFormat '%d-%m-%Y'
[Timespan]$currentTimeZoneBias = [TimeZone]::CurrentTimeZone.GetUtcOffset($currentDateTime)
[Globalization.CultureInfo]$culture = Get-Culture
[String]$currentLanguage = $culture.TwoLetterISOLanguageName.ToUpper()
[Globalization.CultureInfo]$uiculture = Get-UICulture
[String]$currentUILanguage = $uiculture.TwoLetterISOLanguageName.ToUpper()
## Variables: Environment Variables
[PSObject]$envHost = $Host
[PSObject]$envShellFolders = Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction 'SilentlyContinue'
[String]$envAllUsersProfile = $env:ALLUSERSPROFILE
[String]$envAppData = [Environment]::GetFolderPath('ApplicationData')
[String]$envArchitecture = $env:PROCESSOR_ARCHITECTURE
[String]$envCommonDesktop = $envShellFolders | Select-Object -ExpandProperty 'Common Desktop' -ErrorAction 'SilentlyContinue'
[String]$envCommonDocuments = $envShellFolders | Select-Object -ExpandProperty 'Common Documents' -ErrorAction 'SilentlyContinue'
[String]$envCommonStartMenuPrograms = $envShellFolders | Select-Object -ExpandProperty 'Common Programs' -ErrorAction 'SilentlyContinue'
[String]$envCommonStartMenu = $envShellFolders | Select-Object -ExpandProperty 'Common Start Menu' -ErrorAction 'SilentlyContinue'
[String]$envCommonStartUp = $envShellFolders | Select-Object -ExpandProperty 'Common Startup' -ErrorAction 'SilentlyContinue'
[String]$envCommonTemplates = $envShellFolders | Select-Object -ExpandProperty 'Common Templates' -ErrorAction 'SilentlyContinue'
[String]$envComputerName = [Environment]::MachineName.ToUpper()
[String]$envHomeDrive = $env:HOMEDRIVE
[String]$envHomePath = $env:HOMEPATH
[String]$envHomeShare = $env:HOMESHARE
[String]$envLocalAppData = [Environment]::GetFolderPath('LocalApplicationData')
[String[]]$envLogicalDrives = [Environment]::GetLogicalDrives()
[String]$envProgramData = [Environment]::GetFolderPath('CommonApplicationData')
[String]$envPublic = $env:PUBLIC
[String]$envSystemDrive = $env:SYSTEMDRIVE
[String]$envSystemRoot = $env:SYSTEMROOT
[String]$envTemp = [IO.Path]::GetTempPath()
[String]$envUserCookies = [Environment]::GetFolderPath('Cookies')
[String]$envUserDesktop = [Environment]::GetFolderPath('DesktopDirectory')
[String]$envUserFavorites = [Environment]::GetFolderPath('Favorites')
[String]$envUserInternetCache = [Environment]::GetFolderPath('InternetCache')
[String]$envUserInternetHistory = [Environment]::GetFolderPath('History')
[String]$envUserMyDocuments = [Environment]::GetFolderPath('MyDocuments')
[String]$envUserName = [Environment]::UserName
[String]$envUserPictures = [Environment]::GetFolderPath('MyPictures')
[String]$envUserProfile = $env:USERPROFILE
[String]$envUserSendTo = [Environment]::GetFolderPath('SendTo')
[String]$envUserStartMenu = [Environment]::GetFolderPath('StartMenu')
[String]$envUserStartMenuPrograms = [Environment]::GetFolderPath('Programs')
[String]$envUserStartUp = [Environment]::GetFolderPath('StartUp')
[String]$envUserTemplates = [Environment]::GetFolderPath('Templates')
[String]$envSystem32Directory = [Environment]::SystemDirectory
[String]$envWinDir = $env:WINDIR
## Variables: Domain Membership
[Boolean]$IsMachinePartOfDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').PartOfDomain
[String]$envMachineWorkgroup = ''
[String]$envMachineADDomain = ''
[String]$envLogonServer = ''
[String]$MachineDomainController = ''
[String]$envComputerNameFQDN = $envComputerName
If ($IsMachinePartOfDomain) {
[String]$envMachineADDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | Where-Object { $_ } | ForEach-Object { $_.ToLower() }
Try {
$envComputerNameFQDN = ([Net.Dns]::GetHostEntry('localhost')).HostName
}
Catch {
# Function GetHostEntry failed, but we can construct the FQDN in another way
$envComputerNameFQDN = $envComputerNameFQDN + '.' + $envMachineADDomain
}
Try {
[String]$envLogonServer = $env:LOGONSERVER | Where-Object { (($_) -and (-not $_.Contains('\\MicrosoftAccount'))) } | ForEach-Object { $_.TrimStart('\') } | ForEach-Object { ([Net.Dns]::GetHostEntry($_)).HostName }
}
Catch {
}
# If running in system context or if GetHostEntry fails, fall back on the logonserver value stored in the registry
If (-not $envLogonServer) {
[String]$envLogonServer = Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty 'DCName' -ErrorAction 'SilentlyContinue'
}
## Remove backslashes at the beginning
While ($envLogonServer.StartsWith('\')) {
$envLogonServer = $envLogonServer.Substring(1)
}
Try {
[String]$MachineDomainController = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name
}
Catch {
}
}
Else {
[String]$envMachineWorkgroup = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').Domain | Where-Object { $_ } | ForEach-Object { $_.ToUpper() }
}
[String]$envMachineDNSDomain = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName | Where-Object { $_ } | ForEach-Object { $_.ToLower() }
[String]$envUserDNSDomain = $env:USERDNSDOMAIN | Where-Object { $_ } | ForEach-Object { $_.ToLower() }
Try {
[String]$envUserDomain = [Environment]::UserDomainName.ToUpper()
}
Catch {
}
## Variables: Operating System
[PSObject]$envOS = Get-WmiObject -Class 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue'
[String]$envOSName = $envOS.Caption.Trim()
[String]$envOSServicePack = $envOS.CSDVersion
[Version]$envOSVersion = $envOS.Version
[String]$envOSVersionMajor = $envOSVersion.Major
[String]$envOSVersionMinor = $envOSVersion.Minor
[String]$envOSVersionBuild = $envOSVersion.Build
If ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'UBR') {
[String]$envOSVersionRevision = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'UBR' -ErrorAction 'SilentlyContinue').UBR
}
ElseIf ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'BuildLabEx') {
[String]$envOSVersionRevision = , ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'BuildLabEx' -ErrorAction 'SilentlyContinue').BuildLabEx -split '\.') | ForEach-Object { $_[1] }
}
If ($envOSVersionRevision -notmatch '^[\d\.]+$') { $envOSVersionRevision = '' }
If ($envOSVersionRevision) { [string]$envOSVersion = "$($envOSVersion.ToString()).$envOSVersionRevision" } Else { [string]$envOSVersion = "$($envOSVersion.ToString())" }
# Get the operating system type
[int32]$envOSProductType = $envOS.ProductType
[boolean]$IsServerOS = [boolean]($envOSProductType -eq 3)
[boolean]$IsDomainControllerOS = [boolean]($envOSProductType -eq 2)
[boolean]$IsWorkStationOS = [boolean]($envOSProductType -eq 1)
[boolean]$IsMultiSessionOS = [boolean]($envOSName -match '^Microsoft Windows \d+ Enterprise for Virtual Desktops$')
Switch ($envOSProductType) {
3 { [string]$envOSProductTypeName = 'Server' }
2 { [string]$envOSProductTypeName = 'Domain Controller' }
1 { [string]$envOSProductTypeName = 'Workstation' }
Default { [string]$envOSProductTypeName = 'Unknown' }
}
# Get the OS Architecture
[Boolean]$Is64Bit = [Boolean]((Get-WmiObject -Class 'Win32_Processor' -ErrorAction 'SilentlyContinue' | Where-Object { $_.DeviceID -eq 'CPU0' } | Select-Object -ExpandProperty 'AddressWidth') -eq 64)
If ($Is64Bit) {
[String]$envOSArchitecture = '64-bit'
}
Else {
[String]$envOSArchitecture = '32-bit'
}
## Variables: Current Process Architecture
[Boolean]$Is64BitProcess = [Boolean]([IntPtr]::Size -eq 8)
If ($Is64BitProcess) {
[String]$psArchitecture = 'x64'
}
Else {
[String]$psArchitecture = 'x86'
}
## Variables: Get Normalized ProgramFiles and CommonProgramFiles Paths
[String]$envProgramFiles = ''
[String]$envProgramFilesX86 = ''
[String]$envCommonProgramFiles = ''
[String]$envCommonProgramFilesX86 = ''
If ($Is64Bit) {
If ($Is64BitProcess) {
[String]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles')
[String]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles')
}
Else {
[String]$envProgramFiles = [Environment]::GetEnvironmentVariable('ProgramW6432')
[String]$envCommonProgramFiles = [Environment]::GetEnvironmentVariable('CommonProgramW6432')
}
## Powershell 2 doesn't support X86 folders so need to use variables instead
Try {
[String]$envProgramFilesX86 = [Environment]::GetFolderPath('ProgramFilesX86')
[String]$envCommonProgramFilesX86 = [Environment]::GetFolderPath('CommonProgramFilesX86')
}
Catch {
[String]$envProgramFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
[String]$envCommonProgramFilesX86 = [Environment]::GetEnvironmentVariable('CommonProgramFiles(x86)')
}
}
Else {
[String]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles')
[String]$envProgramFilesX86 = $envProgramFiles
[String]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles')
[String]$envCommonProgramFilesX86 = $envCommonProgramFiles
}
## Variables: Office C2R version, bitness and channel
If ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'VersionToReport') {
[String]$envOfficeVersion = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -Name 'VersionToReport' -ErrorAction 'SilentlyContinue').VersionToReport
}
If ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'Platform') {
[String]$envOfficeBitness = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -Name 'Platform' -ErrorAction 'SilentlyContinue').Platform
}
If ((Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -ErrorAction 'SilentlyContinue').PSObject.Properties.Name -contains 'CDNBaseURL') {
[String]$envOfficeCDNBaseURL = (Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' -Name 'CDNBaseURL' -ErrorAction 'SilentlyContinue').CDNBaseURL
Switch -regex ([String]$envofficeCDNBaseURL) {
"492350f6-3a01-4f97-b9c0-c7c6ddf67d60" {$envOfficeChannel = "monthly"}
"7ffbc6bf-bc32-4f92-8982-f9dd17fd3114" {$envOfficeChannel = "semi-annual"}
"64256afe-f5d9-4f86-8936-8840a6a4f5be" {$envOfficeChannel = "monthly targeted"}
"b8f9b850-328d-4355-9145-c59439a0c4cf" {$envOfficeChannel = "semi-annual targeted"}
}
}
## Variables: Hardware
[Int32]$envSystemRAM = Get-WmiObject -Class 'Win32_PhysicalMemory' -ErrorAction 'SilentlyContinue' | Measure-Object -Property 'Capacity' -Sum -ErrorAction 'SilentlyContinue' | ForEach-Object { [Math]::Round(($_.Sum / 1GB), 2) }
## Variables: PowerShell And CLR (.NET) Versions
[Hashtable]$envPSVersionTable = $PSVersionTable
# PowerShell Version
[Version]$envPSVersion = $envPSVersionTable.PSVersion
[String]$envPSVersionMajor = $envPSVersion.Major
[String]$envPSVersionMinor = $envPSVersion.Minor
[String]$envPSVersionBuild = $envPSVersion.Build
[String]$envPSVersionRevision = $envPSVersion.Revision
[String]$envPSVersion = $envPSVersion.ToString()
# CLR (.NET) Version used by PowerShell
[Version]$envCLRVersion = $envPSVersionTable.CLRVersion
[String]$envCLRVersionMajor = $envCLRVersion.Major
[String]$envCLRVersionMinor = $envCLRVersion.Minor
[String]$envCLRVersionBuild = $envCLRVersion.Build
[String]$envCLRVersionRevision = $envCLRVersion.Revision
[String]$envCLRVersion = $envCLRVersion.ToString()
## Variables: Permissions/Accounts
[Security.Principal.WindowsIdentity]$CurrentProcessToken = [Security.Principal.WindowsIdentity]::GetCurrent()
[Security.Principal.SecurityIdentifier]$CurrentProcessSID = $CurrentProcessToken.User
[String]$ProcessNTAccount = $CurrentProcessToken.Name
[String]$ProcessNTAccountSID = $CurrentProcessSID.Value
[Boolean]$IsAdmin = [Boolean]($CurrentProcessToken.Groups -contains [Security.Principal.SecurityIdentifier]'S-1-5-32-544')
[Boolean]$IsLocalSystemAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'LocalSystemSid')
[Boolean]$IsLocalServiceAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'LocalServiceSid')
[Boolean]$IsNetworkServiceAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'NetworkServiceSid')
[Boolean]$IsServiceAccount = [Boolean]($CurrentProcessToken.Groups -contains [Security.Principal.SecurityIdentifier]'S-1-5-6')
[Boolean]$IsProcessUserInteractive = [Environment]::UserInteractive
$GetAccountNameUsingSid = [ScriptBlock] {
Param (
[String]$SecurityIdentifier = $null
)
Try {
Return (New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ([Security.Principal.WellKnownSidType]::"$SecurityIdentifier", $null)).Translate([System.Security.Principal.NTAccount]).Value
}
Catch {
Return ($null)
}
}
[String]$LocalSystemNTAccount = & $GetAccountNameUsingSid 'LocalSystemSid'
[String]$LocalUsersGroup = & $GetAccountNameUsingSid 'BuiltinUsersSid'
# Test if the current Windows is a Home edition
Try {
If (!((Get-WmiObject -Class Win32_OperatingSystem | Select -Expand Caption) -like "*Home*")){
[string]$LocalPowerUsersGroup = & $GetAccountNameUsingSid 'BuiltinPowerUsersSid'
}
}
Catch{}
[String]$LocalAdministratorsGroup = & $GetAccountNameUsingSid 'BuiltinAdministratorsSid'
# Check if script is running in session zero
If ($IsLocalSystemAccount -or $IsLocalServiceAccount -or $IsNetworkServiceAccount -or $IsServiceAccount) {
$SessionZero = $true
}
Else {
$SessionZero = $false
}
## Variables: Script Name and Script Paths
[String]$scriptPath = $MyInvocation.MyCommand.Definition
[String]$scriptName = [IO.Path]::GetFileNameWithoutExtension($scriptPath)
[String]$scriptFileName = Split-Path -Path $scriptPath -Leaf
[String]$scriptRoot = Split-Path -Path $scriptPath -Parent
[String]$invokingScript = (Get-Variable -Name 'MyInvocation').Value.ScriptName
# Get the invoking script directory
If ($invokingScript) {
# If this script was invoked by another script
[String]$scriptParentPath = Split-Path -Path $invokingScript -Parent
}
Else {
# If this script was not invoked by another script, fall back to the directory one level above this script
[String]$scriptParentPath = (Get-Item -LiteralPath $scriptRoot).Parent.FullName
}
## Variables: App Deploy Script Dependency Files
[String]$appDeployConfigFile = Join-Path -Path $scriptRoot -ChildPath 'AppDeployToolkitConfig.xml'
[String]$appDeployCustomTypesSourceCode = Join-Path -Path $scriptRoot -ChildPath 'AppDeployToolkitMain.cs'
If (-not (Test-Path -LiteralPath $appDeployConfigFile -PathType 'Leaf')) {
Throw 'App Deploy XML configuration file not found.'
}
If (-not (Test-Path -LiteralPath $appDeployCustomTypesSourceCode -PathType 'Leaf')) {
Throw 'App Deploy custom types source code file not found.'
}
# App Deploy Optional Extensions File
[String]$appDeployToolkitDotSourceExtensions = 'AppDeployToolkitExtensions.ps1'
## Import variables from XML configuration file
[Xml.XmlDocument]$xmlConfigFile = Get-Content -LiteralPath $AppDeployConfigFile -Encoding 'UTF8'
[Xml.XmlElement]$xmlConfig = $xmlConfigFile.AppDeployToolkit_Config
# Get Config File Details
[Xml.XmlElement]$configConfigDetails = $xmlConfig.Config_File
[String]$configConfigVersion = [Version]$configConfigDetails.Config_Version
[String]$configConfigDate = $configConfigDetails.Config_Date
# Get Banner and Icon details
[Xml.XmlElement]$xmlBannerIconOptions = $xmlConfig.BannerIcon_Options
[String]$configBannerIconFileName = $xmlBannerIconOptions.Icon_Filename
[String]$configBannerLogoImageFileName = $xmlBannerIconOptions.LogoImage_Filename
[String]$configBannerIconBannerName = $xmlBannerIconOptions.Banner_Filename
[Int32]$appDeployLogoBannerMaxHeight = $xmlBannerIconOptions.Banner_MaxHeight
# Get Toast Notification Options
[Xml.XmlElement]$xmlToastOptions = $xmlConfig.Toast_Options
[String]$configToastDisable = $xmlToastOptions.Toast_Disable
[String]$configToastAppName = $xmlToastOptions.Toast_AppName
[String]$appDeployLogoIcon = Join-Path -Path $scriptRoot -ChildPath $configBannerIconFileName
[String]$appDeployLogoImage = Join-Path -Path $scriptRoot -ChildPath $configBannerLogoImageFileName
[String]$appDeployLogoBanner = Join-Path -Path $scriptRoot -ChildPath $configBannerIconBannerName
# Check that dependency files are present
If (-not (Test-Path -LiteralPath $appDeployLogoIcon -PathType 'Leaf')) {
Throw 'App Deploy logo icon file not found.'
}
If (-not (Test-Path -LiteralPath $appDeployLogoBanner -PathType 'Leaf')) {
Throw 'App Deploy logo banner file not found.'
}
# Get Toolkit Options
[Xml.XmlElement]$xmlToolkitOptions = $xmlConfig.Toolkit_Options
[Boolean]$configToolkitRequireAdmin = [Boolean]::Parse($xmlToolkitOptions.Toolkit_RequireAdmin)
[String]$configToolkitTempPath = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_TempPath)
[String]$configToolkitRegPath = $xmlToolkitOptions.Toolkit_RegPath
[String]$configToolkitLogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_LogPath)
[Boolean]$configToolkitCompressLogs = [Boolean]::Parse($xmlToolkitOptions.Toolkit_CompressLogs)
[String]$configToolkitLogStyle = $xmlToolkitOptions.Toolkit_LogStyle
[Double]$configToolkitLogMaxSize = $xmlToolkitOptions.Toolkit_LogMaxSize
[Boolean]$configToolkitLogWriteToHost = [Boolean]::Parse($xmlToolkitOptions.Toolkit_LogWriteToHost)
[Boolean]$configToolkitLogDebugMessage = [Boolean]::Parse($xmlToolkitOptions.Toolkit_LogDebugMessage)
# Get MSI Options
[Xml.XmlElement]$xmlConfigMSIOptions = $xmlConfig.MSI_Options
[String]$configMSILoggingOptions = $xmlConfigMSIOptions.MSI_LoggingOptions
[String]$configMSIInstallParams = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_InstallParams)
[String]$configMSISilentParams = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_SilentParams)
[String]$configMSIUninstallParams = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_UninstallParams)
[String]$configMSILogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_LogPath)
[Int32]$configMSIMutexWaitTime = $xmlConfigMSIOptions.MSI_MutexWaitTime
# Change paths to user accessible ones if RequireAdmin is false
If ($configToolkitRequireAdmin -eq $false) {
If ($xmlToolkitOptions.Toolkit_TempPathNoAdminRights) {
[String]$configToolkitTempPath = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_TempPathNoAdminRights)
}
If ($xmlToolkitOptions.Toolkit_RegPathNoAdminRights) {
[String]$configToolkitRegPath = $xmlToolkitOptions.Toolkit_RegPathNoAdminRights
}
If ($xmlToolkitOptions.Toolkit_LogPathNoAdminRights) {
[String]$configToolkitLogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlToolkitOptions.Toolkit_LogPathNoAdminRights)
}
If ($xmlConfigMSIOptions.MSI_LogPathNoAdminRights) {
[String]$configMSILogDir = $ExecutionContext.InvokeCommand.ExpandString($xmlConfigMSIOptions.MSI_LogPathNoAdminRights)
}
}
# Get UI Options
[Xml.XmlElement]$xmlConfigUIOptions = $xmlConfig.UI_Options
[String]$configInstallationUILanguageOverride = $xmlConfigUIOptions.InstallationUI_LanguageOverride
[Boolean]$configShowBalloonNotifications = [Boolean]::Parse($xmlConfigUIOptions.ShowBalloonNotifications)
[Int32]$configInstallationUITimeout = $xmlConfigUIOptions.InstallationUI_Timeout
[Int32]$configInstallationUIExitCode = $xmlConfigUIOptions.InstallationUI_ExitCode
[Int32]$configInstallationDeferExitCode = $xmlConfigUIOptions.InstallationDefer_ExitCode
[Int32]$configInstallationPersistInterval = $xmlConfigUIOptions.InstallationPrompt_PersistInterval
[Int32]$configInstallationRestartPersistInterval = $xmlConfigUIOptions.InstallationRestartPrompt_PersistInterval
[Int32]$configInstallationPromptToSave = $xmlConfigUIOptions.InstallationPromptToSave_Timeout
[Boolean]$configInstallationWelcomePromptDynamicRunningProcessEvaluation = [Boolean]::Parse($xmlConfigUIOptions.InstallationWelcomePrompt_DynamicRunningProcessEvaluation)
[Int32]$configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval = $xmlConfigUIOptions.InstallationWelcomePrompt_DynamicRunningProcessEvaluationInterval
# Define ScriptBlock for Loading Message UI Language Options (default for English if no localization found)
[ScriptBlock]$xmlLoadLocalizedUIMessages = {
# If a user is logged on, then get primary UI language for logged on user (even if running in session 0)
If ($RunAsActiveUser) {
# Read language defined by Group Policy
If (-not $HKULanguages) {
[String[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\MUI\Settings' -Value 'PreferredUILanguages'
}
If (-not $HKULanguages) {
[String[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Software\Policies\Microsoft\Windows\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID
}
# Read language for Win Vista & higher machines
If (-not $HKULanguages) {
[String[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID
}
If (-not $HKULanguages) {
[String[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop\MuiCached' -Value 'MachinePreferredUILanguages' -SID $RunAsActiveUser.SID
}
If (-not $HKULanguages) {
[String[]]$HKULanguages = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\International' -Value 'LocaleName' -SID $RunAsActiveUser.SID
}
# Read language for Win XP machines
If (-not $HKULanguages) {
[String]$HKULocale = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\International' -Value 'Locale' -SID $RunAsActiveUser.SID
If ($HKULocale) {
[Int32]$HKULocale = [Convert]::ToInt32('0x' + $HKULocale, 16)
[String[]]$HKULanguages = ([Globalization.CultureInfo]($HKULocale)).Name
}
}
If ($HKULanguages) {
[Globalization.CultureInfo]$PrimaryWindowsUILanguage = [Globalization.CultureInfo]($HKULanguages[0])
[String]$HKUPrimaryLanguageShort = $PrimaryWindowsUILanguage.TwoLetterISOLanguageName.ToUpper()
# If the detected language is Chinese, determine if it is simplified or traditional Chinese
If ($HKUPrimaryLanguageShort -eq 'ZH') {
If ($PrimaryWindowsUILanguage.EnglishName -match 'Simplified') {
[String]$HKUPrimaryLanguageShort = 'ZH-Hans'
}
If ($PrimaryWindowsUILanguage.EnglishName -match 'Traditional') {
[String]$HKUPrimaryLanguageShort = 'ZH-Hant'
}
}
# If the detected language is Portuguese, determine if it is Brazilian Portuguese
If ($HKUPrimaryLanguageShort -eq 'PT') {
If ($PrimaryWindowsUILanguage.ThreeLetterWindowsLanguageName -eq 'PTB') {
[String]$HKUPrimaryLanguageShort = 'PT-BR'
}
}
}
}
If ($HKUPrimaryLanguageShort) {
# Use the primary UI language of the logged in user
[String]$xmlUIMessageLanguage = "UI_Messages_$HKUPrimaryLanguageShort"
}
Else {
# Default to UI language of the account executing current process (even if it is the SYSTEM account)
[String]$xmlUIMessageLanguage = "UI_Messages_$currentLanguage"
}
# Default to English if the detected UI language is not available in the XMl config file
If (-not ($xmlConfig.$xmlUIMessageLanguage)) {
[String]$xmlUIMessageLanguage = 'UI_Messages_EN'
}
# Override the detected language if the override option was specified in the XML config file
If ($configInstallationUILanguageOverride) {
[String]$xmlUIMessageLanguage = "UI_Messages_$configInstallationUILanguageOverride"
}
[Xml.XmlElement]$xmlUIMessages = $xmlConfig.$xmlUIMessageLanguage
[String]$configDiskSpaceMessage = $xmlUIMessages.DiskSpace_Message
[String]$configBalloonTextStart = $xmlUIMessages.BalloonText_Start
[String]$configBalloonTextComplete = $xmlUIMessages.BalloonText_Complete
[String]$configBalloonTextRestartRequired = $xmlUIMessages.BalloonText_RestartRequired
[String]$configBalloonTextFastRetry = $xmlUIMessages.BalloonText_FastRetry
[String]$configBalloonTextError = $xmlUIMessages.BalloonText_Error
[String]$configProgressMessageInstall = $xmlUIMessages.Progress_MessageInstall
[String]$configProgressMessageUninstall = $xmlUIMessages.Progress_MessageUninstall
[String]$configProgressMessageRepair = $xmlUIMessages.Progress_MessageRepair
[String]$configClosePromptMessage = $xmlUIMessages.ClosePrompt_Message
[String]$configClosePromptButtonClose = $xmlUIMessages.ClosePrompt_ButtonClose
[String]$configClosePromptButtonDefer = $xmlUIMessages.ClosePrompt_ButtonDefer
[String]$configClosePromptButtonContinue = $xmlUIMessages.ClosePrompt_ButtonContinue
[String]$configClosePromptButtonContinueTooltip = $xmlUIMessages.ClosePrompt_ButtonContinueTooltip
[String]$configClosePromptCountdownMessage = $xmlUIMessages.ClosePrompt_CountdownMessage
[String]$configDeferPromptWelcomeMessage = $xmlUIMessages.DeferPrompt_WelcomeMessage
[String]$configDeferPromptExpiryMessage = $xmlUIMessages.DeferPrompt_ExpiryMessage
[String]$configDeferPromptWarningMessage = $xmlUIMessages.DeferPrompt_WarningMessage
[String]$configDeferPromptRemainingDeferrals = $xmlUIMessages.DeferPrompt_RemainingDeferrals
[String]$configDeferPromptDeadline = $xmlUIMessages.DeferPrompt_Deadline
[String]$configBlockExecutionMessage = $xmlUIMessages.BlockExecution_Message
[String]$configDeploymentTypeInstall = $xmlUIMessages.DeploymentType_Install
[String]$configDeploymentTypeUnInstall = $xmlUIMessages.DeploymentType_UnInstall
[String]$configDeploymentTypeRepair = $xmlUIMessages.DeploymentType_Repair
[String]$configRestartPromptTitle = $xmlUIMessages.RestartPrompt_Title
[String]$configRestartPromptMessage = $xmlUIMessages.RestartPrompt_Message
[String]$configRestartPromptMessageTime = $xmlUIMessages.RestartPrompt_MessageTime
[String]$configRestartPromptMessageRestart = $xmlUIMessages.RestartPrompt_MessageRestart
[String]$configRestartPromptTimeRemaining = $xmlUIMessages.RestartPrompt_TimeRemaining
[String]$configRestartPromptButtonRestartLater = $xmlUIMessages.RestartPrompt_ButtonRestartLater
[String]$configRestartPromptButtonRestartNow = $xmlUIMessages.RestartPrompt_ButtonRestartNow
[String]$configWelcomePromptCountdownMessage = $xmlUIMessages.WelcomePrompt_CountdownMessage
[String]$configWelcomePromptCustomMessage = $xmlUIMessages.WelcomePrompt_CustomMessage
}
## Variables: Script Directories
[String]$dirFiles = Join-Path -Path $scriptParentPath -ChildPath 'Files'
[String]$dirSupportFiles = Join-Path -Path $scriptParentPath -ChildPath 'SupportFiles'
[String]$dirAppDeployTemp = Join-Path -Path $configToolkitTempPath -ChildPath $appDeployToolkitName
If (-not (Test-Path -LiteralPath $dirAppDeployTemp -PathType 'Container' -ErrorAction 'SilentlyContinue')) {
$null = New-Item -Path $dirAppDeployTemp -ItemType 'Directory' -Force -ErrorAction 'SilentlyContinue'
}
## Set the deployment type to "Install" if it has not been specified
If (-not $deploymentType) {
[String]$deploymentType = 'Install'
}
## Variables: Executables
[String]$exeWusa = "$envWinDir\System32\wusa.exe" # Installs Standalone Windows Updates
[String]$exeMsiexec = "$envWinDir\System32\msiexec.exe" # Installs MSI Installers
[String]$exeSchTasks = "$envWinDir\System32\schtasks.exe" # Manages Scheduled Tasks
## Variables: RegEx Patterns
[String]$MSIProductCodeRegExPattern = '^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$'
## Variables: Invalid FileName Characters
[Char[]]$invalidFileNameChars = [IO.Path]::GetinvalidFileNameChars()
## Variables: Registry Keys
# Registry keys for native and WOW64 applications
[String[]]$regKeyApplications = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
If ($is64Bit) {
[String]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Lotus\Notes'
}
Else {
[String]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Lotus\Notes'
}
[String]$regKeyAppExecution = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options'
## COM Objects: Initialize
[__ComObject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'SilentlyContinue'
[__ComObject]$ShellApp = New-Object -ComObject 'Shell.Application' -ErrorAction 'SilentlyContinue'
## Variables: Reset/Remove Variables
[Boolean]$msiRebootDetected = $false
[Boolean]$BlockExecution = $false
[Boolean]$installationStarted = $false
[Boolean]$runningTaskSequence = $false
If (Test-Path -LiteralPath 'variable:welcomeTimer') {
Remove-Variable -Name 'welcomeTimer' -Scope 'Script'
}
# Reset the deferral history
If (Test-Path -LiteralPath 'variable:deferHistory') {
Remove-Variable -Name 'deferHistory'
}
If (Test-Path -LiteralPath 'variable:deferTimes') {
Remove-Variable -Name 'deferTimes'
}
If (Test-Path -LiteralPath 'variable:deferDays') {
Remove-Variable -Name 'deferDays'
}
## Variables: System DPI Scale Factor (Requires PSADT.UiAutomation loaded)
[ScriptBlock]$GetDisplayScaleFactor = {
# If a user is logged on, then get display scale factor for logged on user (even if running in session 0)
[Boolean]$UserDisplayScaleFactor = $false
[System.Drawing.Graphics]$GraphicsObject = $null
[IntPtr]$DeviceContextHandle = [IntPtr]::Zero
[Int32]$dpiScale = 0
[Int32]$dpiPixels = 0
Try {
# Get Graphics Object from the current Window Handle
[System.Drawing.Graphics]$GraphicsObject = [System.Drawing.Graphics]::FromHwnd([IntPtr]::Zero)
# Get Device Context Handle
[IntPtr]$DeviceContextHandle = $GraphicsObject.GetHdc()
# Get Logical and Physical screen height
[Int32]$LogicalScreenHeight = [PSADT.UiAutomation]::GetDeviceCaps($DeviceContextHandle, [Int32][PSADT.UiAutomation+DeviceCap]::VERTRES)
[Int32]$PhysicalScreenHeight = [PSADT.UiAutomation]::GetDeviceCaps($DeviceContextHandle, [Int32][PSADT.UiAutomation+DeviceCap]::DESKTOPVERTRES)
# Calculate dpi scale and pixels
[Int32]$dpiScale = [Math]::Round([Double]$PhysicalScreenHeight / [Double]$LogicalScreenHeight, 2) * 100
[Int32]$dpiPixels = [Math]::Round(($dpiScale / 100) * 96, 0)
}
Catch {
[Int32]$dpiScale = 0
[Int32]$dpiPixels = 0
}
Finally {
# Release the device context handle and dispose of the graphics object
If ($null -ne $GraphicsObject) {
If ($DeviceContextHandle -ne [IntPtr]::Zero) {
$GraphicsObject.ReleaseHdc($DeviceContextHandle)
}
$GraphicsObject.Dispose()
}
}
# Failed to get dpi, try to read them from registry - Might not be accurate
If ($RunAsActiveUser) {
If ($dpiPixels -lt 1) {
[Int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics' -Value 'AppliedDPI' -SID $RunAsActiveUser.SID
}
If ($dpiPixels -lt 1) {
[Int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop' -Value 'LogPixels' -SID $RunAsActiveUser.SID
}
[Boolean]$UserDisplayScaleFactor = $true
}
# Failed to get dpi from first two registry entries, try to read FontDPI - Usually inaccurate
If ($dpiPixels -lt 1) {
# This registry setting only exists if system scale factor has been changed at least once
[Int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI' -Value 'LogPixels'
[Boolean]$UserDisplayScaleFactor = $false
}
# Calculate dpi scale if its empty and we have dpi pixels
If (($dpiScale -lt 1) -and ($dpiPixels -gt 0)) {
[Int32]$dpiScale = [Math]::Round(($dpiPixels * 100) / 96)
}
}
## Variables: Resolve Parameters. For use in a pipeline
[ScriptBlock]$ResolveParameters = {
Param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]$Parameter
)
Switch ($Parameter.Value.GetType().Name) {
'SwitchParameter' {
"-$($Parameter.Key):`$$($Parameter.Value.ToString().ToLower())"
}
'Boolean' {
"-$($Parameter.Key):`$$($Parameter.Value.ToString().ToLower())"
}
'Int16' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'Int32' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'Int64' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'UInt16' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'UInt32' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'UInt64' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'Single' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'Double' {
"-$($Parameter.Key):$($Parameter.Value)"
}
'Decimal' {
"-$($Parameter.Key):$($Parameter.Value)"
}
default {
"-$($Parameter.Key):`'$($Parameter.Value)`'"
}
}
}
#endregion
##*=============================================
##* END VARIABLE DECLARATION
##*=============================================
##*=============================================
##* FUNCTION LISTINGS
##*=============================================
#region FunctionListings
#region Function Write-FunctionHeaderOrFooter
Function Write-FunctionHeaderOrFooter {
<#
.SYNOPSIS
Write the function header or footer to the log upon first entering or exiting a function.
.DESCRIPTION
Write the "Function Start" message, the bound parameters the function was invoked with, or the "Function End" message when entering or exiting a function.
Messages are debug messages so will only be logged if LogDebugMessage option is enabled in XML config file.
.PARAMETER CmdletName
The name of the function this function is invoked from.
.PARAMETER CmdletBoundParameters
The bound parameters of the function this function is invoked from.
.PARAMETER Header
Write the function header.
.PARAMETER Footer
Write the function footer.
.INPUTS
None
You cannot pipe objects to this function.
.OUTPUTS
None
This function does not generate any output.
.EXAMPLE
Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
.EXAMPLE
Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
.NOTES
This is an internal script function and should typically not be called directly.
.LINK
https://psappdeploytoolkit.com
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[ValidateNotNullorEmpty()]
[String]$CmdletName,
[Parameter(Mandatory = $true, ParameterSetName = 'Header')]
[AllowEmptyCollection()]
[Hashtable]$CmdletBoundParameters,
[Parameter(Mandatory = $true, ParameterSetName = 'Header')]
[Switch]$Header,
[Parameter(Mandatory = $true, ParameterSetName = 'Footer')]
[Switch]$Footer
)
If ($Header) {
Write-Log -Message 'Function Start' -Source ${CmdletName} -DebugMessage
## Get the parameters that the calling function was invoked with
[String]$CmdletBoundParameters = $CmdletBoundParameters | Format-Table -Property @{ Label = 'Parameter'; Expression = { "[-$($_.Key)]" } }, @{ Label = 'Value'; Expression = { $_.Value }; Alignment = 'Left' }, @{ Label = 'Type'; Expression = { $_.Value.GetType().Name }; Alignment = 'Left' } -AutoSize -Wrap | Out-String
If ($CmdletBoundParameters) {
Write-Log -Message "Function invoked with bound parameter(s): `r`n$CmdletBoundParameters" -Source ${CmdletName} -DebugMessage
}
Else {
Write-Log -Message 'Function invoked without any bound parameters.' -Source ${CmdletName} -DebugMessage
}
}
ElseIf ($Footer) {
Write-Log -Message 'Function End' -Source ${CmdletName} -DebugMessage
}
}
#endregion
#region Function Execute-MSP
Function Execute-MSP {
<#
.SYNOPSIS
Reads SummaryInfo targeted product codes in MSP file and determines if the MSP file applies to any installed products
If a valid installed product is found, triggers the Execute-MSI function to patch the installation.
Uses default config MSI parameters. You can use -AddParameters to add additional parameters.
.PARAMETER Path
Path to the msp file
.PARAMETER AddParameters
Additional parameters
.INPUTS
None
You cannot pipe objects to this function.
.OUTPUTS
None
This function does not generate any output.
.EXAMPLE
Execute-MSP -Path 'Adobe_Reader_11.0.3_EN.msp'
.EXAMPLE
Execute-MSP -Path 'AcroRdr2017Upd1701130143_MUI.msp' -AddParameters 'ALLUSERS=1'
.NOTES
.LINK
https://psappdeploytoolkit.com
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, HelpMessage = 'Please enter the path to the MSP file')]
[ValidateScript({ ('.msp' -contains [IO.Path]::GetExtension($_)) })]
[Alias('FilePath')]
[String]$Path,
[Parameter(Mandatory = $false)]
[ValidateNotNullorEmpty()]
[String]$AddParameters
)
Begin {
## Get the name of this function and write header
[String]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
}
Process {
## If the MSP is in the Files directory, set the full path to the MSP
If (Test-Path -LiteralPath (Join-Path -Path $dirFiles -ChildPath $path -ErrorAction 'SilentlyContinue') -PathType 'Leaf' -ErrorAction 'SilentlyContinue') {
[String]$mspFile = Join-Path -Path $dirFiles -ChildPath $path
}
ElseIf (Test-Path -LiteralPath $Path -ErrorAction 'SilentlyContinue') {
[String]$mspFile = (Get-Item -LiteralPath $Path).FullName
}
Else {
Write-Log -Message "Failed to find MSP file [$path]." -Severity 3 -Source ${CmdletName}
If (-not $ContinueOnError) {
Throw "Failed to find MSP file [$path]."
}
Continue
}
Write-Log -Message 'Checking MSP file for valid product codes.' -Source ${CmdletName}
[Boolean]$IsMSPNeeded = $false
## Create a Windows Installer object
[__ComObject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop'
## Define properties for how the MSI database is opened
[Int32]$msiOpenDatabaseModePatchFile = 32
[Int32]$msiOpenDatabaseMode = $msiOpenDatabaseModePatchFile
## Open database in read only mode
[__ComObject]$Database = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($mspFile, $msiOpenDatabaseMode)
## Get the SummaryInformation from the windows installer database
[__ComObject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation'
[Hashtable]$SummaryInfoProperty = @{}
$AllTargetedProductCodes = (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7)).Split(';')
ForEach ($FormattedProductCode in $AllTargetedProductCodes) {
[PSObject]$MSIInstalled = Get-InstalledApplication -ProductCode $FormattedProductCode
If ($MSIInstalled) {
[Boolean]$IsMSPNeeded = $true
}
}
Try {
$null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation)
}
Catch {
}
Try {
$null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Database)
}
Catch {
}
Try {
$null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer)
}
Catch {
}
If ($IsMSPNeeded) {
If ($AddParameters) {
Execute-MSI -Action 'Patch' -Path $Path -AddParameters $AddParameters
}
Else {
Execute-MSI -Action 'Patch' -Path $Path
}
}
}
}
#endregion
#region Function Write-Log
Function Write-Log {
<#
.SYNOPSIS
Write messages to a log file in CMTrace.exe compatible format or Legacy text file format.
.DESCRIPTION
Write messages to a log file in CMTrace.exe compatible format or Legacy text file format and optionally display in the console.
.PARAMETER Message
The message to write to the log file or output to the console.
.PARAMETER Severity
Defines message type. When writing to console or CMTrace.exe log format, it allows highlighting of message type.
Options: 1 = Information (default), 2 = Warning (highlighted in yellow), 3 = Error (highlighted in red)
.PARAMETER Source
The source of the message being logged.
.PARAMETER ScriptSection
The heading for the portion of the script that is being executed. Default is: $script:installPhase.
.PARAMETER LogType
Choose whether to write a CMTrace.exe compatible log file or a Legacy text log file.
.PARAMETER LogFileDirectory
Set the directory where the log file will be saved.
.PARAMETER LogFileName
Set the name of the log file.
.PARAMETER MaxLogFileSizeMB
Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
.PARAMETER WriteHost
Write the log message to the console.
.PARAMETER ContinueOnError
Suppress writing log message to console on failure to write message to log file. Default is: $true.
.PARAMETER PassThru
Return the message that was passed to the function
.PARAMETER DebugMessage
Specifies that the message is a debug message. Debug messages only get logged if -LogDebugMessage is set to $true.
.PARAMETER LogDebugMessage
Debug messages only get logged if this parameter is set to $true in the config XML file.
.INPUTS
System.String
The message to write to the log file or output to the console.
.OUTPUTS
None
This function does not generate any output.
.EXAMPLE
Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace'
.EXAMPLE
Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy'
.EXAMPLE
Write-Log -Message "Log only message" -WriteHost $false
.NOTES
.LINK
https://psappdeploytoolkit.com
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[AllowEmptyCollection()]
[Alias('Text')]
[String[]]$Message,
[Parameter(Mandatory = $false, Position = 1)]
[ValidateRange(1, 3)]
[Int16]$Severity = 1,
[Parameter(Mandatory = $false, Position = 2)]
[ValidateNotNull()]
[String]$Source = $([String]$parentFunctionName = [IO.Path]::GetFileNameWithoutExtension((Get-Variable -Name 'MyInvocation' -Scope 1 -ErrorAction 'SilentlyContinue').Value.MyCommand.Name); If ($parentFunctionName) {
$parentFunctionName
}
Else {
'Unknown'
}),
[Parameter(Mandatory = $false, Position = 3)]
[ValidateNotNullorEmpty()]
[String]$ScriptSection = $script:installPhase,
[Parameter(Mandatory = $false, Position = 4)]
[ValidateSet('CMTrace', 'Legacy')]
[String]$LogType = $configToolkitLogStyle,
[Parameter(Mandatory = $false, Position = 5)]
[ValidateNotNullorEmpty()]
[String]$LogFileDirectory = $(If ($configToolkitCompressLogs) {
$logTempFolder
}
Else {
$configToolkitLogDir
}),
[Parameter(Mandatory = $false, Position = 6)]
[ValidateNotNullorEmpty()]
[String]$LogFileName = $logName,
[Parameter(Mandatory = $false, Position = 7)]
[ValidateNotNullorEmpty()]
[Decimal]$MaxLogFileSizeMB = $configToolkitLogMaxSize,
[Parameter(Mandatory = $false, Position = 8)]
[ValidateNotNullorEmpty()]
[Boolean]$WriteHost = $configToolkitLogWriteToHost,
[Parameter(Mandatory = $false, Position = 9)]
[ValidateNotNullorEmpty()]
[Boolean]$ContinueOnError = $true,
[Parameter(Mandatory = $false, Position = 10)]
[Switch]$PassThru = $false,
[Parameter(Mandatory = $false, Position = 11)]
[Switch]$DebugMessage = $false,
[Parameter(Mandatory = $false, Position = 12)]
[Boolean]$LogDebugMessage = $configToolkitLogDebugMessage
)
Begin {
## Get the name of this function
[String]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
## Logging Variables
# Log file date/time
[DateTime]$DateTimeNow = Get-Date
[String]$LogTime = $DateTimeNow.ToString('HH\:mm\:ss.fff')
[String]$LogDate = $DateTimeNow.ToString('MM-dd-yyyy')
If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) {
[Int32]$script:LogTimeZoneBias = [TimeZone]::CurrentTimeZone.GetUtcOffset($DateTimeNow).TotalMinutes
}
[String]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias
# Initialize variables
[Boolean]$ExitLoggingFunction = $false
If (-not (Test-Path -LiteralPath 'variable:DisableLogging')) {
$DisableLogging = $false
}
# Check if the script section is defined
[Boolean]$ScriptSectionDefined = [Boolean](-not [String]::IsNullOrEmpty($ScriptSection))
# Get the file name of the source script
Try {
If ($script:MyInvocation.Value.ScriptName) {
[String]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -Leaf -ErrorAction 'Stop'
}
Else {
[String]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -Leaf -ErrorAction 'Stop'
}
}
Catch {
$ScriptSource = ''
}
## Create script block for generating CMTrace.exe compatible log entry
[ScriptBlock]$CMTraceLogString = {
Param (
[String]$lMessage,
[String]$lSource,
[Int16]$lSeverity
)
"" + "