From da90b5f1b9ddd8f16b8485d91b3988d335df516d Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:28:16 -0700 Subject: [PATCH 01/15] Move to the "SupportFiles" subdirectory for consistent structure --- {Files => SupportFiles}/M365Apps_Install.xml | 74 +++++++++---------- .../M365Apps_Uninstall.xml | 18 ++--- {Files => SupportFiles}/Project_Install.xml | 54 +++++++------- {Files => SupportFiles}/Project_Uninstall.xml | 24 +++--- {Files => SupportFiles}/Visio_Install.xml | 62 ++++++++-------- {Files => SupportFiles}/Visio_Uninstall.xml | 26 +++---- 6 files changed, 129 insertions(+), 129 deletions(-) rename {Files => SupportFiles}/M365Apps_Install.xml (97%) rename {Files => SupportFiles}/M365Apps_Uninstall.xml (96%) rename {Files => SupportFiles}/Project_Install.xml (97%) rename {Files => SupportFiles}/Project_Uninstall.xml (95%) rename {Files => SupportFiles}/Visio_Install.xml (97%) rename {Files => SupportFiles}/Visio_Uninstall.xml (95%) diff --git a/Files/M365Apps_Install.xml b/SupportFiles/M365Apps_Install.xml similarity index 97% rename from Files/M365Apps_Install.xml rename to SupportFiles/M365Apps_Install.xml index 91f2530..a32de77 100644 --- a/Files/M365Apps_Install.xml +++ b/SupportFiles/M365Apps_Install.xml @@ -1,38 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Files/M365Apps_Uninstall.xml b/SupportFiles/M365Apps_Uninstall.xml similarity index 96% rename from Files/M365Apps_Uninstall.xml rename to SupportFiles/M365Apps_Uninstall.xml index 27f614d..6fb067e 100644 --- a/Files/M365Apps_Uninstall.xml +++ b/SupportFiles/M365Apps_Uninstall.xml @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/Files/Project_Install.xml b/SupportFiles/Project_Install.xml similarity index 97% rename from Files/Project_Install.xml rename to SupportFiles/Project_Install.xml index d4d2d44..2bf01f1 100644 --- a/Files/Project_Install.xml +++ b/SupportFiles/Project_Install.xml @@ -1,28 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Files/Project_Uninstall.xml b/SupportFiles/Project_Uninstall.xml similarity index 95% rename from Files/Project_Uninstall.xml rename to SupportFiles/Project_Uninstall.xml index 48badf7..e337de5 100644 --- a/Files/Project_Uninstall.xml +++ b/SupportFiles/Project_Uninstall.xml @@ -1,12 +1,12 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/Files/Visio_Install.xml b/SupportFiles/Visio_Install.xml similarity index 97% rename from Files/Visio_Install.xml rename to SupportFiles/Visio_Install.xml index 950d4eb..050c2ae 100644 --- a/Files/Visio_Install.xml +++ b/SupportFiles/Visio_Install.xml @@ -1,32 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Files/Visio_Uninstall.xml b/SupportFiles/Visio_Uninstall.xml similarity index 95% rename from Files/Visio_Uninstall.xml rename to SupportFiles/Visio_Uninstall.xml index 35e4de1..644ffa2 100644 --- a/Files/Visio_Uninstall.xml +++ b/SupportFiles/Visio_Uninstall.xml @@ -1,14 +1,14 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file From 42e64e49297a76f76835e5b05329b185764d4905 Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:29:54 -0700 Subject: [PATCH 02/15] Update XML reference paths to dirSupportFiles --- Deploy-M365Apps.ps1 | 8 ++++---- Deploy-Project.ps1 | 4 ++-- Deploy-Visio.ps1 | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Deploy-M365Apps.ps1 b/Deploy-M365Apps.ps1 index 3bc79b0..59d3263 100644 --- a/Deploy-M365Apps.ps1 +++ b/Deploy-M365Apps.ps1 @@ -137,8 +137,8 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\M365Apps_Install.xml`"" - + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\M365Apps_Install.xml`"" + ## Force update group policy gpupdate /force /wait:0 @@ -150,7 +150,7 @@ Try { ## ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 + Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } @@ -183,7 +183,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\M365Apps_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\M365Apps_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index 93dd11d..20af57a 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -137,7 +137,7 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Project_Install.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Install.xml`"" ## Force update group policy gpupdate /force /wait:0 @@ -183,7 +183,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Project_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index 7293e5a..ec748f0 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -137,7 +137,7 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Visio_Install.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Visio_Install.xml`"" ## Force update group policy gpupdate /force /wait:0 @@ -183,7 +183,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirFiles\Visio_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION From 5fd9dadd16ae800c8280bf797d4c62961724bd8d Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:35:37 -0700 Subject: [PATCH 03/15] Cleanup XML. Move logging to PSADT logging dir. --- SupportFiles/M365Apps_Install.xml | 12 ++++++------ SupportFiles/M365Apps_Uninstall.xml | 7 +++---- SupportFiles/Project_Install.xml | 8 +++----- SupportFiles/Project_Uninstall.xml | 13 +++++-------- SupportFiles/Visio_Install.xml | 7 ++----- SupportFiles/Visio_Uninstall.xml | 15 +++++---------- 6 files changed, 24 insertions(+), 38 deletions(-) diff --git a/SupportFiles/M365Apps_Install.xml b/SupportFiles/M365Apps_Install.xml index a32de77..6597e3b 100644 --- a/SupportFiles/M365Apps_Install.xml +++ b/SupportFiles/M365Apps_Install.xml @@ -2,25 +2,20 @@ - - + - - - - @@ -31,8 +26,13 @@ + + + + + \ No newline at end of file diff --git a/SupportFiles/M365Apps_Uninstall.xml b/SupportFiles/M365Apps_Uninstall.xml index 6fb067e..2ceb878 100644 --- a/SupportFiles/M365Apps_Uninstall.xml +++ b/SupportFiles/M365Apps_Uninstall.xml @@ -1,10 +1,9 @@ - - - + + - + \ No newline at end of file diff --git a/SupportFiles/Project_Install.xml b/SupportFiles/Project_Install.xml index 2bf01f1..3b9a449 100644 --- a/SupportFiles/Project_Install.xml +++ b/SupportFiles/Project_Install.xml @@ -2,17 +2,12 @@ - - - - - @@ -23,6 +18,9 @@ + + + \ No newline at end of file diff --git a/SupportFiles/Project_Uninstall.xml b/SupportFiles/Project_Uninstall.xml index e337de5..3177d27 100644 --- a/SupportFiles/Project_Uninstall.xml +++ b/SupportFiles/Project_Uninstall.xml @@ -1,12 +1,9 @@ - - + - - - - + + + + - - diff --git a/SupportFiles/Visio_Install.xml b/SupportFiles/Visio_Install.xml index 050c2ae..2970e95 100644 --- a/SupportFiles/Visio_Install.xml +++ b/SupportFiles/Visio_Install.xml @@ -2,19 +2,14 @@ - - - - - @@ -25,8 +20,10 @@ + + \ No newline at end of file diff --git a/SupportFiles/Visio_Uninstall.xml b/SupportFiles/Visio_Uninstall.xml index 644ffa2..737983f 100644 --- a/SupportFiles/Visio_Uninstall.xml +++ b/SupportFiles/Visio_Uninstall.xml @@ -1,14 +1,9 @@ - - - - - - - - - - \ No newline at end of file + + + + + \ No newline at end of file From 543e8780e504ba526a30c95e129c7fd7bc1f9ec1 Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:42:05 -0700 Subject: [PATCH 04/15] WaitForMsiExec --- Deploy-M365Apps.ps1 | 397 +++++++++++++++++++++----------------------- Deploy-Project.ps1 | 13 +- Deploy-Visio.ps1 | 13 +- 3 files changed, 204 insertions(+), 219 deletions(-) diff --git a/Deploy-M365Apps.ps1 b/Deploy-M365Apps.ps1 index 59d3263..43b1b2e 100644 --- a/Deploy-M365Apps.ps1 +++ b/Deploy-M365Apps.ps1 @@ -1,26 +1,26 @@ <# .SYNOPSIS - This script performs the installation or uninstallation of an application(s). - # LICENSE # - PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. - Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. - 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 . + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + 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 . .DESCRIPTION - The script is provided as a template to perform an install or uninstall of an application(s). - The script either performs an "Install" deployment type or an "Uninstall" deployment type. - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. - The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. .PARAMETER DeploymentType - The type of deployment to perform. Default is: Install. + The type of deployment to perform. Default is: Install. .PARAMETER DeployMode - Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. .PARAMETER AllowRebootPassThru - Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. ConfigMgr) if detected from an installation. If 3010 is passed back to ConfigMgr, a reboot prompt will be triggered. .PARAMETER TerminalServerMode - Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. .PARAMETER DisableLogging - Disables logging to file for the script. Default is: $false. + Disables logging to file for the script. Default is: $false. .EXAMPLE powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE @@ -30,214 +30,203 @@ .EXAMPLE Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES - Toolkit Exit Code Ranges: - 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 - 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 - 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> [CmdletBinding()] Param ( - [Parameter(Mandatory=$false)] - [ValidateSet('Install','Uninstall','Repair')] - [string]$DeploymentType = 'Install', - [Parameter(Mandatory=$false)] - [ValidateSet('Interactive','Silent','NonInteractive')] - [string]$DeployMode = 'Interactive', - [Parameter(Mandatory=$false)] - [switch]$AllowRebootPassThru = $false, - [Parameter(Mandatory=$false)] - [switch]$TerminalServerMode = $false, - [Parameter(Mandatory=$false)] - [switch]$DisableLogging = $false + [Parameter(Mandatory = $false)] + [ValidateSet('Install', 'Uninstall', 'Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory = $false)] + [ValidateSet('Interactive', 'Silent', 'NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory = $false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory = $false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory = $false)] + [switch]$DisableLogging = $false ) Try { - ## Set the script execution policy for this process - Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} - - ##*=============================================== - ##* VARIABLE DECLARATION - ##*=============================================== - ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'M365 Apps' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' - ##*=============================================== - ## Variables: Install Titles (Only set here to override defaults set by the toolkit) - [string]$installName = '' - [string]$installTitle = '' - - ##* Do not modify section below - #region DoNotModify - - ## Variables: Exit Code - [int32]$mainExitCode = 0 - - ## Variables: Script - [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' - [hashtable]$deployAppScriptParameters = $psBoundParameters - - ## Variables: Environment - If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } - [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent - - ## Dot source the required App Deploy Toolkit Functions - Try { - [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" - If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } - If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } - } - Catch { - If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } - Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' - ## Exit the script, returning the exit code to SCCM - If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } - } - - #endregion - ##* Do not modify section above - ##*=============================================== - ##* END VARIABLE DECLARATION - ##*=============================================== - - If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { - ##*=============================================== - ##* PRE-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Installation' - - ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false - - ## - - - ##*=============================================== - ##* INSTALLATION - ##*=============================================== - [string]$installPhase = 'Installation' - - ## Handle Zero-Config MSI Installations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } - } - - ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\M365Apps_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 - - - ##*=============================================== - ##* POST-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Installation' - - ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 - - ## Display a message at the end of the install - #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } - } - ElseIf ($deploymentType -ieq 'Uninstall') - { - ##*=============================================== - ##* PRE-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Uninstallation' - - ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - - ## - - - ##*=============================================== - ##* UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Uninstallation' + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = 'Microsoft' + [string]$appName = '365 Apps for Enterprise' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.1.0' + [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.2' + [string]$deployAppScriptDate = '08/05/2020' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } Catch { + If ($mainExitCode -eq 0) { [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to ConfigMgr + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' + + ## Show Welcome Message, close required applications, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' + + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } - ## Handle Zero-Config MSI Uninstallations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } - - # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\M365Apps_Uninstall.xml`"" + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\M365Apps_Install.xml`"" - ##*=============================================== - ##* POST-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Uninstallation' + ## Force update group policy + Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose - ## + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' + ## + ## Popup notification for restart + Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 - } - ElseIf ($deploymentType -ieq 'Repair') - { - ##*=============================================== - ##* PRE-REPAIR - ##*=============================================== - [string]$installPhase = 'Pre-Repair' + ## Display a message at the end of the install + #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } ElseIf ($deploymentType -ieq 'Uninstall') { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' - ## Show Progress Message (with the default message) - Show-InstallationProgress + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - ## + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ##*=============================================== - ##* REPAIR - ##*=============================================== - [string]$installPhase = 'Repair' + ## - ## Handle Zero-Config MSI Repairs - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } - # + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' - ##*=============================================== - ##* POST-REPAIR - ##*=============================================== - [string]$installPhase = 'Post-Repair' + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } - ## + # + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\M365Apps_Uninstall.xml`"" + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' - } - ##*=============================================== - ##* END SCRIPT BODY - ##*=============================================== + ## - ## Call the Exit-Script function to perform final cleanup operations - Exit-Script -ExitCode $mainExitCode -} -Catch { - [int32]$mainExitCode = 60001 - [string]$mainErrorMessage = "$(Resolve-Error)" - Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName - Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' - Exit-Script -ExitCode $mainExitCode + } ElseIf ($deploymentType -ieq 'Repair') { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' + + ## Show Progress Message (with the default message) + Show-InstallationProgress + + ## + + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' + + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # + + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + + ## + + } + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== + + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode +} Catch { + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode } diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index 20af57a..d72383f 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -116,7 +116,7 @@ Try { ##*=============================================== [string]$installPhase = 'Pre-Installation' - ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + ## Show Welcome Message, close required applications, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) @@ -137,11 +137,10 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Install.xml`"" + ## Force update group policy + Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose ##*=============================================== ##* POST-INSTALLATION @@ -162,7 +161,7 @@ Try { ##*=============================================== [string]$installPhase = 'Pre-Uninstallation' - ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) @@ -183,7 +182,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index ec748f0..74cd1ef 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -137,11 +137,10 @@ Try { } ## - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Visio_Install.xml`"" - - ## Force update group policy - gpupdate /force /wait:0 + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Install.xml`"" + ## Force update group policy + Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose ##*=============================================== ##* POST-INSTALLATION @@ -162,7 +161,7 @@ Try { ##*=============================================== [string]$installPhase = 'Pre-Uninstallation' - ## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) @@ -170,7 +169,6 @@ Try { ## - ##*=============================================== ##* UNINSTALLATION ##*=============================================== @@ -183,7 +181,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION @@ -192,7 +190,6 @@ Try { ## - } ElseIf ($deploymentType -ieq 'Repair') { From ec3ddc8d81368cb7c340930b7f02c256d4f86212 Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:43:33 -0700 Subject: [PATCH 05/15] Just some more cleanup. Change gpupdate to cmdlet --- Deploy-Project.ps1 | 18 +++++++++--------- Deploy-Visio.ps1 | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index d72383f..f5e07eb 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -61,15 +61,15 @@ Try { ##* VARIABLE DECLARATION ##*=============================================== ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'Project' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' + [string]$appVendor = 'Microsoft' + [string]$appName = 'Project' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.1.0' + [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) [string]$installName = '' diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index 74cd1ef..cd6f445 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -63,13 +63,13 @@ Try { ## Variables: Application [string]$appVendor = 'Microsoft' [string]$appName = 'Visio' - [string]$appVersion = '' - [string]$appArch = 'x64' - [string]$appLang = 'EN' - [string]$appRevision = '01' - [string]$appScriptVersion = '1.0.0' - [string]$appScriptDate = '07/08/2020' - [string]$appScriptAuthor = 'Sandy Zeng' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.1.0' + [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) [string]$installName = '' From 4c69e824f9a6f507abb44d7a9c8a45cfbb247fea Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:45:12 -0700 Subject: [PATCH 06/15] PUSH --- SupportFiles/Project_Uninstall.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SupportFiles/Project_Uninstall.xml b/SupportFiles/Project_Uninstall.xml index 3177d27..bb5c793 100644 --- a/SupportFiles/Project_Uninstall.xml +++ b/SupportFiles/Project_Uninstall.xml @@ -6,4 +6,4 @@ - + \ No newline at end of file From 6c882a71b88bee874c2375dabe2f358a36d49428 Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 10 Aug 2020 13:46:33 -0700 Subject: [PATCH 07/15] WaitForMsiExec --- Deploy-Project.ps1 | 4 ++-- Deploy-Visio.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index f5e07eb..bf48ef4 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -149,7 +149,7 @@ Try { ## ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 + Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } @@ -182,7 +182,7 @@ Try { } # - Execute-Process -Path "$dirFiles\setup.exe" -Parameters "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" ##*=============================================== ##* POST-UNINSTALLATION diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index cd6f445..65e37e4 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -149,7 +149,7 @@ Try { ## ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 + Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } From 057234179e0ad795788ccfd4ea079b871a6e5ce0 Mon Sep 17 00:00:00 2001 From: Cameron Kollwitz Date: Mon, 24 Aug 2020 17:17:14 -0700 Subject: [PATCH 08/15] Updates --- Deploy-M365Apps.ps1 | 3 +++ Deploy-Project.ps1 | 3 +++ Deploy-Visio.ps1 | 3 +++ SupportFiles/M365Apps_Install.xml | 4 ++-- SupportFiles/M365Apps_Uninstall.xml | 4 ++-- SupportFiles/Project_Install.xml | 4 ++-- SupportFiles/Visio_Install.xml | 4 ++-- 7 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Deploy-M365Apps.ps1 b/Deploy-M365Apps.ps1 index 43b1b2e..7a14935 100644 --- a/Deploy-M365Apps.ps1 +++ b/Deploy-M365Apps.ps1 @@ -1,4 +1,7 @@ <# + https://github.com/cameronkollwitz/DeployM365Apps/ +#> +<# .SYNOPSIS This script performs the installation or uninstallation of an application(s). # LICENSE # diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index bf48ef4..73353ff 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -1,4 +1,7 @@ <# + https://github.com/cameronkollwitz/DeployM365Apps/ +#> +<# .SYNOPSIS This script performs the installation or uninstallation of an application(s). # LICENSE # diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index 65e37e4..5bd4015 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -1,4 +1,7 @@ <# + https://github.com/cameronkollwitz/DeployM365Apps/ +#> +<# .SYNOPSIS This script performs the installation or uninstallation of an application(s). # LICENSE # diff --git a/SupportFiles/M365Apps_Install.xml b/SupportFiles/M365Apps_Install.xml index 6597e3b..5f8f15f 100644 --- a/SupportFiles/M365Apps_Install.xml +++ b/SupportFiles/M365Apps_Install.xml @@ -1,5 +1,5 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/SupportFiles/M365Apps_Uninstall.xml b/SupportFiles/M365Apps_Uninstall.xml index 2ceb878..5941fb4 100644 --- a/SupportFiles/M365Apps_Uninstall.xml +++ b/SupportFiles/M365Apps_Uninstall.xml @@ -1,9 +1,9 @@ - + - + \ No newline at end of file diff --git a/SupportFiles/Project_Install.xml b/SupportFiles/Project_Install.xml index 3b9a449..043e2e2 100644 --- a/SupportFiles/Project_Install.xml +++ b/SupportFiles/Project_Install.xml @@ -1,5 +1,5 @@ - + @@ -18,7 +18,7 @@ - + diff --git a/SupportFiles/Visio_Install.xml b/SupportFiles/Visio_Install.xml index 2970e95..d36d32a 100644 --- a/SupportFiles/Visio_Install.xml +++ b/SupportFiles/Visio_Install.xml @@ -1,5 +1,5 @@ - + @@ -20,7 +20,7 @@ - + From d15b63554332063d33e92337078824dac17dc31d Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 17 Jan 2021 20:52:11 -0800 Subject: [PATCH 09/15] Add initial README.md --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5695642 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# DeployM365Apps From 6692f9611c6d999031ad3e81c171e36ce2f94dba Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 17 Jan 2021 20:52:27 -0800 Subject: [PATCH 10/15] Add CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..a0cf709 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1 @@ +# CHANGELOG From 4c2451f17a5b18788005e7243d99ee476cf52f3a Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 17 Jan 2021 20:52:49 -0800 Subject: [PATCH 11/15] Add CONTRIBUTING --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c6b9e95 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +# CONTRIBUTING From 039c50f87dc2190e9b3d59bde26f2e4d0a1c9d58 Mon Sep 17 00:00:00 2001 From: cameronkollwitz Date: Sun, 17 Jan 2021 21:16:12 -0800 Subject: [PATCH 12/15] Update to v3.8.3 --- .gitignore | 1 + AppDeployToolkit/AppDeployToolkitConfig.xml | 124 +- .../AppDeployToolkitExtensions.ps1 | 16 +- AppDeployToolkit/AppDeployToolkitHelp.ps1 | 144 +- AppDeployToolkit/AppDeployToolkitMain.cs | 1899 +- AppDeployToolkit/AppDeployToolkitMain.ps1 | 18920 ++++++++-------- CHANGELOG | 3 + Deploy-Application.ps1 | 237 + Deploy-M365Apps.ps1 | 66 +- Deploy-Project.ps1 | 354 +- Deploy-Visio.ps1 | 374 +- README.md | 2 + SupportFiles/M365Apps_Install.xml | 4 +- SupportFiles/Project_Install.xml | 4 +- SupportFiles/Visio_Install.xml | 2 +- 15 files changed, 11565 insertions(+), 10585 deletions(-) create mode 100644 .gitignore create mode 100644 Deploy-Application.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3c2f59 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +# .gitignore diff --git a/AppDeployToolkit/AppDeployToolkitConfig.xml b/AppDeployToolkit/AppDeployToolkitConfig.xml index e0aa7f9..a8ca9e3 100644 --- a/AppDeployToolkit/AppDeployToolkitConfig.xml +++ b/AppDeployToolkit/AppDeployToolkitConfig.xml @@ -8,8 +8,8 @@ --> - 3.8.2 - 08/05/2020 + 3.8.3 + 30/09/2020 @@ -18,10 +18,16 @@ $envTemp - HKLM:SOFTWARE + HKLM:\SOFTWARE $envWinDir\Logs\Software + $envTemp + + HKCU:\SOFTWARE + + $envProgramData\Logs\Software + False CMTrace @@ -52,6 +58,8 @@ $envWinDir\Logs\Software + $envProgramData\Logs\Software + REBOOT=ReallySuppress /QB-! REBOOT=ReallySuppress /QN @@ -126,8 +134,7 @@ The following programs must be closed before the installation can proceed. Please save your work, close the programs, and then continue. - Alternatively, save your work and click "Close Programs". - + Alternatively, save your work and click "Close Programs". NOTE: The program(s) will be automatically closed in: @@ -181,7 +188,7 @@ Your computer will be automatically restarted at the end of the countdown. Time remaining: - Restart Later + Minimize Restart Now @@ -201,8 +208,7 @@ Følgende programmer skal lukkes før installationen kan fortsætte. Gem dit arbejde, luk programmerne og fortsæt. - Alternativt kan du gemme dit arbejde og trykke på "Luk Programmer". - + Alternativt kan du gemme dit arbejde og trykke på "Luk Programmer". BEMÆRK: Programmet/Programmerne vil automatisk blive lukket om: Følgende applikation vil nu blive installeret: Du kan vælge at udsætte installationen indtil udsættelsesperioden udløber: @@ -231,7 +237,7 @@ Du bør venligst gemme dit arbejde og genstarte indenfor det givne tidsrum. Din computer vil automatisk blive genstartet når nedtællingen er færdig. Tid tilbage: - Genstart Senere + Minimere Genstart Nu @@ -251,8 +257,7 @@ Les programmes suivants doivent être fermés afin que l'installation s'initialise. Merci de sauvegarder votre travail, fermer tous les programmes, et continuer. - Vous pouvez aussi sauvegarder votre travail puis cliquez sur « Fermer Programmes ». - + Vous pouvez aussi sauvegarder votre travail puis cliquez sur « Fermer Programmes ». REMARQUE: Les programmes seront automatiquement fermés dans: L'application suivante est sur le point d'être installée: Vous pouvez choisir de reporter l'installation: @@ -280,7 +285,7 @@ Merci de sauvegarder votre travail et de redémarrer avant que le temps spécifié ne soit écoulé. Votre ordinateur sera automatiquement redémarré à la fin du décompte. Temps restant: - Redémarrer Plus Tard + Minimiser Redémarrer Maintenant @@ -300,8 +305,7 @@ Die folgenden Programme müssen geschlossen werden, bevor die Installation fortgesetzt werden kann. Bitte speichern Sie Ihre Arbeit, schließen Sie die Programme und fahren Sie dann fort. - Alternativ können Sie Ihre Arbeit speichern und dann auf „Programme Schließen“ klicken. - + Alternativ können Sie Ihre Arbeit speichern und dann auf „Programme Schließen“ klicken. HINWEIS: Diese Programme werden automatisch geschlossen: Die folgende Anwendung soll installiert werden: Sie können die Installation verzögern, bis die Rückstellung abläuft: @@ -329,7 +333,7 @@ Bitte speichern Sie Ihre Arbeit und starten Sie den Computer innerhalb der vorgegebenen Zeit neu. Am Ende des Countdowns wird Ihr Computer automatisch neu gestartet. Verbleibende Zeit: - Später Neustarten + Minimieren Jetzt Neustarten @@ -349,8 +353,7 @@ I seguenti programmi devono essere chiusi prima che l'installazione possa procedere. Salvare il lavoro , chiudere i programmi, e poi continuare. - In alternativa, salvare il lavoro e fare clic su "Chiudi Programmi". - + In alternativa, salvare il lavoro e fare clic su "Chiudi Programmi". NOTA: il programma(s) sarà chiuso automaticamente in: La seguente applicazione sta per essere installata: Si può decidere di posticipare l'installazione fino alla prossima richiesta automatica: @@ -378,7 +381,7 @@ Salvare il lavoro e riavviare entro il tempo assegnato. Il computer verrà riavviato automaticamente al termine del conto alla rovescia. Tempo rimanente: - Riavvia Seguito + Minimizzare Riavvia Ora @@ -398,8 +401,7 @@ インストールを実行するために、下記のプログラムを閉じる必要があります。 実行中のアプリケーションを保存し、閉じてから続行してください。 - または、実行中のアプリケーションを保存し、プログラムを強制終了ボタンをクリックしてくだい - + または、実行中のアプリケーションを保存し、プログラムを強制終了ボタンをクリックしてくだい 注意: これらのプログラムは自動的に閉じられます: このアプリケーションはこれからインストールされます。 再試行可能回数が0になるまでは、都合の良い時にインストール可能です。 @@ -427,7 +429,7 @@ 実行中のアプリケーションを保存し、再起動してください。 カウントダウン後にコンピュータが再起動します。 残時間: - 後で再起動 + 最小 化 今すぐ再起動 @@ -447,8 +449,7 @@ Følgende programmer må lukkes før installasjonen kan fortsette. Lagre arbeidet, lukk programmene og velg "Fortsett" - Eller velg "Lukk Programmer" uten å lagre. - + Eller velg "Lukk Programmer" uten å lagre. OBS: Programmet vil automatisk lukkes om: Følgende program vil bli installert: Du kan velge å utsette installasjonen et begrenset antall ganger inntil fristen utløper: @@ -476,7 +477,7 @@ Lagre arbeidet ditt og gjør en omstart av pc innen fristen. Pcen vil automatisk starte på nytt, når nedtellingen er slutt. Tid som gjenstår: - Omstart Senere + Minimere Omstart Nå @@ -496,8 +497,7 @@ De volgende applicaties moeten afgesloten worden om de installatie te voltooien. Sla je werk op, sluit de applicaties, en ga verder. - Of, sla je werk op en klik op 'Sluit Applicaties'. - + Of, sla je werk op en klik op 'Sluit Applicaties'. LET OP: De applicatie(s) worden afgesloten over: De volgende applicatie wordt zometeen geïnstalleerd: Je kan de installatie uitstellen tot het maximale uitsteltermijn is verstreken: @@ -525,7 +525,7 @@ Gelieve je werk op te slaan en binnen het toegestane termijn de computer herstarten De computer zal herstarten als de teller op nul staat Resterende tijd: - Herstart Later + Minimaliseren Herstart Nu @@ -545,8 +545,7 @@ Następujące programy muszą zostać zamknięte przed rozpoczęciem instalacji. Proszę zapisać wszystkie dokumenty i zamknąć programy, a następnie kliknąć przycisk „Kontynuuj”. - Alternatywnie zapisz wszystkie dokumenty i kliknij przycisk „Zamknij Programy”. - + Alternatywnie zapisz wszystkie dokumenty i kliknij przycisk „Zamknij Programy”. UWAGA: Programy zostaną automatycznie zamknięte za: Zostanie zainstalowana następująca aplikacja: Instalacja może zostać przełożona na późniejszy termin. @@ -574,7 +573,7 @@ Proszę zapisać wszystkie dokumenty i zrestartować komputer w wyznaczonym czasie. Komputer zostanie automatycznie zrestartowany po upływie wyznaczonego czasu. Pozostały czas do restartu automatycznego: - Restartuj Później + Zminimalizować Restartuj Teraz @@ -594,8 +593,7 @@ Programas de seguir devem ser fechados antes que a instalação possa prosseguir. Por favor, guarde o seu trabalho, feche os programas e em seguida continuar. - Como alternativa, salve seu trabalho e clique em "Fechar Programas". - + Como alternativa, salve seu trabalho e clique em "Fechar Programas". NOTA: O programa será fechado automaticamente em: O seguinte aplicativo está prestes a ser instalado: Você pode optar por adiar a instalação até que expire o diferimento: @@ -623,7 +621,7 @@ Por favor, salve o trabalho e reiniciar no tempo alocado. Seu computador será reiniciado automaticamente no final da contagem regressiva. Tempo restante: - Reiniciar Mais Tarde + Minimizar Reinicie Agora @@ -643,8 +641,7 @@ Os seguintes programas precisam ser fechados antes que a instalação possa prosseguir. Salve seu trabalho, feche os programas e depois continue. - Como alternativa, salve seu trabalho e clique em "Fechar Programas". - + Como alternativa, salve seu trabalho e clique em "Fechar Programas". OBSERVAÇÃO: O(s) programa(s) será(ão) automaticamente fechado(s) em: O seguinte aplicativo está prestes a ser instalado: Você pode optar por adiar a instalação até que o adiamento expire: @@ -672,7 +669,7 @@ Salve seu trabalho e reinicie dentro do prazo estipulado. Seu computador será reiniciado automaticamente no final da contagem regressiva. Tempo restante: - Reiniciar Mais Tarde + Minimizar Reiniciar Agora @@ -692,8 +689,7 @@ Los siguientes programas deben cerrarse antes de la instalación puede proceder. Por favor, guarde el trabajo, cerrar los programas y luego continuar. - Alternativamente, guarde su trabajo y haga clic en "Cerrar Programas". - + Alternativamente, guarde su trabajo y haga clic en "Cerrar Programas". NOTA: El programa se cerrará automáticamente en: La siguiente aplicación está a punto de instalarse: Puede decidir aplazar la instalación hasta que expire el aplazamiento: @@ -721,7 +717,7 @@ Por favor guarde su trabajo y reinicie dentro del tiempo asignado. El ordenador se reiniciará automáticamente al final de la cuenta regresiva de. Tiempo restante: - Reiniciar Más Tarde + Minimizar Reiniciar Ahora @@ -741,8 +737,7 @@ Följande program måste stängas innan installationen kan fortsätta. Se till att spara ditt arbete, stäng de öppna programmen och klicka sen på "Fortsätt". - Alternativt, spara ditt arbete och klicka på "Stäng Program". - + Alternativt, spara ditt arbete och klicka på "Stäng Program". OBS: Programmen kommer automatiskt att avslutas om: Följande applikation kommer att installeras: Du kan välja att fördröja installationen ett begränsat antal gånger under en begränsad tid: @@ -770,7 +765,7 @@ Se till att spara ditt arbete innan tiden går ut och en automatisk omstart sker. Din dator kommer automatiskt att starta om när nedräkningen är slut. Återstående tid: - Starta Om Senare + Minimera Starta Om Nu @@ -790,8 +785,7 @@ يجب إغلاق البرامج التالية قبل التمكن من متابعة عملية التثبيت. يرجى حفظ عملك، وإغلاق البرامج، ومن ثم المتابعة. - يمكنك بدلا من ذلك، حفظ عملك والنقر فوق "إغلاق البرامج". - + يمكنك بدلا من ذلك، حفظ عملك والنقر فوق "إغلاق البرامج". ملاحظة: سيتم إغلاق البرنامج/البرامج بشكل تلقائي خلال: التطبيق التالي على وشك التثبيت: بإمكانك اختيار تأجيل التثبيت إلى حين انتهاء صلاحية التأجيل: @@ -819,7 +813,7 @@ يرجى حفظ عملك وإعادة التشغيل خلال الوقت المخصص. ستتم إعادة تشغيل حاسوبك بشكل تلقائي عند نهاية العد التنازلي. الزمن المتبقي: - إعادة التشغيل لاحقًا + تقليل إعادة التشغيل الآن @@ -839,8 +833,7 @@ יש לסגור את התכנות הבאות בטרם ההתקנה תוכל להתחיל. אנא שמור על העבודה שלך, סגור את התכניות, ואז המשך. - לחילופין, שמור על העבודה שלך והקלק על "סגור תכניות". - + לחילופין, שמור על העבודה שלך והקלק על "סגור תכניות". שים לב: התכנית(ות) תסגרנה באופן אוטומטי תוך: היישום הבא עומד להיות מותקן: אתה יכול לבחור לדחות את ההתקנה עד שמשך זמן הדחיה יפוג. @@ -868,7 +861,7 @@ אנא שמור על העבודה שלך ואתחל במסגרת הזמן המוקצב. המחשב שלך יאותחל באופן אוטומטי בסיום הספירה לאחור. הזמן הנותר: - אתחל מאוחר יותר + מזער את אתחל עכשיו @@ -888,8 +881,7 @@ 설치를 계속하려면 다음의 프로그램을 종료해야 합니다. 사용자 작업을 저장하고 프로그램을 종료한 후 계속하세요. - 다른 방법으로는 사용자 작업을 저장하고 "프로그램 종료"를 클릭하세요. - + 다른 방법으로는 사용자 작업을 저장하고 "프로그램 종료"를 클릭하세요. 참고: 프로그램이 자동으로 종료되는 경우: 다음의 응용 프로그램을 설치합니다: 지연 기간이 만료될 때까지 설치를 연기할 수 있습니다: @@ -917,7 +909,7 @@ 사용자 작업을 저장하고 지정된 시간 이내에 다시 시작하세요. 카운트다운이 종료되면 컴퓨터는 자동으로 다시 시작합니다. 남은 시간: - 나중에 다시 시작 + 최소화 지금 다시 시작 @@ -937,8 +929,7 @@ Перед продолжением установки необходимо закрыть следующие программы. Пожалуйста, сохраните вашу работу и закройте программы, а затем продолжите установку. - Также вы можете сохранить вашу работу и нажать "Закрыть программы". - + Также вы можете сохранить вашу работу и нажать "Закрыть программы". ПРИМЕЧАНИЕ: Эти программы будут автоматически закрыты через: Планируется установка следующего приложения: Вы можете отложить установку приложения до тех пор, пока не истечет срок действия этой отсрочки: @@ -966,7 +957,7 @@ Пожалуйста, сохраните вашу работу и выполните перезагрузку в отведенное время. Ваш компьютер будет автоматически перезагружен по завершению обратного отсчета. Оставшееся время: - Перезагрузить позже + Минимизировать Перезагрузить сейчас @@ -986,8 +977,7 @@ 为继续安装,必须关闭下列程序。 请保存您的工作,关闭程序,然后继续。 - 或者保存您的工作,点击"关闭程序"。 - + 或者保存您的工作,点击"关闭程序"。 注:下列程序将自动关闭: 即将安装下列应用程式: 在延期失效前,可选择延迟安装: @@ -1015,7 +1005,7 @@ 请保存您的工作,并在容许时间重启计算机。 倒计时结束后,计算机将自动重启。 剩余时间: - 稍后重启 + 最小化 现在重启 @@ -1035,8 +1025,7 @@ 在繼續安裝前必須關閉下列程序。 請保存您的工作,關閉程序,然後繼續。 - 或者保存您的工作,然後點擊"關閉程序"。 - + 或者保存您的工作,然後點擊"關閉程序"。 注:下列程序將自動關閉: 即將安裝下列應用程式: 在延期失效前,可選擇延遲安裝: @@ -1064,7 +1053,7 @@ 請保存您的工作,然後在容許時間重啟計算機。 倒計時結束後,計算機將自動重啟。 剩餘時間: - 稍後重啟 + 最小化 現在重啟 @@ -1084,8 +1073,7 @@ Nasledujúce programy musia byť zatvorené, než bude inštalácia pokračovať. Prosím, uložte svoju prácu, zatvorte dané programy a potom kliknite na pokračovať. - Prípadne môžete uložiť svoju prácu a potom kliknite na tlačidlo "Ukončiť programy". - + Prípadne môžete uložiť svoju prácu a potom kliknite na tlačidlo "Ukončiť programy". Poznámka: Programy budú automaticky ukončené za: Nasledujúca aplikácia bude nainštalovaná: Inštaláciu môžete niekoľkokrát odložiť: @@ -1113,7 +1101,7 @@ Prosím, uložte si prácu a reštartujte počítač v stanovenej lehote. Na konci odpočítavania, bude váš počítač automaticky reštartovaný. Zostávajúci čas: - Reštartovať Neskôr + Minimalizovať Reštartovať Teraz @@ -1133,8 +1121,7 @@ Následující programy musí být zavřené, aby instalace mohla pokračovat. Prosím, uložte svou práci, zavřete program a potom klikněte na "Pokračovat". - Případně můžete svou práci uložit a kliknout na tlačítko "Ukončit programy". - + Případně můžete svou práci uložit a kliknout na tlačítko "Ukončit programy". Upozornění: Programy budou automaticky zavřené za: Nasledující aplikace bude nainstalována: Instalaci můžete několikrát odložit: @@ -1162,7 +1149,7 @@ Prosím, uložte si práci a restartujte počítač ve stanoveném čase. Na konci odpočítávání, bude váš počítač automaticky restartovaný. Zbývající čas: - Restartovat později + Minimalizovat Restartovat nyní @@ -1181,8 +1168,7 @@ Kérjük mentse munkáját és a folytatáshoz zárja be a futó alkalmazásokat. Vagy - Kérjük mentse munkáját és kattintson a „Programok bezárása”-ra. - + Kérjük mentse munkáját és kattintson a „Programok bezárása”-ra. Megjegyzés: a programok automatikusan bezárásra kerülnek,: A következő alkalmazások telepítésre kerülnek: A telepítést elhalaszthatja amíg a rendelkezésre álló idő lejár: @@ -1210,7 +1196,7 @@ Kérem mentse munkáját, és a megadott időn belül indítsa újra.. A hátralévő idő leteltével a számítógép újraindul. Hátralévő idő: - Újraindítás később + Minimalizál Újraindítás most diff --git a/AppDeployToolkit/AppDeployToolkitExtensions.ps1 b/AppDeployToolkit/AppDeployToolkitExtensions.ps1 index 6bbe8a4..704c69e 100644 --- a/AppDeployToolkit/AppDeployToolkitExtensions.ps1 +++ b/AppDeployToolkit/AppDeployToolkitExtensions.ps1 @@ -1,20 +1,20 @@ -<# +<# .SYNOPSIS - This script is a template that allows you to extend the toolkit with your own custom functions. + This script is a template that allows you to extend the toolkit with your own custom functions. # LICENSE # PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. 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 . .DESCRIPTION - The script is automatically dot-sourced by the AppDeployToolkitMain.ps1 script. + The script is automatically dot-sourced by the AppDeployToolkitMain.ps1 script. .NOTES Toolkit Exit Code Ranges: 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> [CmdletBinding()] Param ( @@ -27,8 +27,8 @@ Param ( # Variables: Script [string]$appDeployToolkitExtName = 'PSAppDeployToolkitExt' [string]$appDeployExtScriptFriendlyName = 'App Deploy Toolkit Extensions' -[version]$appDeployExtScriptVersion = [version]'3.8.2' -[string]$appDeployExtScriptDate = '08/05/2020' +[version]$appDeployExtScriptVersion = [version]'3.8.3' +[string]$appDeployExtScriptDate = '30/09/2020' [hashtable]$appDeployExtScriptParameters = $PSBoundParameters ##*=============================================== @@ -46,9 +46,9 @@ Param ( ##*=============================================== If ($scriptParentPath) { - Write-Log -Message "Script [$($MyInvocation.MyCommand.Definition)] dot-source invoked by [$(((Get-Variable -Name MyInvocation).Value).ScriptName)]" -Source $appDeployToolkitExtName + Write-Log -Message "Script [$($MyInvocation.MyCommand.Definition)] dot-source invoked by [$(((Get-Variable -Name MyInvocation).Value).ScriptName)]" -Source $appDeployToolkitExtName } Else { - Write-Log -Message "Script [$($MyInvocation.MyCommand.Definition)] invoked directly" -Source $appDeployToolkitExtName + Write-Log -Message "Script [$($MyInvocation.MyCommand.Definition)] invoked directly" -Source $appDeployToolkitExtName } ##*=============================================== diff --git a/AppDeployToolkit/AppDeployToolkitHelp.ps1 b/AppDeployToolkit/AppDeployToolkitHelp.ps1 index dc516fb..b5214ee 100644 --- a/AppDeployToolkit/AppDeployToolkitHelp.ps1 +++ b/AppDeployToolkit/AppDeployToolkitHelp.ps1 @@ -1,18 +1,18 @@ <# .SYNOPSIS - Displays a graphical console to browse the help for the App Deployment Toolkit functions + Displays a graphical console to browse the help for the App Deployment Toolkit functions # LICENSE # PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. 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 . .DESCRIPTION - Displays a graphical console to browse the help for the App Deployment Toolkit functions + Displays a graphical console to browse the help for the App Deployment Toolkit functions .EXAMPLE - AppDeployToolkitHelp.ps1 + AppDeployToolkitHelp.ps1 .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> ##*=============================================== @@ -22,8 +22,8 @@ ## Variables: Script [string]$appDeployToolkitHelpName = 'PSAppDeployToolkitHelp' [string]$appDeployHelpScriptFriendlyName = 'App Deploy Toolkit Help' -[version]$appDeployHelpScriptVersion = [version]'3.8.2' -[string]$appDeployHelpScriptDate = '08/05/2020' +[version]$appDeployHelpScriptVersion = [version]'3.8.3' +[string]$appDeployHelpScriptDate = '30/09/2020' ## Variables: Environment [string]$scriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent @@ -39,75 +39,75 @@ ##*=============================================== Function Show-HelpConsole { - ## Import the Assemblies - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - Add-Type -AssemblyName System.Drawing -ErrorAction 'Stop' + ## Import the Assemblies + Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' + Add-Type -AssemblyName System.Drawing -ErrorAction 'Stop' - ## Form Objects - $HelpForm = New-Object -TypeName 'System.Windows.Forms.Form' - $HelpListBox = New-Object -TypeName 'System.Windows.Forms.ListBox' - $HelpTextBox = New-Object -TypeName 'System.Windows.Forms.RichTextBox' - $InitialFormWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' + ## Form Objects + $HelpForm = New-Object -TypeName 'System.Windows.Forms.Form' + $HelpListBox = New-Object -TypeName 'System.Windows.Forms.ListBox' + $HelpTextBox = New-Object -TypeName 'System.Windows.Forms.RichTextBox' + $InitialFormWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' - ## Form Code - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 665 - $System_Drawing_Size.Width = 957 - $HelpForm.ClientSize = $System_Drawing_Size - $HelpForm.DataBindings.DefaultDataSourceUpdateMode = 0 - $HelpForm.Name = 'HelpForm' - $HelpForm.Text = 'PowerShell App Deployment Toolkit Help Console' - $HelpForm.WindowState = 'Normal' - $HelpForm.ShowInTaskbar = $true - $HelpForm.FormBorderStyle = 'Fixed3D' - $HelpForm.MaximizeBox = $false - $HelpForm.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - $HelpListBox.Anchor = 7 - $HelpListBox.BorderStyle = 1 - $HelpListBox.DataBindings.DefaultDataSourceUpdateMode = 0 - $HelpListBox.Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList ('Microsoft Sans Serif', 9.75, 1, 3, 1) - $HelpListBox.FormattingEnabled = $true - $HelpListBox.ItemHeight = 16 - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = 0 - $HelpListBox.Location = $System_Drawing_Point - $HelpListBox.Name = 'HelpListBox' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 658 - $System_Drawing_Size.Width = 271 - $HelpListBox.Size = $System_Drawing_Size - $HelpListBox.Sorted = $true - $HelpListBox.TabIndex = 2 - $HelpListBox.add_SelectedIndexChanged({ $HelpTextBox.Text = Get-Help -Name $HelpListBox.SelectedItem -Full | Out-String }) - $helpFunctions = Get-Command -CommandType 'Function' | Where-Object { ($_.HelpUri -match 'psappdeploytoolkit') -and ($_.Definition -notmatch 'internal script function') } | Select-Object -ExpandProperty Name - $null = $HelpListBox.Items.AddRange($helpFunctions) - $HelpForm.Controls.Add($HelpListBox) - $HelpTextBox.Anchor = 11 - $HelpTextBox.BorderStyle = 1 - $HelpTextBox.DataBindings.DefaultDataSourceUpdateMode = 0 - $HelpTextBox.Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList ('Microsoft Sans Serif', 8.5, 0, 3, 1) - $HelpTextBox.ForeColor = [System.Drawing.Color]::FromArgb(255, 0, 0, 0) - $System_Drawing_Point = New-Object -TypeName System.Drawing.Point - $System_Drawing_Point.X = 277 - $System_Drawing_Point.Y = 0 - $HelpTextBox.Location = $System_Drawing_Point - $HelpTextBox.Name = 'HelpTextBox' - $HelpTextBox.ReadOnly = $True - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 658 - $System_Drawing_Size.Width = 680 - $HelpTextBox.Size = $System_Drawing_Size - $HelpTextBox.TabIndex = 1 - $HelpTextBox.Text = '' - $HelpForm.Controls.Add($HelpTextBox) + ## Form Code + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' + $System_Drawing_Size.Height = 665 + $System_Drawing_Size.Width = 957 + $HelpForm.ClientSize = $System_Drawing_Size + $HelpForm.DataBindings.DefaultDataSourceUpdateMode = 0 + $HelpForm.Name = 'HelpForm' + $HelpForm.Text = 'PowerShell App Deployment Toolkit Help Console' + $HelpForm.WindowState = 'Normal' + $HelpForm.ShowInTaskbar = $true + $HelpForm.FormBorderStyle = 'Fixed3D' + $HelpForm.MaximizeBox = $false + $HelpForm.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + $HelpListBox.Anchor = 7 + $HelpListBox.BorderStyle = 1 + $HelpListBox.DataBindings.DefaultDataSourceUpdateMode = 0 + $HelpListBox.Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList ('Microsoft Sans Serif', 9.75, 1, 3, 1) + $HelpListBox.FormattingEnabled = $true + $HelpListBox.ItemHeight = 16 + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' + $System_Drawing_Point.X = 0 + $System_Drawing_Point.Y = 0 + $HelpListBox.Location = $System_Drawing_Point + $HelpListBox.Name = 'HelpListBox' + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' + $System_Drawing_Size.Height = 658 + $System_Drawing_Size.Width = 271 + $HelpListBox.Size = $System_Drawing_Size + $HelpListBox.Sorted = $true + $HelpListBox.TabIndex = 2 + $HelpListBox.add_SelectedIndexChanged({ $HelpTextBox.Text = Get-Help -Name $HelpListBox.SelectedItem -Full | Out-String }) + $helpFunctions = Get-Command -CommandType 'Function' | Where-Object { ($_.HelpUri -match 'psappdeploytoolkit') -and ($_.Definition -notmatch 'internal script function') } | Select-Object -ExpandProperty Name + $null = $HelpListBox.Items.AddRange($helpFunctions) + $HelpForm.Controls.Add($HelpListBox) + $HelpTextBox.Anchor = 11 + $HelpTextBox.BorderStyle = 1 + $HelpTextBox.DataBindings.DefaultDataSourceUpdateMode = 0 + $HelpTextBox.Font = New-Object -TypeName 'System.Drawing.Font' -ArgumentList ('Microsoft Sans Serif', 8.5, 0, 3, 1) + $HelpTextBox.ForeColor = [System.Drawing.Color]::FromArgb(255, 0, 0, 0) + $System_Drawing_Point = New-Object -TypeName System.Drawing.Point + $System_Drawing_Point.X = 277 + $System_Drawing_Point.Y = 0 + $HelpTextBox.Location = $System_Drawing_Point + $HelpTextBox.Name = 'HelpTextBox' + $HelpTextBox.ReadOnly = $True + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' + $System_Drawing_Size.Height = 658 + $System_Drawing_Size.Width = 680 + $HelpTextBox.Size = $System_Drawing_Size + $HelpTextBox.TabIndex = 1 + $HelpTextBox.Text = '' + $HelpForm.Controls.Add($HelpTextBox) - ## Save the initial state of the form - $InitialFormWindowState = $HelpForm.WindowState - ## Init the OnLoad event to correct the initial state of the form - $HelpForm.add_Load($OnLoadForm_StateCorrection) - ## Show the Form - $null = $HelpForm.ShowDialog() + ## Save the initial state of the form + $InitialFormWindowState = $HelpForm.WindowState + ## Init the OnLoad event to correct the initial state of the form + $HelpForm.add_Load($OnLoadForm_StateCorrection) + ## Show the Form + $null = $HelpForm.ShowDialog() } ##*=============================================== diff --git a/AppDeployToolkit/AppDeployToolkitMain.cs b/AppDeployToolkit/AppDeployToolkitMain.cs index 2e952ce..ba7deb3 100644 --- a/AppDeployToolkit/AppDeployToolkitMain.cs +++ b/AppDeployToolkit/AppDeployToolkitMain.cs @@ -1,792 +1,1129 @@ -// Date Modified: 08/05/2020 -// Version Number: 3.8.2 - +// Date Modified: 30/09/2020 +// Version Number: 3.8.3 using System; -using System.Text; using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.DirectoryServices; -using System.Security.Principal; -using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text; using System.Text.RegularExpressions; + using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; namespace PSADT { - public class Msi - { - enum LoadLibraryFlags : int - { - DONT_RESOLVE_DLL_REFERENCES = 0x00000001, - LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, - LOAD_LIBRARY_AS_DATAFILE = 0x00000002, - LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, - LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, - LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 - } - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, LoadLibraryFlags dwFlags); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - static extern int LoadString(IntPtr hInstance, int uID, StringBuilder lpBuffer, int nBufferMax); - - // Get MSI exit code message from msimsg.dll resource dll - public static string GetMessageFromMsiExitCode(int errCode) - { - IntPtr hModuleInstance = LoadLibraryEx("msimsg.dll", IntPtr.Zero, LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE); - - StringBuilder sb = new StringBuilder(255); - LoadString(hModuleInstance, errCode, sb, sb.Capacity + 1); - - return sb.ToString(); - } - } - - public class Explorer - { - private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); - private const int WM_SETTINGCHANGE = 0x1a; - private const int SMTO_ABORTIFHUNG = 0x0002; - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - static extern bool SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern IntPtr SendMessageTimeout(IntPtr hWnd, int Msg, IntPtr wParam, string lParam, int fuFlags, int uTimeout, IntPtr lpdwResult); - - [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); - - public static void RefreshDesktopAndEnvironmentVariables() - { - // Update desktop icons - SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero); - SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, null, SMTO_ABORTIFHUNG, 100, IntPtr.Zero); - // Update environment variables - SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, IntPtr.Zero, "Environment", SMTO_ABORTIFHUNG, 100, IntPtr.Zero); - } - } - - public sealed class FileVerb - { - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int LoadString(IntPtr h, int id, StringBuilder sb, int maxBuffer); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr LoadLibrary(string s); - - public static string GetPinVerb(int VerbId) - { - IntPtr hShell32 = LoadLibrary("shell32.dll"); - const int nChars = 255; - StringBuilder Buff = new StringBuilder("", nChars); - - LoadString(hShell32, VerbId, Buff, Buff.Capacity); - return Buff.ToString(); - } - } - - public sealed class IniFile - { - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetPrivateProfileString(string lpAppName, string lpKeyName, string lpDefault, StringBuilder lpReturnedString, int nSize, string lpFileName); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool WritePrivateProfileString(string lpAppName, string lpKeyName, StringBuilder lpString, string lpFileName); - - public static string GetIniValue(string section, string key, string filepath) - { - string sDefault = ""; - const int nChars = 1024; - StringBuilder Buff = new StringBuilder(nChars); - - GetPrivateProfileString(section, key, sDefault, Buff, Buff.Capacity, filepath); - return Buff.ToString(); - } - - public static void SetIniValue(string section, string key, StringBuilder value, string filepath) - { - WritePrivateProfileString(section, key, value, filepath); - } - } - - public class UiAutomation - { - public enum GetWindow_Cmd : int - { - GW_HWNDFIRST = 0, - GW_HWNDLAST = 1, - GW_HWNDNEXT = 2, - GW_HWNDPREV = 3, - GW_OWNER = 4, - GW_CHILD = 5, - GW_ENABLEDPOPUP = 6 - } - - public enum ShowWindowEnum - { - Hide = 0, - ShowNormal = 1, - ShowMinimized = 2, - ShowMaximized = 3, - Maximize = 3, - ShowNormalNoActivate = 4, - Show = 5, - Minimize = 6, - ShowMinNoActivate = 7, - ShowNoActivate = 8, - Restore = 9, - ShowDefault = 10, - ForceMinimized = 11 - } - - public enum UserNotificationState - { - // http://msdn.microsoft.com/en-us/library/bb762533(v=vs.85).aspx - ScreenSaverOrLockedOrFastUserSwitching =1, // A screen saver is displayed, the machine is locked, or a nonactive Fast User Switching session is in progress. - FullScreenOrPresentationModeOrLoginScreen =2, // A full-screen application is running or Presentation Settings are applied. Presentation Settings allow a user to put their machine into a state fit for an uninterrupted presentation, such as a set of PowerPoint slides, with a single click. Also returns this state if machine is at the login screen. - RunningDirect3DFullScreen =3, // A full-screen, exclusive mode, Direct3D application is running. - PresentationMode =4, // The user has activated Windows presentation settings to block notifications and pop-up messages. - AcceptsNotifications =5, // None of the other states are found, notifications can be freely sent. - QuietTime =6, // Introduced in Windows 7. The current user is in "quiet time", which is the first hour after a new user logs into his or her account for the first time. - WindowsStoreAppRunning =7 // Introduced in Windows 8. A Windows Store app is running. - } - - // Only for Vista or above - [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = false)] - static extern int SHQueryUserNotificationState(out UserNotificationState pquns); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumWindows(EnumWindowsProcD lpEnumFunc, ref IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetWindowTextLength(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern IntPtr GetDesktopWindow(); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern IntPtr GetShellWindow(); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool IsWindowEnabled(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool IsIconic(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool ShowWindow(IntPtr hWnd, ShowWindowEnum flags); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr SetActiveWindow(IntPtr hwnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr SetFocus(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern bool BringWindowToTop(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetCurrentThreadId(); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach); - - [DllImport("user32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); - - [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr DestroyMenu(IntPtr hWnd); - - public delegate bool EnumWindowsProcD(IntPtr hWnd, ref IntPtr lItems); - - public static bool EnumWindowsProc(IntPtr hWnd, ref IntPtr lItems) - { - if (hWnd != IntPtr.Zero) - { - GCHandle hItems = GCHandle.FromIntPtr(lItems); - List items = hItems.Target as List; - items.Add(hWnd); - return true; - } - else - { - return false; - } - } - - public static List EnumWindows() - { - try - { - List items = new List(); - EnumWindowsProcD CallBackPtr = new EnumWindowsProcD(EnumWindowsProc); - GCHandle hItems = GCHandle.Alloc(items); - IntPtr lItems = GCHandle.ToIntPtr(hItems); - EnumWindows(CallBackPtr, ref lItems); - return items; - } - catch (Exception ex) - { - throw new Exception("An error occured during window enumeration: " + ex.Message); - } - } - - public static string GetWindowText(IntPtr hWnd) - { - int iTextLength = GetWindowTextLength(hWnd); - if (iTextLength > 0) - { - StringBuilder sb = new StringBuilder(iTextLength); - GetWindowText(hWnd, sb, iTextLength + 1); - return sb.ToString(); - } - else - { - return String.Empty; - } - } - - public static bool BringWindowToFront(IntPtr windowHandle) - { - bool breturn = false; - if (IsIconic(windowHandle)) - { - // Show minimized window because SetForegroundWindow does not work for minimized windows - ShowWindow(windowHandle, ShowWindowEnum.ShowMaximized); - } - - int lpdwProcessId; - int windowThreadProcessId = GetWindowThreadProcessId(GetForegroundWindow(), out lpdwProcessId); - int currentThreadId = GetCurrentThreadId(); - AttachThreadInput(windowThreadProcessId, currentThreadId, true); - - BringWindowToTop(windowHandle); - breturn = SetForegroundWindow(windowHandle); - SetActiveWindow(windowHandle); - SetFocus(windowHandle); - - AttachThreadInput(windowThreadProcessId, currentThreadId, false); - return breturn; - } - - public static int GetWindowThreadProcessId(IntPtr windowHandle) - { - int processID = 0; - GetWindowThreadProcessId(windowHandle, out processID); - return processID; - } - - public static IntPtr GetWindowLong(IntPtr hWnd, int nIndex) - { - if (IntPtr.Size == 4) - { - return GetWindowLong32(hWnd, nIndex); - } - return GetWindowLongPtr64(hWnd, nIndex); - } - - public static string GetUserNotificationState() - { - // Only works for Windows Vista or higher - UserNotificationState state; - int returnVal = SHQueryUserNotificationState(out state); - return state.ToString(); - } - } - - public class QueryUser - { - [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern IntPtr WTSOpenServer(string pServerName); - - [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern void WTSCloseServer(IntPtr hServer); - - [DllImport("wtsapi32.dll", CharSet = CharSet.Ansi, SetLastError = false)] - public static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WTS_INFO_CLASS wtsInfoClass, out IntPtr pBuffer, out int pBytesReturned); - - [DllImport("wtsapi32.dll", CharSet = CharSet.Ansi, SetLastError = false)] - public static extern int WTSEnumerateSessions(IntPtr hServer, int Reserved, int Version, out IntPtr pSessionInfo, out int pCount); - - [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern void WTSFreeMemory(IntPtr pMemory); - - [DllImport("winsta.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int WinStationQueryInformation(IntPtr hServer, int sessionId, int information, ref WINSTATIONINFORMATIONW pBuffer, int bufferLength, ref int returnedLength); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern int GetCurrentProcessId(); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = false)] - public static extern bool ProcessIdToSessionId(int processId, ref int pSessionId); - - public class TerminalSessionData - { - public int SessionId; - public string ConnectionState; - public string SessionName; - public bool IsUserSession; - public TerminalSessionData(int sessionId, string connState, string sessionName, bool isUserSession) - { - SessionId = sessionId; - ConnectionState = connState; - SessionName = sessionName; - IsUserSession = isUserSession; - } - } - - public class TerminalSessionInfo - { - public string NTAccount; - public string SID; - public string UserName; - public string DomainName; - public int SessionId; - public string SessionName; - public string ConnectState; - public bool IsCurrentSession; - public bool IsConsoleSession; - public bool IsActiveUserSession; - public bool IsUserSession; - public bool IsRdpSession; - public bool IsLocalAdmin; - public DateTime? LogonTime; - public TimeSpan? IdleTime; - public DateTime? DisconnectTime; - public string ClientName; - public string ClientProtocolType; - public string ClientDirectory; - public int ClientBuildNumber; - } - - [StructLayout(LayoutKind.Sequential)] - private struct WTS_SESSION_INFO - { - public Int32 SessionId; - [MarshalAs(UnmanagedType.LPStr)] - public string SessionName; - public WTS_CONNECTSTATE_CLASS State; - } - - [StructLayout(LayoutKind.Sequential)] - public struct WINSTATIONINFORMATIONW - { - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 70)] - private byte[] Reserved1; - public int SessionId; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - private byte[] Reserved2; - public FILETIME ConnectTime; - public FILETIME DisconnectTime; - public FILETIME LastInputTime; - public FILETIME LoginTime; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1096)] - private byte[] Reserved3; - public FILETIME CurrentTime; - } - - public enum WINSTATIONINFOCLASS - { - WinStationInformation = 8 - } - - public enum WTS_CONNECTSTATE_CLASS - { - Active, - Connected, - ConnectQuery, - Shadow, - Disconnected, - Idle, - Listen, - Reset, - Down, - Init - } - - public enum WTS_INFO_CLASS - { - SessionId=4, - UserName, - SessionName, - DomainName, - ConnectState, - ClientBuildNumber, - ClientName, - ClientDirectory, - ClientProtocolType=16 - } - - private static IntPtr OpenServer(string Name) - { - IntPtr server = WTSOpenServer(Name); - return server; - } - - private static void CloseServer(IntPtr ServerHandle) - { - WTSCloseServer(ServerHandle); - } - - private static IList PtrToStructureList(IntPtr ppList, int count) where T : struct - { - List result = new List(); - long pointer = ppList.ToInt64(); - int sizeOf = Marshal.SizeOf(typeof(T)); - - for (int index = 0; index < count; index++) - { - T item = (T) Marshal.PtrToStructure(new IntPtr(pointer), typeof(T)); - result.Add(item); - pointer += sizeOf; - } - return result; - } - - public static DateTime? FileTimeToDateTime(FILETIME ft) - { - if (ft.dwHighDateTime == 0 && ft.dwLowDateTime == 0) - { - return null; - } - long hFT = (((long) ft.dwHighDateTime) << 32) + ft.dwLowDateTime; - return DateTime.FromFileTime(hFT); - } - - public static WINSTATIONINFORMATIONW GetWinStationInformation(IntPtr server, int sessionId) - { - int retLen = 0; - WINSTATIONINFORMATIONW wsInfo = new WINSTATIONINFORMATIONW(); - WinStationQueryInformation(server, sessionId, (int) WINSTATIONINFOCLASS.WinStationInformation, ref wsInfo, Marshal.SizeOf(typeof(WINSTATIONINFORMATIONW)), ref retLen); - return wsInfo; - } - - public static TerminalSessionData[] ListSessions(string ServerName) - { - IntPtr server = IntPtr.Zero; - if (ServerName == "localhost" || ServerName == String.Empty) - { - ServerName = Environment.MachineName; - } - - List results = new List(); - - try - { - server = OpenServer(ServerName); - IntPtr ppSessionInfo = IntPtr.Zero; - int count; - bool _isUserSession = false; - IList sessionsInfo; - - if (WTSEnumerateSessions(server, 0, 1, out ppSessionInfo, out count) == 0) - { - throw new Win32Exception(); - } - - try - { - sessionsInfo = PtrToStructureList(ppSessionInfo, count); - } - finally - { - WTSFreeMemory(ppSessionInfo); - } - - foreach (WTS_SESSION_INFO sessionInfo in sessionsInfo) - { - if (sessionInfo.SessionName != "Services" && sessionInfo.SessionName != "RDP-Tcp") - { - _isUserSession = true; - } - results.Add(new TerminalSessionData(sessionInfo.SessionId, sessionInfo.State.ToString(), sessionInfo.SessionName, _isUserSession)); - _isUserSession = false; - } - } - finally - { - CloseServer(server); - } - - TerminalSessionData[] returnData = results.ToArray(); - return returnData; - } - - public static TerminalSessionInfo GetSessionInfo(string ServerName, int SessionId) - { - IntPtr server = IntPtr.Zero; - IntPtr buffer = IntPtr.Zero; - int bytesReturned; - TerminalSessionInfo data = new TerminalSessionInfo(); - bool _IsCurrentSessionId = false; - bool _IsConsoleSession = false; - bool _IsUserSession = false; - int currentSessionID = 0; - string _NTAccount = String.Empty; - if (ServerName == "localhost" || ServerName == String.Empty) - { - ServerName = Environment.MachineName; - } - if (ProcessIdToSessionId(GetCurrentProcessId(), ref currentSessionID) == false) - { - currentSessionID = -1; - } - - // Get all members of the local administrators group - bool _IsLocalAdminCheckSuccess = false; - List localAdminGroupSidsList = new List(); - try - { - DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + ServerName + ",Computer"); - string localAdminGroupName = new SecurityIdentifier("S-1-5-32-544").Translate(typeof(NTAccount)).Value.Split('\\')[1]; - DirectoryEntry admGroup = localMachine.Children.Find(localAdminGroupName, "group"); - object members = admGroup.Invoke("members", null); - string validSidPattern = @"^S-\d-\d+-(\d+-){1,14}\d+$"; - foreach (object groupMember in (IEnumerable)members) - { - DirectoryEntry member = new DirectoryEntry(groupMember); - if (member.Name != String.Empty) - { - if (Regex.IsMatch(member.Name, validSidPattern)) - { - localAdminGroupSidsList.Add(member.Name); - } - else - { - localAdminGroupSidsList.Add((new NTAccount(member.Name)).Translate(typeof(SecurityIdentifier)).Value); - } - } - } - _IsLocalAdminCheckSuccess = true; - } - catch { } - - try - { - server = OpenServer(ServerName); - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.ClientBuildNumber, out buffer, out bytesReturned) == false) - { - return data; - } - int lData = Marshal.ReadInt32(buffer); - data.ClientBuildNumber = lData; - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.ClientDirectory, out buffer, out bytesReturned) == false) - { - return data; - } - string strData = Marshal.PtrToStringAnsi(buffer); - data.ClientDirectory = strData; - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.ClientName, out buffer, out bytesReturned) == false) - { - return data; - } - strData = Marshal.PtrToStringAnsi(buffer); - data.ClientName = strData; - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.ClientProtocolType, out buffer, out bytesReturned) == false) - { - return data; - } - Int16 intData = Marshal.ReadInt16(buffer); - if (intData == 2) - { - strData = "RDP"; - data.IsRdpSession = true; - } - else - { - strData = ""; - data.IsRdpSession = false; - } - data.ClientProtocolType = strData; - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.ConnectState, out buffer, out bytesReturned) == false) - { - return data; - } - lData = Marshal.ReadInt32(buffer); - data.ConnectState = ((WTS_CONNECTSTATE_CLASS) lData).ToString(); - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.SessionId, out buffer, out bytesReturned) == false) - { - return data; - } - lData = Marshal.ReadInt32(buffer); - data.SessionId = lData; - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.DomainName, out buffer, out bytesReturned) == false) - { - return data; - } - strData = Marshal.PtrToStringAnsi(buffer).ToUpper(); - data.DomainName = strData; - if (strData != String.Empty) - { - _NTAccount = strData; - } - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.UserName, out buffer, out bytesReturned) == false) - { - return data; - } - strData = Marshal.PtrToStringAnsi(buffer); - data.UserName = strData; - if (strData != String.Empty) - { - data.NTAccount = _NTAccount + "\\" + strData; - string _Sid = (new NTAccount(_NTAccount + "\\" + strData)).Translate(typeof(SecurityIdentifier)).Value; - data.SID = _Sid; - if (_IsLocalAdminCheckSuccess == true) - { - foreach (string localAdminGroupSid in localAdminGroupSidsList) - { - if (localAdminGroupSid == _Sid) - { - data.IsLocalAdmin = true; - break; - } - else - { - data.IsLocalAdmin = false; - } - } - } - } - - if (WTSQuerySessionInformation(server, SessionId, WTS_INFO_CLASS.SessionName, out buffer, out bytesReturned) == false) - { - return data; - } - strData = Marshal.PtrToStringAnsi(buffer); - data.SessionName = strData; - if (strData != "Services" && strData != "RDP-Tcp" && data.UserName != String.Empty) - { - _IsUserSession = true; - } - data.IsUserSession = _IsUserSession; - if (strData == "Console") - { - _IsConsoleSession = true; - } - data.IsConsoleSession = _IsConsoleSession; - - WINSTATIONINFORMATIONW wsInfo = GetWinStationInformation(server, SessionId); - DateTime? _loginTime = FileTimeToDateTime(wsInfo.LoginTime); - DateTime? _lastInputTime = FileTimeToDateTime(wsInfo.LastInputTime); - DateTime? _disconnectTime = FileTimeToDateTime(wsInfo.DisconnectTime); - DateTime? _currentTime = FileTimeToDateTime(wsInfo.CurrentTime); - TimeSpan? _idleTime = (_currentTime != null && _lastInputTime != null) ? _currentTime.Value - _lastInputTime.Value : TimeSpan.Zero; - data.LogonTime = _loginTime; - data.IdleTime = _idleTime; - data.DisconnectTime = _disconnectTime; - - if (currentSessionID == SessionId) - { - _IsCurrentSessionId = true; - } - data.IsCurrentSession = _IsCurrentSessionId; - } - finally - { - WTSFreeMemory(buffer); - buffer = IntPtr.Zero; - CloseServer(server); - } - return data; - } - - public static TerminalSessionInfo[] GetUserSessionInfo(string ServerName) - { - if (ServerName == "localhost" || ServerName == String.Empty) - { - ServerName = Environment.MachineName; - } - - // Find and get detailed information for all user sessions - // Also determine the active user session. If a console user exists, then that will be the active user session. - // If no console user exists but users are logged in, such as on terminal servers, then select the first logged-in non-console user that is either 'Active' or 'Connected' as the active user. - TerminalSessionData[] sessions = ListSessions(ServerName); - TerminalSessionInfo sessionInfo = new TerminalSessionInfo(); - List userSessionsInfo = new List(); - string firstActiveUserNTAccount = String.Empty; - bool IsActiveUserSessionSet = false; - foreach (TerminalSessionData session in sessions) - { - if (session.IsUserSession == true) - { - sessionInfo = GetSessionInfo(ServerName, session.SessionId); - if (sessionInfo.IsUserSession == true) - { - if ((firstActiveUserNTAccount == String.Empty) && (sessionInfo.ConnectState == "Active" || sessionInfo.ConnectState == "Connected")) - { - firstActiveUserNTAccount = sessionInfo.NTAccount; - } - - if (sessionInfo.IsConsoleSession == true) - { - sessionInfo.IsActiveUserSession = true; - IsActiveUserSessionSet = true; - } - else - { - sessionInfo.IsActiveUserSession = false; - } - - userSessionsInfo.Add(sessionInfo); - } - } - } - - TerminalSessionInfo[] userSessions = userSessionsInfo.ToArray(); - if (IsActiveUserSessionSet == false) - { - foreach (TerminalSessionInfo userSession in userSessions) - { - if (userSession.NTAccount == firstActiveUserNTAccount) - { - userSession.IsActiveUserSession = true; - break; - } - } - } - - return userSessions; - } - } + public class Msi + { + enum LoadLibraryFlags : int + { + DONT_RESOLVE_DLL_REFERENCES = 0x00000001, + LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, + LOAD_LIBRARY_AS_DATAFILE = 0x00000002, + LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, + LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, + LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 + } + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + static extern IntPtr + LoadLibraryEx( + string lpFileName, + IntPtr hFile, + LoadLibraryFlags dwFlags + ); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + static extern int + LoadString( + IntPtr hInstance, + int uID, + StringBuilder lpBuffer, + int nBufferMax + ); + + // Get MSI exit code message from msimsg.dll resource dll + public static string GetMessageFromMsiExitCode(int errCode) + { + IntPtr hModuleInstance = + LoadLibraryEx("msimsg.dll", + IntPtr.Zero, + LoadLibraryFlags.LOAD_LIBRARY_AS_DATAFILE); + + StringBuilder sb = new StringBuilder(255); + LoadString(hModuleInstance, errCode, sb, sb.Capacity + 1); + + return sb.ToString(); + } + } + + public class Explorer + { + private static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); + + private const int WM_SETTINGCHANGE = 0x1a; + + private const int SMTO_ABORTIFHUNG = 0x0002; + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + static extern bool + SendNotifyMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern IntPtr + SendMessageTimeout( + IntPtr hWnd, + int Msg, + IntPtr wParam, + string lParam, + int fuFlags, + int uTimeout, + IntPtr lpdwResult + ); + + [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern int + SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); + + public static void RefreshDesktopAndEnvironmentVariables() + { + // Update desktop icons + SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero); + SendMessageTimeout(HWND_BROADCAST, + WM_SETTINGCHANGE, + IntPtr.Zero, + null, + SMTO_ABORTIFHUNG, + 100, + IntPtr.Zero); + + // Update environment variables + SendMessageTimeout(HWND_BROADCAST, + WM_SETTINGCHANGE, + IntPtr.Zero, + "Environment", + SMTO_ABORTIFHUNG, + 100, + IntPtr.Zero); + } + } + + public sealed class FileVerb + { + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern int + LoadString(IntPtr h, int id, StringBuilder sb, int maxBuffer); + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern IntPtr LoadLibrary(string s); + + public static string GetPinVerb(int VerbId) + { + IntPtr hShell32 = LoadLibrary("shell32.dll"); + const int nChars = 255; + StringBuilder Buff = new StringBuilder("", nChars); + + LoadString(hShell32, VerbId, Buff, Buff.Capacity); + return Buff.ToString(); + } + } + + public sealed class IniFile + { + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern int + GetPrivateProfileString( + string lpAppName, + string lpKeyName, + string lpDefault, + StringBuilder lpReturnedString, + int nSize, + string lpFileName + ); + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool + WritePrivateProfileString( + string lpAppName, + string lpKeyName, + StringBuilder lpString, + string lpFileName + ); + + public static string + GetIniValue(string section, string key, string filepath) + { + string sDefault = ""; + const int nChars = 1024; + StringBuilder Buff = new StringBuilder(nChars); + + GetPrivateProfileString(section, + key, + sDefault, + Buff, + Buff.Capacity, + filepath); + return Buff.ToString(); + } + + public static void SetIniValue( + string section, + string key, + StringBuilder value, + string filepath + ) + { + WritePrivateProfileString (section, key, value, filepath); + } + } + + public class UiAutomation + { + public enum GetWindow_Cmd : int + { + GW_HWNDFIRST = 0, + GW_HWNDLAST = 1, + GW_HWNDNEXT = 2, + GW_HWNDPREV = 3, + GW_OWNER = 4, + GW_CHILD = 5, + GW_ENABLEDPOPUP = 6 + } + + public enum ShowWindowEnum + { + Hide = 0, + ShowNormal = 1, + ShowMinimized = 2, + ShowMaximized = 3, + Maximize = 3, + ShowNormalNoActivate = 4, + Show = 5, + Minimize = 6, + ShowMinNoActivate = 7, + ShowNoActivate = 8, + Restore = 9, + ShowDefault = 10, + ForceMinimized = 11 + } + + public enum UserNotificationState + { + // http://msdn.microsoft.com/en-us/library/bb762533(v=vs.85).aspx + ScreenSaverOrLockedOrFastUserSwitching = 1, // A screen saver is displayed, the machine is locked, or a nonactive Fast User Switching session is in progress. + FullScreenOrPresentationModeOrLoginScreen = 2, // A full-screen application is running or Presentation Settings are applied. Presentation Settings allow a user to put their machine into a state fit for an uninterrupted presentation, such as a set of PowerPoint slides, with a single click. Also returns this state if machine is at the login screen. + RunningDirect3DFullScreen = 3, // A full-screen, exclusive mode, Direct3D application is running. + PresentationMode = 4, // The user has activated Windows presentation settings to block notifications and pop-up messages. + AcceptsNotifications = 5, // None of the other states are found, notifications can be freely sent. + QuietTime = 6, // Introduced in Windows 7. The current user is in "quiet time", which is the first hour after a new user logs into his or her account for the first time. + WindowsStoreAppRunning = 7 // Introduced in Windows 8. A Windows Store app is running. + } + + // Only for Vista or above + [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = false)] + static extern int + SHQueryUserNotificationState(out UserNotificationState pquns); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool + EnumWindows(EnumWindowsProcD lpEnumFunc, ref IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern int + GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindowEnabled(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsIconic(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ShowWindow(IntPtr hWnd, ShowWindowEnum flags); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern IntPtr SetActiveWindow(IntPtr hwnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern IntPtr SetFocus(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern bool BringWindowToTop(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern int + GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern int GetCurrentThreadId(); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern bool + AttachThreadInput(int idAttach, int idAttachTo, bool fAttach); + + [ + DllImport( + "user32.dll", + EntryPoint = "GetWindowLong", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex); + + [ + DllImport( + "user32.dll", + EntryPoint = "GetWindowLongPtr", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern bool + EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern IntPtr DestroyMenu(IntPtr hWnd); + + public delegate bool EnumWindowsProcD(IntPtr hWnd, ref IntPtr lItems); + + public static bool EnumWindowsProc(IntPtr hWnd, ref IntPtr lItems) + { + if (hWnd != IntPtr.Zero) + { + GCHandle hItems = GCHandle.FromIntPtr(lItems); + List items = hItems.Target as List; + items.Add (hWnd); + return true; + } + else + { + return false; + } + } + + public static List EnumWindows() + { + try + { + List items = new List(); + EnumWindowsProcD CallBackPtr = + new EnumWindowsProcD(EnumWindowsProc); + GCHandle hItems = GCHandle.Alloc(items); + IntPtr lItems = GCHandle.ToIntPtr(hItems); + EnumWindows(CallBackPtr, ref lItems); + return items; + } + catch (Exception ex) + { + throw new Exception("An error occured during window enumeration: " + + ex.Message); + } + } + + public static string GetWindowText(IntPtr hWnd) + { + int iTextLength = GetWindowTextLength(hWnd); + if (iTextLength > 0) + { + StringBuilder sb = new StringBuilder(iTextLength); + GetWindowText(hWnd, sb, iTextLength + 1); + return sb.ToString(); + } + else + { + return String.Empty; + } + } + + public static bool BringWindowToFront(IntPtr windowHandle) + { + bool breturn = false; + if (IsIconic(windowHandle)) + { + // Show minimized window because SetForegroundWindow does not work for minimized windows + ShowWindow(windowHandle, ShowWindowEnum.ShowMaximized); + } + + int lpdwProcessId; + int windowThreadProcessId = + GetWindowThreadProcessId(GetForegroundWindow(), + out lpdwProcessId); + int currentThreadId = GetCurrentThreadId(); + AttachThreadInput(windowThreadProcessId, currentThreadId, true); + + BringWindowToTop (windowHandle); + breturn = SetForegroundWindow(windowHandle); + SetActiveWindow (windowHandle); + SetFocus (windowHandle); + + AttachThreadInput(windowThreadProcessId, currentThreadId, false); + return breturn; + } + + public static int GetWindowThreadProcessId(IntPtr windowHandle) + { + int processID = 0; + GetWindowThreadProcessId(windowHandle, out processID); + return processID; + } + + public static IntPtr GetWindowLong(IntPtr hWnd, int nIndex) + { + if (IntPtr.Size == 4) + { + return GetWindowLong32(hWnd, nIndex); + } + return GetWindowLongPtr64(hWnd, nIndex); + } + + public static string GetUserNotificationState() + { + // Only works for Windows Vista or higher + UserNotificationState state; + int returnVal = SHQueryUserNotificationState(out state); + return state.ToString(); + } + } + + public class QueryUser + { + [ + DllImport( + "wtsapi32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern IntPtr WTSOpenServer(string pServerName); + + [ + DllImport( + "wtsapi32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern void WTSCloseServer(IntPtr hServer); + + [ + DllImport( + "wtsapi32.dll", + CharSet = CharSet.Ansi, + SetLastError = false) + ] + public static extern bool + WTSQuerySessionInformation( + IntPtr hServer, + int sessionId, + WTS_INFO_CLASS wtsInfoClass, + out IntPtr pBuffer, + out int pBytesReturned + ); + + [ + DllImport( + "wtsapi32.dll", + CharSet = CharSet.Ansi, + SetLastError = false) + ] + public static extern int + WTSEnumerateSessions( + IntPtr hServer, + int Reserved, + int Version, + out IntPtr pSessionInfo, + out int pCount + ); + + [ + DllImport( + "wtsapi32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern void WTSFreeMemory(IntPtr pMemory); + + [DllImport("winsta.dll", CharSet = CharSet.Auto, SetLastError = false)] + public static extern int + WinStationQueryInformation( + IntPtr hServer, + int sessionId, + int information, + ref WINSTATIONINFORMATIONW pBuffer, + int bufferLength, + ref int returnedLength + ); + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern int GetCurrentProcessId(); + + [ + DllImport( + "kernel32.dll", + CharSet = CharSet.Auto, + SetLastError = false) + ] + public static extern bool + ProcessIdToSessionId(int processId, ref int pSessionId); + + public class TerminalSessionData + { + public int SessionId; + + public string ConnectionState; + + public string SessionName; + + public bool IsUserSession; + + public TerminalSessionData( + int sessionId, + string connState, + string sessionName, + bool isUserSession + ) + { + SessionId = sessionId; + ConnectionState = connState; + SessionName = sessionName; + IsUserSession = isUserSession; + } + } + + public class TerminalSessionInfo + { + public string NTAccount; + + public string SID; + + public string UserName; + + public string DomainName; + + public int SessionId; + + public string SessionName; + + public string ConnectState; + + public bool IsCurrentSession; + + public bool IsConsoleSession; + + public bool IsActiveUserSession; + + public bool IsUserSession; + + public bool IsRdpSession; + + public bool IsLocalAdmin; + + public DateTime? LogonTime; + + public TimeSpan? IdleTime; + + public DateTime? DisconnectTime; + + public string ClientName; + + public string ClientProtocolType; + + public string ClientDirectory; + + public int ClientBuildNumber; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public Int32 SessionId; + + [MarshalAs(UnmanagedType.LPStr)] + public string SessionName; + + public WTS_CONNECTSTATE_CLASS State; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WINSTATIONINFORMATIONW + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 70)] + private byte[] Reserved1; + + public int SessionId; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + private byte[] Reserved2; + + public FILETIME ConnectTime; + + public FILETIME DisconnectTime; + + public FILETIME LastInputTime; + + public FILETIME LoginTime; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1096)] + private byte[] Reserved3; + + public FILETIME CurrentTime; + } + + public enum WINSTATIONINFOCLASS + { + WinStationInformation = 8 + } + + public enum WTS_CONNECTSTATE_CLASS + { + Active, + Connected, + ConnectQuery, + Shadow, + Disconnected, + Idle, + Listen, + Reset, + Down, + Init + } + + public enum WTS_INFO_CLASS + { + SessionId = 4, + UserName, + SessionName, + DomainName, + ConnectState, + ClientBuildNumber, + ClientName, + ClientDirectory, + ClientProtocolType = 16 + } + + private static IntPtr OpenServer(string Name) + { + IntPtr server = WTSOpenServer(Name); + return server; + } + + private static void CloseServer(IntPtr ServerHandle) + { + WTSCloseServer (ServerHandle); + } + + private static IList PtrToStructureList(IntPtr ppList, int count) + where T : struct + { + List result = new List(); + long pointer = ppList.ToInt64(); + int sizeOf = Marshal.SizeOf(typeof (T)); + + for (int index = 0; index < count; index++) + { + T item = + (T) Marshal.PtrToStructure(new IntPtr(pointer), typeof (T)); + result.Add (item); + pointer += sizeOf; + } + return result; + } + + public static DateTime? FileTimeToDateTime(FILETIME ft) + { + if (ft.dwHighDateTime == 0 && ft.dwLowDateTime == 0) + { + return null; + } + long hFT = (((long) ft.dwHighDateTime) << 32) + ft.dwLowDateTime; + return DateTime.FromFileTime(hFT); + } + + public static WINSTATIONINFORMATIONW + GetWinStationInformation(IntPtr server, int sessionId) + { + int retLen = 0; + WINSTATIONINFORMATIONW wsInfo = new WINSTATIONINFORMATIONW(); + WinStationQueryInformation(server, + sessionId, + (int) WINSTATIONINFOCLASS.WinStationInformation, + ref wsInfo, + Marshal.SizeOf(typeof (WINSTATIONINFORMATIONW)), + ref retLen); + return wsInfo; + } + + public static TerminalSessionData[] ListSessions(string ServerName) + { + IntPtr server = IntPtr.Zero; + if (ServerName == "localhost" || ServerName == String.Empty) + { + ServerName = Environment.MachineName; + } + + List results = new List(); + + try + { + server = OpenServer(ServerName); + IntPtr ppSessionInfo = IntPtr.Zero; + int count; + bool _isUserSession = false; + IList sessionsInfo; + + if ( + WTSEnumerateSessions(server, + 0, + 1, + out ppSessionInfo, + out count) == + 0 + ) + { + throw new Win32Exception(); + } + + try + { + sessionsInfo = + PtrToStructureList(ppSessionInfo, + count); + } + finally + { + WTSFreeMemory (ppSessionInfo); + } + + foreach (WTS_SESSION_INFO sessionInfo in sessionsInfo) + { + if ( + sessionInfo.SessionName != "Services" && + sessionInfo.SessionName != "RDP-Tcp" + ) + { + _isUserSession = true; + } + results + .Add(new TerminalSessionData(sessionInfo.SessionId, + sessionInfo.State.ToString(), + sessionInfo.SessionName, + _isUserSession)); + _isUserSession = false; + } + } + finally + { + CloseServer (server); + } + + TerminalSessionData[] returnData = results.ToArray(); + return returnData; + } + + public static TerminalSessionInfo + GetSessionInfo(string ServerName, int SessionId) + { + IntPtr server = IntPtr.Zero; + IntPtr buffer = IntPtr.Zero; + int bytesReturned; + TerminalSessionInfo data = new TerminalSessionInfo(); + bool _IsCurrentSessionId = false; + bool _IsConsoleSession = false; + bool _IsUserSession = false; + int currentSessionID = 0; + string _NTAccount = String.Empty; + if (ServerName == "localhost" || ServerName == String.Empty) + { + ServerName = Environment.MachineName; + } + if ( + ProcessIdToSessionId(GetCurrentProcessId(), + ref currentSessionID) == + false + ) + { + currentSessionID = -1; + } + + // Get all members of the local administrators group + bool _IsLocalAdminCheckSuccess = false; + List localAdminGroupSidsList = new List(); + try + { + DirectoryEntry localMachine = + new DirectoryEntry("WinNT://" + ServerName + ",Computer"); + string localAdminGroupName = + new SecurityIdentifier("S-1-5-32-544") + .Translate(typeof (NTAccount)) + .Value + .Split('\\')[1]; + DirectoryEntry admGroup = + localMachine.Children.Find(localAdminGroupName, "group"); + object members = admGroup.Invoke("members", null); + string validSidPattern = @"^S-\d-\d+-(\d+-){1,14}\d+$"; + foreach (object groupMember in (IEnumerable) members) + { + DirectoryEntry member = new DirectoryEntry(groupMember); + if (member.Name != String.Empty) + { + if (Regex.IsMatch(member.Name, validSidPattern)) + { + localAdminGroupSidsList.Add(member.Name); + } + else + { + localAdminGroupSidsList + .Add((new NTAccount(member.Name)) + .Translate(typeof (SecurityIdentifier)) + .Value); + } + } + } + _IsLocalAdminCheckSuccess = true; + } + catch + { + } + + try + { + server = OpenServer(ServerName); + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.ClientBuildNumber, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + int lData = Marshal.ReadInt32(buffer); + data.ClientBuildNumber = lData; + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.ClientDirectory, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + string strData = Marshal.PtrToStringAnsi(buffer); + data.ClientDirectory = strData; + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.ClientName, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + strData = Marshal.PtrToStringAnsi(buffer); + data.ClientName = strData; + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.ClientProtocolType, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + Int16 intData = Marshal.ReadInt16(buffer); + if (intData == 2) + { + strData = "RDP"; + data.IsRdpSession = true; + } + else + { + strData = ""; + data.IsRdpSession = false; + } + data.ClientProtocolType = strData; + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.ConnectState, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + lData = Marshal.ReadInt32(buffer); + data.ConnectState = ((WTS_CONNECTSTATE_CLASS) lData).ToString(); + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.SessionId, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + lData = Marshal.ReadInt32(buffer); + data.SessionId = lData; + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.DomainName, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + strData = Marshal.PtrToStringAnsi(buffer).ToUpper(); + data.DomainName = strData; + if (strData != String.Empty) + { + _NTAccount = strData; + } + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.UserName, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + strData = Marshal.PtrToStringAnsi(buffer); + data.UserName = strData; + if (strData != String.Empty) + { + data.NTAccount = _NTAccount + "\\" + strData; + string _Sid = + (new NTAccount(_NTAccount + "\\" + strData)) + .Translate(typeof (SecurityIdentifier)) + .Value; + data.SID = _Sid; + if (_IsLocalAdminCheckSuccess == true) + { + foreach (string + localAdminGroupSid + in + localAdminGroupSidsList + ) + { + if (localAdminGroupSid == _Sid) + { + data.IsLocalAdmin = true; + break; + } + else + { + data.IsLocalAdmin = false; + } + } + } + } + + if ( + WTSQuerySessionInformation(server, + SessionId, + WTS_INFO_CLASS.SessionName, + out buffer, + out bytesReturned) == + false + ) + { + return data; + } + strData = Marshal.PtrToStringAnsi(buffer); + data.SessionName = strData; + if ( + strData != "Services" && + strData != "RDP-Tcp" && + data.UserName != String.Empty + ) + { + _IsUserSession = true; + } + data.IsUserSession = _IsUserSession; + if (strData == "Console") + { + _IsConsoleSession = true; + } + data.IsConsoleSession = _IsConsoleSession; + + WINSTATIONINFORMATIONW wsInfo = + GetWinStationInformation(server, SessionId); + DateTime? _loginTime = FileTimeToDateTime(wsInfo.LoginTime); + DateTime? _lastInputTime = + FileTimeToDateTime(wsInfo.LastInputTime); + DateTime? _disconnectTime = + FileTimeToDateTime(wsInfo.DisconnectTime); + DateTime? _currentTime = FileTimeToDateTime(wsInfo.CurrentTime); + TimeSpan? _idleTime = + (_currentTime != null && _lastInputTime != null) + ? _currentTime.Value - _lastInputTime.Value + : TimeSpan.Zero; + data.LogonTime = _loginTime; + data.IdleTime = _idleTime; + data.DisconnectTime = _disconnectTime; + + if (currentSessionID == SessionId) + { + _IsCurrentSessionId = true; + } + data.IsCurrentSession = _IsCurrentSessionId; + } + finally + { + WTSFreeMemory (buffer); + buffer = IntPtr.Zero; + CloseServer (server); + } + return data; + } + + public static TerminalSessionInfo[] + GetUserSessionInfo(string ServerName) + { + if (ServerName == "localhost" || ServerName == String.Empty) + { + ServerName = Environment.MachineName; + } + + // Find and get detailed information for all user sessions + // Also determine the active user session. If a console user exists, then that will be the active user session. + // If no console user exists but users are logged in, such as on terminal servers, then select the first logged-in non-console user that is either 'Active' or 'Connected' as the active user. + TerminalSessionData[] sessions = ListSessions(ServerName); + TerminalSessionInfo sessionInfo = new TerminalSessionInfo(); + List userSessionsInfo = + new List(); + string firstActiveUserNTAccount = String.Empty; + bool IsActiveUserSessionSet = false; + foreach (TerminalSessionData session in sessions) + { + if (session.IsUserSession == true) + { + sessionInfo = GetSessionInfo(ServerName, session.SessionId); + if (sessionInfo.IsUserSession == true) + { + if ( + (firstActiveUserNTAccount == String.Empty) && + ( + sessionInfo.ConnectState == "Active" || + sessionInfo.ConnectState == "Connected" + ) + ) + { + firstActiveUserNTAccount = sessionInfo.NTAccount; + } + + if (sessionInfo.IsConsoleSession == true) + { + sessionInfo.IsActiveUserSession = true; + IsActiveUserSessionSet = true; + } + else + { + sessionInfo.IsActiveUserSession = false; + } + + userSessionsInfo.Add (sessionInfo); + } + } + } + + TerminalSessionInfo[] userSessions = userSessionsInfo.ToArray(); + if (IsActiveUserSessionSet == false) + { + foreach (TerminalSessionInfo userSession in userSessions) + { + if (userSession.NTAccount == firstActiveUserNTAccount) + { + userSession.IsActiveUserSession = true; + break; + } + } + } + + return userSessions; + } + } } diff --git a/AppDeployToolkit/AppDeployToolkitMain.ps1 b/AppDeployToolkit/AppDeployToolkitMain.ps1 index a13aa36..3d9e95b 100644 --- a/AppDeployToolkit/AppDeployToolkitMain.ps1 +++ b/AppDeployToolkit/AppDeployToolkitMain.ps1 @@ -1,63 +1,63 @@ <# .SYNOPSIS - This script contains the functions and logic engine for the Deploy-Application.ps1 script. - # LICENSE # - PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. - Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. - 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 . + This script contains the functions and logic engine for the Deploy-Application.ps1 script. + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + 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 . .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. + 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. .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. + 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. + 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. + 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. + 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. + 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. + This parameter is passed to the script when it is being called externally, e.g. from a scheduled task or asynchronously. .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. + 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 - http://psappdeploytoolkit.com + http://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, - [int32]$CountdownNoHideSeconds, - [switch]$NoCountdown = $false, - [switch]$AsyncToolkitLaunch = $false + ## 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, + [int32]$CountdownNoHideSeconds, + [switch]$NoCountdown = $false, + [switch]$AsyncToolkitLaunch = $false ) ##*============================================= @@ -70,9 +70,9 @@ Param ( [string]$appDeployMainScriptFriendlyName = 'App Deploy Toolkit Main' ## Variables: Script Info -[version]$appDeployMainScriptVersion = [version]'3.8.2' -[version]$appDeployMainScriptMinimumConfigVersion = [version]'3.8.2' -[string]$appDeployMainScriptDate = '08/05/2020' +[version]$appDeployMainScriptVersion = [version]'3.8.3' +[version]$appDeployMainScriptMinimumConfigVersion = [version]'3.8.3' +[string]$appDeployMainScriptDate = '30/09/2020' [hashtable]$appDeployMainScriptParameters = $PSBoundParameters ## Variables: Datetime and Culture @@ -87,12 +87,10 @@ Param ( ## Variables: Environment Variables [psobject]$envHost = $Host -[psobject]$envShellFolders = Get-ItemProperty -Path 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction 'SilentlyContinue' +[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]$envCommonProgramFiles = [Environment]::GetFolderPath('CommonProgramFiles') -[string]$envCommonProgramFilesX86 = ${env:CommonProgramFiles(x86)} [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' @@ -100,14 +98,11 @@ Param ( [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]$envComputerNameFQDN = ([Net.Dns]::GetHostEntry('localhost')).HostName [string]$envHomeDrive = $env:HOMEDRIVE [string]$envHomePath = $env:HOMEPATH [string]$envHomeShare = $env:HOMESHARE [string]$envLocalAppData = [Environment]::GetFolderPath('LocalApplicationData') [string[]]$envLogicalDrives = [Environment]::GetLogicalDrives() -[string]$envProgramFiles = [Environment]::GetFolderPath('ProgramFiles') -[string]$envProgramFilesX86 = ${env:ProgramFiles(x86)} [string]$envProgramData = [Environment]::GetFolderPath('CommonApplicationData') [string]$envPublic = $env:PUBLIC [string]$envSystemDrive = $env:SYSTEMDRIVE @@ -129,9 +124,6 @@ Param ( [string]$envUserTemplates = [Environment]::GetFolderPath('Templates') [string]$envSystem32Directory = [Environment]::SystemDirectory [string]$envWinDir = $env:WINDIR -# Handle X86 environment variables so they are never empty -If (-not $envCommonProgramFilesX86) { [string]$envCommonProgramFilesX86 = $envCommonProgramFiles } -If (-not $envProgramFilesX86) { [string]$envProgramFilesX86 = $envProgramFiles } ## Variables: Domain Membership [boolean]$IsMachinePartOfDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'SilentlyContinue').PartOfDomain @@ -139,23 +131,39 @@ If (-not $envProgramFilesX86) { [string]$envProgramFilesX86 = $envProgramFiles } [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 { - [string]$envLogonServer = $env:LOGONSERVER | Where-Object { (($_) -and (-not $_.Contains('\\MicrosoftAccount'))) } | ForEach-Object { $_.TrimStart('\') } | ForEach-Object { ([Net.Dns]::GetHostEntry($_)).HostName } - # If running in system context, fall back on the logonserver value stored in the registry - If (-not $envLogonServer) { [string]$envLogonServer = Get-ItemProperty -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty 'DCName' -ErrorAction 'SilentlyContinue' } - [string]$MachineDomainController = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name - } - Catch { } + [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 } + # If running in system context, 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' } + } + Catch { + # If GetHostEntry fails, just use the registry value + [string]$envLogonServer = Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty 'DCName' -ErrorAction 'SilentlyContinue' + } + + 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]$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() + [string]$envUserDomain = [Environment]::UserDomainName.ToUpper() } Catch { } @@ -167,20 +175,24 @@ Catch { } [string]$envOSVersionMajor = $envOSVersion.Major [string]$envOSVersionMinor = $envOSVersion.Minor [string]$envOSVersionBuild = $envOSVersion.Build -If ($envOSVersionMajor -eq 10) {$envOSVersionRevision = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'UBR' -ErrorAction SilentlyContinue).UBR} -Else { [string]$envOSVersionRevision = ,((Get-ItemProperty -Path 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name 'BuildLabEx' -ErrorAction 'SilentlyContinue').BuildLabEx -split '\.') | ForEach-Object { $_[1] } } +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 { "$($envOSVersion.ToString())" } +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) Switch ($envOSProductType) { - 3 { [string]$envOSProductTypeName = 'Server' } - 2 { [string]$envOSProductTypeName = 'Domain Controller' } - 1 { [string]$envOSProductTypeName = 'Workstation' } - Default { [string]$envOSProductTypeName = 'Unknown' } + 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) @@ -190,6 +202,37 @@ If ($Is64Bit) { [string]$envOSArchitecture = '64-bit' } Else { [string]$envOSArc [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: Hardware [int32]$envSystemRAM = Get-WMIObject -Class Win32_PhysicalMemory -ComputerName $env:COMPUTERNAME -ErrorAction 'SilentlyContinue' | Measure-Object -Property Capacity -Sum -ErrorAction SilentlyContinue | ForEach-Object {[Math]::Round(($_.sum / 1GB),2)} @@ -233,12 +276,12 @@ If ($IsLocalSystemAccount -or $IsLocalServiceAccount -or $IsNetworkServiceAccoun [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 + # 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 + # 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 @@ -251,7 +294,7 @@ If (-not (Test-Path -LiteralPath $appDeployCustomTypesSourceCode -PathType 'Leaf [string]$appDeployToolkitDotSourceExtensions = 'AppDeployToolkitExtensions.ps1' ## Import variables from XML configuration file -[Xml.XmlDocument]$xmlConfigFile = Get-Content -LiteralPath $AppDeployConfigFile +[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 @@ -276,7 +319,7 @@ Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' [Int32]$appDeployLogoBannerHeight = $appDeployLogoBannerObject.Height if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { - $appDeployLogoBannerHeight = $appDeployLogoBannerMaxHeight + $appDeployLogoBannerHeight = $appDeployLogoBannerMaxHeight } [Int32]$appDeployLogoBannerHeightDifference = $appDeployLogoBannerHeight - $appDeployLogoBannerBaseHeight @@ -299,6 +342,21 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { [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 @@ -313,103 +371,103 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { [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 'HKLM:SOFTWARE\Policies\Microsoft\MUI\Settings' -Value 'PreferredUILanguages' - } - If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\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 'HKCU\Control Panel\Desktop' -Value 'PreferredUILanguages' -SID $RunAsActiveUser.SID - } - If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop\MuiCached' -Value 'MachinePreferredUILanguages' -SID $RunAsActiveUser.SID - } - If (-not $HKULanguages) { - [string[]]$HKULanguages = Get-RegistryKey -Key 'HKCU\Control Panel\International' -Value 'LocaleName' -SID $RunAsActiveUser.SID - } - # Read language for Win XP machines - If (-not $HKULanguages) { - [string]$HKULocale = Get-RegistryKey -Key 'HKCU\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 + # 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 @@ -421,8 +479,8 @@ if ($appDeployLogoBannerHeight -gt $appDeployLogoBannerMaxHeight) { If (-not $deploymentType) { [string]$deploymentType = 'Install' } ## Variables: Executables -[string]$exeWusa = 'wusa.exe' # Installs Standalone Windows Updates -[string]$exeMsiexec = 'msiexec.exe' # Installs MSI Installers +[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 @@ -433,14 +491,14 @@ If (-not $deploymentType) { [string]$deploymentType = 'Install' } ## Variables: Registry Keys # Registry keys for native and WOW64 applications -[string[]]$regKeyApplications = 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +[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 = 'HKLM:SOFTWARE\Wow6432Node\Lotus\Notes' + [string]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Lotus\Notes' } Else { - [string]$regKeyLotusNotes = 'HKLM:SOFTWARE\Lotus\Notes' + [string]$regKeyLotusNotes = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Lotus\Notes' } -[string]$regKeyAppExecution = 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options' +[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' @@ -459,27 +517,27 @@ If (Test-Path -LiteralPath 'variable:deferDays') { Remove-Variable -Name 'deferD ## Variables: System DPI Scale Factor [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 - If ($RunAsActiveUser) { - [int32]$dpiPixels = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop\WindowMetrics' -Value 'AppliedDPI' -SID $RunAsActiveUser.SID - If (-not ([string]$dpiPixels)) { - [int32]$dpiPixels = Get-RegistryKey -Key 'HKCU\Control Panel\Desktop' -Value 'LogPixels' -SID $RunAsActiveUser.SID - } - [boolean]$UserDisplayScaleFactor = $true - } - If (-not ([string]$dpiPixels)) { - # This registry setting only exists if system scale factor has been changed at least once - [int32]$dpiPixels = Get-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontDPI' -Value 'LogPixels' - [boolean]$UserDisplayScaleFactor = $false - } - Switch ($dpiPixels) { - 96 { [int32]$dpiScale = 100 } - 120 { [int32]$dpiScale = 125 } - 144 { [int32]$dpiScale = 150 } - 192 { [int32]$dpiScale = 200 } - Default { [int32]$dpiScale = 100 } - } + # If a user is logged on, then get display scale factor for logged on user (even if running in session 0) + [boolean]$UserDisplayScaleFactor = $false + If ($RunAsActiveUser) { + [int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics' -Value 'AppliedDPI' -SID $RunAsActiveUser.SID + If (-not ([string]$dpiPixels)) { + [int32]$dpiPixels = Get-RegistryKey -Key 'Registry::HKEY_CURRENT_USER\Control Panel\Desktop' -Value 'LogPixels' -SID $RunAsActiveUser.SID + } + [boolean]$UserDisplayScaleFactor = $true + } + If (-not ([string]$dpiPixels)) { + # 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 + } + Switch ($dpiPixels) { + 96 { [int32]$dpiScale = 100 } + 120 { [int32]$dpiScale = 125 } + 144 { [int32]$dpiScale = 150 } + 192 { [int32]$dpiScale = 200 } + Default { [int32]$dpiScale = 100 } + } } #endregion ##*============================================= @@ -495,117 +553,130 @@ If (Test-Path -LiteralPath 'variable:deferDays') { Remove-Variable -Name 'deferD Function Write-FunctionHeaderOrFooter { <# .SYNOPSIS - Write the function header or footer to the log upon first entering or exiting a function. + 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. + 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. + The name of the function this function is invoked from. .PARAMETER CmdletBoundParameters - The bound parameters of the function this function is invoked from. + The bound parameters of the function this function is invoked from. .PARAMETER Header - Write the function header. + Write the function header. .PARAMETER Footer - Write the function footer. + Write the function footer. .EXAMPLE - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header .EXAMPLE - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://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): `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 - } + [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): `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. + 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. .PARAMETER Path + Path to the msp file +.PARAMETER AddParameters + Additional parameters .EXAMPLE - Execute-MSP -Path 'Adobe_Reader_11.0.3_EN.msp' + Execute-MSP -Path 'Adobe_Reader_11.0.3_EN.msp' .NOTES .LINK - http://psappdeploytoolkit.com + http://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 - ) - - 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 - - $Installer = New-Object -com WindowsInstaller.Installer - $Database = $Installer.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $Installer, $($mspFile,([int32]32))) - [__comobject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation' - [hashtable]$SummaryInfoProperty = @{} - $all = (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7)).Split(";") - Foreach($FormattedProductCode in $all) { - [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) { Execute-MSI -Action Patch -Path $Path } - } + [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 + + $Installer = New-Object -com WindowsInstaller.Installer + $Database = $Installer.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $Null, $Installer, $($mspFile,([int32]32))) + [__comobject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation' + [hashtable]$SummaryInfoProperty = @{} + $all = (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7)).Split(";") + Foreach($FormattedProductCode in $all) { + [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 @@ -613,305 +684,305 @@ Function Execute-MSP { Function Write-Log { <# .SYNOPSIS - Write messages to a log file in CMTrace.exe compatible format or Legacy text file format. + 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. + 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. + 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) + 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. + 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. + 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. + 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. + Set the directory where the log file will be saved. .PARAMETER LogFileName - Set the name of the log file. + Set the name of the log file. .PARAMETER MaxLogFileSizeMB - Maximum file size limit for log file in megabytes (MB). Default is 10 MB. + Maximum file size limit for log file in megabytes (MB). Default is 10 MB. .PARAMETER WriteHost - Write the log message to the console. + 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. + 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 + 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. + 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. + Debug messages only get logged if this parameter is set to $true in the config XML file. .EXAMPLE - Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace' + 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' + Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy' .NOTES .LINK - http://psappdeploytoolkit.com + http://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 = '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 - [string]$LogTime = (Get-Date -Format 'HH\:mm\:ss.fff').ToString() - [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString() - If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).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 - ) - "" + "" - } - - ## Create script block for writing log entry to the console - [scriptblock]$WriteLogLineToHost = { - Param ( - [string]$lTextLogLine, - [int16]$lSeverity - ) - If ($WriteHost) { - # Only output using color options if running in a host which supports colors. - If ($Host.UI.RawUI.ForegroundColor) { - Switch ($lSeverity) { - 3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' } - 2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' } - 1 { Write-Host -Object $lTextLogLine } - } - } - # If executing "powershell.exe -File .ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log. - Else { - Write-Output -InputObject $lTextLogLine - } - } - } - - ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file - If (($DebugMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return } - ## Exit function if logging to file is disabled and logging to console host is disabled - If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return } - ## Exit Begin block if logging is disabled - If ($DisableLogging) { Return } - ## Exit function function if it is an [Initialization] message and the toolkit has been relaunched - If (($AsyncToolkitLaunch) -and ($ScriptSection -eq 'Initialization')) { [boolean]$ExitLoggingFunction = $true; Return } - - ## Create the directory where the log file will be saved - If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) { - Try { - $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop' - } - Catch { - [boolean]$ExitLoggingFunction = $true - # If error creating directory, write message to console - If (-not $ContinueOnError) { - Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red' - } - Return - } - } - - ## Assemble the fully qualified path to the log file - [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName - } - Process { - ## Exit function if logging is disabled - If ($ExitLoggingFunction) { Return } - - ForEach ($Msg in $Message) { - ## If the message is not $null or empty, create the log entry for the different logging methods - [string]$CMTraceMsg = '' - [string]$ConsoleLogLine = '' - [string]$LegacyTextLogLine = '' - If ($Msg) { - # Create the CMTrace log message - If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" } - - # Create a Console and Legacy "text" log entry - [string]$LegacyMsg = "[$LogDate $LogTime]" - If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" } - If ($Source) { - [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg" - Switch ($Severity) { - 3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" } - 2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" } - 1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" } - } - } - Else { - [string]$ConsoleLogLine = "$LegacyMsg :: $Msg" - Switch ($Severity) { - 3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" } - 2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" } - 1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" } - } - } - } - - ## Execute script block to create the CMTrace.exe compatible log entry - [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $Severity - - ## Choose which log type to write to file - If ($LogType -ieq 'CMTrace') { - [string]$LogLine = $CMTraceLogLine - } - Else { - [string]$LogLine = $LegacyTextLogLine - } - - ## Write the log entry to the log file if logging is not currently disabled - If (-not $DisableLogging) { - Try { - $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop' - } - Catch { - If (-not $ContinueOnError) { - Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red' - } - } - } - - ## Execute script block to write the log entry to the console if $WriteHost is $true - & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity - } - } - End { - ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0 - Try { - If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) { - [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop' - [decimal]$LogFileSizeMB = $LogFile.Length/1MB - If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) { - ## Change the file extension to "lo_" - [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_') - [hashtable]$ArchiveLogParams = @{ ScriptSection = $ScriptSection; Source = ${CmdletName}; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; WriteHost = $WriteHost; ContinueOnError = $ContinueOnError; PassThru = $false } - - ## Log message about archiving the log file - $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]." - Write-Log -Message $ArchiveLogMessage @ArchiveLogParams - - ## Archive existing log file from .log to .lo_. Overwrites any existing .lo_ file. This is the same method SCCM uses for log files. - Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop' - - ## Start new log file and Log message about archiving the old log file - $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached." - Write-Log -Message $NewLogMessage @ArchiveLogParams - } - } - } - Catch { - ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size - } - Finally { - If ($PassThru) { Write-Output -InputObject $Message } - } - } + [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 + [string]$LogTime = (Get-Date -Format 'HH\:mm\:ss.fff').ToString() + [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString() + If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).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 + ) + "" + "" + } + + ## Create script block for writing log entry to the console + [scriptblock]$WriteLogLineToHost = { + Param ( + [string]$lTextLogLine, + [int16]$lSeverity + ) + If ($WriteHost) { + # Only output using color options if running in a host which supports colors. + If ($Host.UI.RawUI.ForegroundColor) { + Switch ($lSeverity) { + 3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' } + 2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' } + 1 { Write-Host -Object $lTextLogLine } + } + } + # If executing "powershell.exe -File .ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log. + Else { + Write-Output -InputObject $lTextLogLine + } + } + } + + ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file + If (($DebugMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return } + ## Exit function if logging to file is disabled and logging to console host is disabled + If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return } + ## Exit Begin block if logging is disabled + If ($DisableLogging) { Return } + ## Exit function function if it is an [Initialization] message and the toolkit has been relaunched + If (($AsyncToolkitLaunch) -and ($ScriptSection -eq 'Initialization')) { [boolean]$ExitLoggingFunction = $true; Return } + + ## Create the directory where the log file will be saved + If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) { + Try { + $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop' + } + Catch { + [boolean]$ExitLoggingFunction = $true + # If error creating directory, write message to console + If (-not $ContinueOnError) { + Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red' + } + Return + } + } + + ## Assemble the fully qualified path to the log file + [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName + } + Process { + ## Exit function if logging is disabled + If ($ExitLoggingFunction) { Return } + + ForEach ($Msg in $Message) { + ## If the message is not $null or empty, create the log entry for the different logging methods + [string]$CMTraceMsg = '' + [string]$ConsoleLogLine = '' + [string]$LegacyTextLogLine = '' + If ($Msg) { + # Create the CMTrace log message + If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" } + + # Create a Console and Legacy "text" log entry + [string]$LegacyMsg = "[$LogDate $LogTime]" + If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" } + If ($Source) { + [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg" + Switch ($Severity) { + 3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" } + 2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" } + 1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" } + } + } + Else { + [string]$ConsoleLogLine = "$LegacyMsg :: $Msg" + Switch ($Severity) { + 3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" } + 2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" } + 1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" } + } + } + } + + ## Execute script block to create the CMTrace.exe compatible log entry + [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $Severity + + ## Choose which log type to write to file + If ($LogType -ieq 'CMTrace') { + [string]$LogLine = $CMTraceLogLine + } + Else { + [string]$LogLine = $LegacyTextLogLine + } + + ## Write the log entry to the log file if logging is not currently disabled + If (-not $DisableLogging) { + Try { + $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop' + } + Catch { + If (-not $ContinueOnError) { + Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red' + } + } + } + + ## Execute script block to write the log entry to the console if $WriteHost is $true + & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity + } + } + End { + ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0 + Try { + If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) { + [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop' + [decimal]$LogFileSizeMB = $LogFile.Length/1MB + If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) { + ## Change the file extension to "lo_" + [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_') + [hashtable]$ArchiveLogParams = @{ ScriptSection = $ScriptSection; Source = ${CmdletName}; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; WriteHost = $WriteHost; ContinueOnError = $ContinueOnError; PassThru = $false } + + ## Log message about archiving the log file + $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]." + Write-Log -Message $ArchiveLogMessage @ArchiveLogParams + + ## Archive existing log file from .log to .lo_. Overwrites any existing .lo_ file. This is the same method SCCM uses for log files. + Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop' + + ## Start new log file and Log message about archiving the old log file + $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached." + Write-Log -Message $NewLogMessage @ArchiveLogParams + } + } + } + Catch { + ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size + } + Finally { + If ($PassThru) { Write-Output -InputObject $Message } + } + } } #endregion #region Function Remove-InvalidFileNameChars Function Remove-InvalidFileNameChars { - <# - .SYNOPSIS - Remove invalid characters from the supplied string. - .DESCRIPTION - Remove invalid characters from the supplied string and returns a valid filename as a string. - .EXAMPLE - Remove-InvalidFileNameChars -Name "Filename/\1" - .NOTES - This functions always returns a string however it can be empty if the name only contains invalid characters. - Do no use this command for an entire path as '\' is not a valid filename character. - .LINK - http://psappdeploytoolkit.com - #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] - [AllowEmptyString()] - [string]$Name - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Output -InputObject (([char[]]$Name | Where-Object { $invalidFileNameChars -notcontains $_ }) -join '') - } - Catch { - Write-Log -Message "Failed to remove invalid characters from the supplied filename. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + <# + .SYNOPSIS + Remove invalid characters from the supplied string. + .DESCRIPTION + Remove invalid characters from the supplied string and returns a valid filename as a string. + .EXAMPLE + Remove-InvalidFileNameChars -Name "Filename/\1" + .NOTES + This functions always returns a string however it can be empty if the name only contains invalid characters. + Do no use this command for an entire path as '\' is not a valid filename character. + .LINK + http://psappdeploytoolkit.com + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] + [AllowEmptyString()] + [string]$Name + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Output -InputObject (([char[]]$Name | Where-Object { $invalidFileNameChars -notcontains $_ }) -join '') + } + Catch { + Write-Log -Message "Failed to remove invalid characters from the supplied filename. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -920,177 +991,177 @@ Function Remove-InvalidFileNameChars { Function New-ZipFile { <# .SYNOPSIS - Create a new zip archive or add content to an existing archive. + Create a new zip archive or add content to an existing archive. .DESCRIPTION - Create a new zip archive or add content to an existing archive by using the Shell object .CopyHere method. + Create a new zip archive or add content to an existing archive by using the Shell object .CopyHere method. .PARAMETER DestinationArchiveDirectoryPath - The path to the directory path where the zip archive will be saved. + The path to the directory path where the zip archive will be saved. .PARAMETER DestinationArchiveFileName - The name of the zip archive. + The name of the zip archive. .PARAMETER SourceDirectoryPath - The path to the directory to be archived, specified as absolute paths. + The path to the directory to be archived, specified as absolute paths. .PARAMETER SourceFilePath - The path to the file to be archived, specified as absolute paths. + The path to the file to be archived, specified as absolute paths. .PARAMETER RemoveSourceAfterArchiving - Remove the source path after successfully archiving the content. Default is: $false. + Remove the source path after successfully archiving the content. Default is: $false. .PARAMETER OverWriteArchive - Overwrite the destination archive path if it already exists. Default is: $false. + Overwrite the destination archive path if it already exists. Default is: $false. .PARAMETER ContinueOnError - Continue if an error is encountered. Default: $true. + Continue if an error is encountered. Default: $true. .EXAMPLE - New-ZipFile -DestinationArchiveDirectoryPath 'E:\Testing' -DestinationArchiveFileName 'TestingLogs.zip' -SourceDirectory 'E:\Testing\Logs' + New-ZipFile -DestinationArchiveDirectoryPath 'E:\Testing' -DestinationArchiveFileName 'TestingLogs.zip' -SourceDirectory 'E:\Testing\Logs' .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding(DefaultParameterSetName='CreateFromDirectory')] - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNullorEmpty()] - [string]$DestinationArchiveDirectoryPath, - [Parameter(Mandatory=$true,Position=1)] - [ValidateNotNullorEmpty()] - [string]$DestinationArchiveFileName, - [Parameter(Mandatory=$true,Position=2,ParameterSetName='CreateFromDirectory')] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Container' })] - [string[]]$SourceDirectoryPath, - [Parameter(Mandatory=$true,Position=2,ParameterSetName='CreateFromFile')] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [string[]]$SourceFilePath, - [Parameter(Mandatory=$false,Position=3)] - [ValidateNotNullorEmpty()] - [switch]$RemoveSourceAfterArchiving = $false, - [Parameter(Mandatory=$false,Position=4)] - [ValidateNotNullorEmpty()] - [switch]$OverWriteArchive = $false, - [Parameter(Mandatory=$false,Position=5)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## Remove invalid characters from the supplied filename - $DestinationArchiveFileName = Remove-InvalidFileNameChars -Name $DestinationArchiveFileName - If ($DestinationArchiveFileName.length -eq 0) { - Throw "Invalid filename characters replacement resulted into an empty string." - } - ## Get the full destination path where the archive will be stored - [string]$DestinationPath = Join-Path -Path $DestinationArchiveDirectoryPath -ChildPath $DestinationArchiveFileName -ErrorAction 'Stop' - Write-Log -Message "Create a zip archive with the requested content at destination path [$DestinationPath]." -Source ${CmdletName} - - ## If the destination archive already exists, delete it if the -OverWriteArchive option was selected - If (($OverWriteArchive) -and (Test-Path -LiteralPath $DestinationPath)) { - Write-Log -Message "An archive at the destination path already exists, deleting file [$DestinationPath]." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $DestinationPath -Force -ErrorAction 'Stop' - } - - ## If archive file does not exist, then create a zero-byte zip archive - If (-not (Test-Path -LiteralPath $DestinationPath)) { - ## Create a zero-byte file - Write-Log -Message "Create a zero-byte file [$DestinationPath]." -Source ${CmdletName} - $null = New-Item -Path $DestinationArchiveDirectoryPath -Name $DestinationArchiveFileName -ItemType 'File' -Force -ErrorAction 'Stop' - - ## Write the file header for a zip file to the zero-byte file - [byte[]]$ZipArchiveByteHeader = 80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - [IO.FileStream]$FileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($DestinationPath, ([IO.FileMode]::Create)) - [IO.BinaryWriter]$BinaryWriter = New-Object -TypeName 'System.IO.BinaryWriter' -ArgumentList ($FileStream) - Write-Log -Message "Write the file header for a zip archive to the zero-byte file [$DestinationPath]." -Source ${CmdletName} - $null = $BinaryWriter.Write($ZipArchiveByteHeader) - $BinaryWriter.Close() - $FileStream.Close() - } - - ## Create a Shell object - [__comobject]$ShellApp = New-Object -ComObject 'Shell.Application' -ErrorAction 'Stop' - ## Create an object representing the archive file - [__comobject]$Archive = $ShellApp.NameSpace($DestinationPath) - - ## Create the archive file - If ($PSCmdlet.ParameterSetName -eq 'CreateFromDirectory') { - ## Create the archive file from a source directory - ForEach ($Directory in $SourceDirectoryPath) { - Try { - # Create an object representing the source directory - [__comobject]$CreateFromDirectory = $ShellApp.NameSpace($Directory) - # Copy all of the files and folders from the source directory to the archive - $null = $Archive.CopyHere($CreateFromDirectory.Items()) - # Wait for archive operation to complete. Archive file count property returns 0 if archive operation is in progress. - Write-Log -Message "Compressing [$($CreateFromDirectory.Count)] file(s) in source directory [$Directory] to destination path [$DestinationPath]..." -Source ${CmdletName} - Do { Start-Sleep -Milliseconds 250 } While ($Archive.Items().Count -eq 0) - } - Finally { - # Release the ComObject representing the source directory - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($CreateFromDirectory) - } - - # If option was selected, recursively delete the source directory after successfully archiving the contents - If ($RemoveSourceAfterArchiving) { - Try { - Write-Log -Message "Recursively delete the source directory [$Directory] as contents have been successfully archived." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $Directory -Recurse -Force -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to recursively delete the source directory [$Directory]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - } - } - Else { - ## Create the archive file from a list of one or more files - [IO.FileInfo[]]$SourceFilePath = [IO.FileInfo[]]$SourceFilePath - ForEach ($File in $SourceFilePath) { - # Copy the files and folders from the source directory to the archive - $null = $Archive.CopyHere($File.FullName) - # Wait for archive operation to complete. Archive file count property returns 0 if archive operation is in progress. - Write-Log -Message "Compressing file [$($File.FullName)] to destination path [$DestinationPath]..." -Source ${CmdletName} - Do { Start-Sleep -Milliseconds 250 } While ($Archive.Items().Count -eq 0) - - # If option was selected, delete the source file after successfully archiving the content - If ($RemoveSourceAfterArchiving) { - Try { - Write-Log -Message "Delete the source file [$($File.FullName)] as it has been successfully archived." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $File.FullName -Force -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to delete the source file [$($File.FullName)]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - } - } - - ## If the archive was created in session 0 or by an Admin, then it may only be readable by elevated users. - # Apply the parent folder's permissions to the archive file to fix the problem. - Write-Log -Message "If the archive was created in session 0 or by an Admin, then it may only be readable by elevated users. Apply permissions from parent folder [$DestinationArchiveDirectoryPath] to file [$DestinationPath]." -Source ${CmdletName} - Try { - [Security.AccessControl.DirectorySecurity]$DestinationArchiveDirectoryPathAcl = Get-Acl -Path $DestinationArchiveDirectoryPath -ErrorAction 'Stop' - Set-Acl -Path $DestinationPath -AclObject $DestinationArchiveDirectoryPathAcl -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to apply parent folder's [$DestinationArchiveDirectoryPath] permissions to file [$DestinationPath]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - Catch { - Write-Log -Message "Failed to archive the requested file(s). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to archive the requested file(s): $($_.Exception.Message)" - } - } - Finally { - ## Release the ComObject representing the archive - If ($Archive) { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Archive) } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding(DefaultParameterSetName='CreateFromDirectory')] + Param ( + [Parameter(Mandatory=$true,Position=0)] + [ValidateNotNullorEmpty()] + [string]$DestinationArchiveDirectoryPath, + [Parameter(Mandatory=$true,Position=1)] + [ValidateNotNullorEmpty()] + [string]$DestinationArchiveFileName, + [Parameter(Mandatory=$true,Position=2,ParameterSetName='CreateFromDirectory')] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Container' })] + [string[]]$SourceDirectoryPath, + [Parameter(Mandatory=$true,Position=2,ParameterSetName='CreateFromFile')] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [string[]]$SourceFilePath, + [Parameter(Mandatory=$false,Position=3)] + [ValidateNotNullorEmpty()] + [switch]$RemoveSourceAfterArchiving = $false, + [Parameter(Mandatory=$false,Position=4)] + [ValidateNotNullorEmpty()] + [switch]$OverWriteArchive = $false, + [Parameter(Mandatory=$false,Position=5)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## Remove invalid characters from the supplied filename + $DestinationArchiveFileName = Remove-InvalidFileNameChars -Name $DestinationArchiveFileName + If ($DestinationArchiveFileName.length -eq 0) { + Throw "Invalid filename characters replacement resulted into an empty string." + } + ## Get the full destination path where the archive will be stored + [string]$DestinationPath = Join-Path -Path $DestinationArchiveDirectoryPath -ChildPath $DestinationArchiveFileName -ErrorAction 'Stop' + Write-Log -Message "Create a zip archive with the requested content at destination path [$DestinationPath]." -Source ${CmdletName} + + ## If the destination archive already exists, delete it if the -OverWriteArchive option was selected + If (($OverWriteArchive) -and (Test-Path -LiteralPath $DestinationPath)) { + Write-Log -Message "An archive at the destination path already exists, deleting file [$DestinationPath]." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $DestinationPath -Force -ErrorAction 'Stop' + } + + ## If archive file does not exist, then create a zero-byte zip archive + If (-not (Test-Path -LiteralPath $DestinationPath)) { + ## Create a zero-byte file + Write-Log -Message "Create a zero-byte file [$DestinationPath]." -Source ${CmdletName} + $null = New-Item -Path $DestinationArchiveDirectoryPath -Name $DestinationArchiveFileName -ItemType 'File' -Force -ErrorAction 'Stop' + + ## Write the file header for a zip file to the zero-byte file + [byte[]]$ZipArchiveByteHeader = 80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + [IO.FileStream]$FileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($DestinationPath, ([IO.FileMode]::Create)) + [IO.BinaryWriter]$BinaryWriter = New-Object -TypeName 'System.IO.BinaryWriter' -ArgumentList ($FileStream) + Write-Log -Message "Write the file header for a zip archive to the zero-byte file [$DestinationPath]." -Source ${CmdletName} + $null = $BinaryWriter.Write($ZipArchiveByteHeader) + $BinaryWriter.Close() + $FileStream.Close() + } + + ## Create a Shell object + [__comobject]$ShellApp = New-Object -ComObject 'Shell.Application' -ErrorAction 'Stop' + ## Create an object representing the archive file + [__comobject]$Archive = $ShellApp.NameSpace($DestinationPath) + + ## Create the archive file + If ($PSCmdlet.ParameterSetName -eq 'CreateFromDirectory') { + ## Create the archive file from a source directory + ForEach ($Directory in $SourceDirectoryPath) { + Try { + # Create an object representing the source directory + [__comobject]$CreateFromDirectory = $ShellApp.NameSpace($Directory) + # Copy all of the files and folders from the source directory to the archive + $null = $Archive.CopyHere($CreateFromDirectory.Items()) + # Wait for archive operation to complete. Archive file count property returns 0 if archive operation is in progress. + Write-Log -Message "Compressing [$($CreateFromDirectory.Count)] file(s) in source directory [$Directory] to destination path [$DestinationPath]..." -Source ${CmdletName} + Do { Start-Sleep -Milliseconds 250 } While ($Archive.Items().Count -eq 0) + } + Finally { + # Release the ComObject representing the source directory + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($CreateFromDirectory) + } + + # If option was selected, recursively delete the source directory after successfully archiving the contents + If ($RemoveSourceAfterArchiving) { + Try { + Write-Log -Message "Recursively delete the source directory [$Directory] as contents have been successfully archived." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $Directory -Recurse -Force -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to recursively delete the source directory [$Directory]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + } + } + Else { + ## Create the archive file from a list of one or more files + [IO.FileInfo[]]$SourceFilePath = [IO.FileInfo[]]$SourceFilePath + ForEach ($File in $SourceFilePath) { + # Copy the files and folders from the source directory to the archive + $null = $Archive.CopyHere($File.FullName) + # Wait for archive operation to complete. Archive file count property returns 0 if archive operation is in progress. + Write-Log -Message "Compressing file [$($File.FullName)] to destination path [$DestinationPath]..." -Source ${CmdletName} + Do { Start-Sleep -Milliseconds 250 } While ($Archive.Items().Count -eq 0) + + # If option was selected, delete the source file after successfully archiving the content + If ($RemoveSourceAfterArchiving) { + Try { + Write-Log -Message "Delete the source file [$($File.FullName)] as it has been successfully archived." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $File.FullName -Force -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to delete the source file [$($File.FullName)]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + } + } + + ## If the archive was created in session 0 or by an Admin, then it may only be readable by elevated users. + # Apply the parent folder's permissions to the archive file to fix the problem. + Write-Log -Message "If the archive was created in session 0 or by an Admin, then it may only be readable by elevated users. Apply permissions from parent folder [$DestinationArchiveDirectoryPath] to file [$DestinationPath]." -Source ${CmdletName} + Try { + [Security.AccessControl.DirectorySecurity]$DestinationArchiveDirectoryPathAcl = Get-Acl -Path $DestinationArchiveDirectoryPath -ErrorAction 'Stop' + Set-Acl -Path $DestinationPath -AclObject $DestinationArchiveDirectoryPathAcl -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to apply parent folder's [$DestinationArchiveDirectoryPath] permissions to file [$DestinationPath]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + Catch { + Write-Log -Message "Failed to archive the requested file(s). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to archive the requested file(s): $($_.Exception.Message)" + } + } + Finally { + ## Release the ComObject representing the archive + If ($Archive) { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Archive) } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -1099,103 +1170,103 @@ Function New-ZipFile { Function Exit-Script { <# .SYNOPSIS - Exit the script, perform cleanup actions, and pass an exit code to the parent process. + Exit the script, perform cleanup actions, and pass an exit code to the parent process. .DESCRIPTION - Always use when exiting the script to ensure cleanup actions are performed. + Always use when exiting the script to ensure cleanup actions are performed. .PARAMETER ExitCode - The exit code to be passed from the script to the parent process, e.g. SCCM + The exit code to be passed from the script to the parent process, e.g. SCCM .EXAMPLE - Exit-Script -ExitCode 0 + Exit-Script -ExitCode 0 .EXAMPLE - Exit-Script -ExitCode 1618 + Exit-Script -ExitCode 1618 .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$ExitCode = 0 - ) - - ## Get the name of this function - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - ## Stop the Close Program Dialog if running - If ($formCloseApps) { $formCloseApps.Close } - - ## Close the Installation Progress Dialog if running - Close-InstallationProgress - - ## If block execution variable is true, call the function to unblock execution - If ($BlockExecution) { Unblock-AppExecution } - - ## If Terminal Server mode was set, turn it off - If ($terminalServerMode) { Disable-TerminalServerInstallMode } - - ## Determine action based on exit code - Switch ($exitCode) { - $configInstallationUIExitCode { $installSuccess = $false } - $configInstallationDeferExitCode { $installSuccess = $false } - 3010 { $installSuccess = $true } - 1641 { $installSuccess = $true } - 0 { $installSuccess = $true } - Default { $installSuccess = $false } - } - - ## Determine if balloon notification should be shown - If ($deployModeSilent) { [boolean]$configShowBalloonNotifications = $false } - - If ($installSuccess) { - If (Test-Path -LiteralPath $regKeyDeferHistory -ErrorAction 'SilentlyContinue') { - Write-Log -Message 'Remove deferral history...' -Source ${CmdletName} - Remove-RegistryKey -Key $regKeyDeferHistory -Recurse - } - - [string]$balloonText = "$deploymentTypeName $configBalloonTextComplete" - ## Handle reboot prompts on successful script completion - If (($AllowRebootPassThru) -and ((($msiRebootDetected) -or ($exitCode -eq 3010)) -or ($exitCode -eq 1641))) { - Write-Log -Message 'A restart has been flagged as required.' -Source ${CmdletName} - [string]$balloonText = "$deploymentTypeName $configBalloonTextRestartRequired" - If (($msiRebootDetected) -and ($exitCode -ne 1641)) { [int32]$exitCode = 3010 } - } - Else { - [int32]$exitCode = 0 - } - - Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText } - } - ElseIf (-not $installSuccess) { - Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} - If (($exitCode -eq $configInstallationUIExitCode) -or ($exitCode -eq $configInstallationDeferExitCode)) { - [string]$balloonText = "$deploymentTypeName $configBalloonTextFastRetry" - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Warning' -BalloonTipText $balloonText } - } - Else { - [string]$balloonText = "$deploymentTypeName $configBalloonTextError" - If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Error' -BalloonTipText $balloonText } - } - } - - [string]$LogDash = '-' * 79 - Write-Log -Message $LogDash -Source ${CmdletName} - - ## Archive the log files to zip format and then delete the temporary logs folder - If ($configToolkitCompressLogs) { - ## Disable logging to file so that we can archive the log files - . $DisableScriptLogging - - [string]$DestinationArchiveFileName = $installName + '_' + $deploymentType + '_' + ((Get-Date -Format 'yyyy-MM-dd-hh-mm-ss').ToString()) + '.zip' - New-ZipFile -DestinationArchiveDirectoryPath $configToolkitLogDir -DestinationArchiveFileName $DestinationArchiveFileName -SourceDirectory $logTempFolder -RemoveSourceAfterArchiving - } - - If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } - ## Reset powershell window title to its previous title - $Host.UI.RawUI.WindowTitle = $oldPSWindowTitle - ## Exit the script, returning the exit code to SCCM - If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $exitCode; Exit } Else { Exit $exitCode } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$ExitCode = 0 + ) + + ## Get the name of this function + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + ## Stop the Close Program Dialog if running + If ($formCloseApps) { $formCloseApps.Close } + + ## Close the Installation Progress Dialog if running + Close-InstallationProgress + + ## If block execution variable is true, call the function to unblock execution + If ($BlockExecution) { Unblock-AppExecution } + + ## If Terminal Server mode was set, turn it off + If ($terminalServerMode) { Disable-TerminalServerInstallMode } + + ## Determine action based on exit code + Switch ($exitCode) { + $configInstallationUIExitCode { $installSuccess = $false } + $configInstallationDeferExitCode { $installSuccess = $false } + 3010 { $installSuccess = $true } + 1641 { $installSuccess = $true } + 0 { $installSuccess = $true } + Default { $installSuccess = $false } + } + + ## Determine if balloon notification should be shown + If ($deployModeSilent) { [boolean]$configShowBalloonNotifications = $false } + + If ($installSuccess) { + If (Test-Path -LiteralPath $regKeyDeferHistory -ErrorAction 'SilentlyContinue') { + Write-Log -Message 'Remove deferral history...' -Source ${CmdletName} + Remove-RegistryKey -Key $regKeyDeferHistory -Recurse + } + + [string]$balloonText = "$deploymentTypeName $configBalloonTextComplete" + ## Handle reboot prompts on successful script completion + If (($AllowRebootPassThru) -and ((($msiRebootDetected) -or ($exitCode -eq 3010)) -or ($exitCode -eq 1641))) { + Write-Log -Message 'A restart has been flagged as required.' -Source ${CmdletName} + [string]$balloonText = "$deploymentTypeName $configBalloonTextRestartRequired" + If (($msiRebootDetected) -and ($exitCode -ne 1641)) { [int32]$exitCode = 3010 } + } + Else { + [int32]$exitCode = 0 + } + + Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText } + } + ElseIf (-not $installSuccess) { + Write-Log -Message "$installName $deploymentTypeName completed with exit code [$exitcode]." -Source ${CmdletName} + If (($exitCode -eq $configInstallationUIExitCode) -or ($exitCode -eq $configInstallationDeferExitCode)) { + [string]$balloonText = "$deploymentTypeName $configBalloonTextFastRetry" + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Warning' -BalloonTipText $balloonText } + } + Else { + [string]$balloonText = "$deploymentTypeName $configBalloonTextError" + If ($configShowBalloonNotifications) { Show-BalloonTip -BalloonTipIcon 'Error' -BalloonTipText $balloonText } + } + } + + [string]$LogDash = '-' * 79 + Write-Log -Message $LogDash -Source ${CmdletName} + + ## Archive the log files to zip format and then delete the temporary logs folder + If ($configToolkitCompressLogs) { + ## Disable logging to file so that we can archive the log files + . $DisableScriptLogging + + [string]$DestinationArchiveFileName = $installName + '_' + $deploymentType + '_' + ((Get-Date -Format 'yyyy-MM-dd-HH-mm-ss').ToString()) + '.zip' + New-ZipFile -DestinationArchiveDirectoryPath $configToolkitLogDir -DestinationArchiveFileName $DestinationArchiveFileName -SourceDirectory $logTempFolder -RemoveSourceAfterArchiving + } + + If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } + ## Reset powershell window title to its previous title + $Host.UI.RawUI.WindowTitle = $oldPSWindowTitle + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $exitCode; Exit } Else { Exit $exitCode } } #endregion @@ -1204,178 +1275,178 @@ Function Exit-Script { Function Resolve-Error { <# .SYNOPSIS - Enumerate error record details. + Enumerate error record details. .DESCRIPTION - Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated. + Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated. .PARAMETER ErrorRecord - The error record to resolve. The default error record is the latest one: $global:Error[0]. This parameter will also accept an array of error records. + The error record to resolve. The default error record is the latest one: $global:Error[0]. This parameter will also accept an array of error records. .PARAMETER Property - The list of properties to display from the error record. Use "*" to display all properties. - Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException + The list of properties to display from the error record. Use "*" to display all properties. + Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException .PARAMETER GetErrorRecord - Get error record details as represented by $_. + Get error record details as represented by $_. .PARAMETER GetErrorInvocation - Get error record invocation information as represented by $_.InvocationInfo. + Get error record invocation information as represented by $_.InvocationInfo. .PARAMETER GetErrorException - Get error record exception details as represented by $_.Exception. + Get error record exception details as represented by $_.Exception. .PARAMETER GetErrorInnerException - Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one. + Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one. .EXAMPLE - Resolve-Error + Resolve-Error .EXAMPLE - Resolve-Error -Property * + Resolve-Error -Property * .EXAMPLE - Resolve-Error -Property InnerException + Resolve-Error -Property InnerException .EXAMPLE - Resolve-Error -GetErrorInvocation:$false + Resolve-Error -GetErrorInvocation:$false .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] - [AllowEmptyCollection()] - [array]$ErrorRecord, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'), - [Parameter(Mandatory=$false,Position=2)] - [switch]$GetErrorRecord = $true, - [Parameter(Mandatory=$false,Position=3)] - [switch]$GetErrorInvocation = $true, - [Parameter(Mandatory=$false,Position=4)] - [switch]$GetErrorException = $true, - [Parameter(Mandatory=$false,Position=5)] - [switch]$GetErrorInnerException = $true - ) - - Begin { - ## If function was called without specifying an error record, then choose the latest error that occurred - If (-not $ErrorRecord) { - If ($global:Error.Count -eq 0) { - #Write-Warning -Message "The `$Error collection is empty" - Return - } - Else { - [array]$ErrorRecord = $global:Error[0] - } - } - - ## Allows selecting and filtering the properties on the error object if they exist - [scriptblock]$SelectProperty = { - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - $InputObject, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string[]]$Property - ) - - [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name' - ForEach ($Prop in $Property) { - If ($Prop -eq '*') { - [string[]]$PropertySelection = $ObjectProperty - Break - } - ElseIf ($ObjectProperty -contains $Prop) { - [string[]]$PropertySelection += $Prop - } - } - Write-Output -InputObject $PropertySelection - } - - # Initialize variables to avoid error if 'Set-StrictMode' is set - $LogErrorRecordMsg = $null - $LogErrorInvocationMsg = $null - $LogErrorExceptionMsg = $null - $LogErrorMessageTmp = $null - $LogInnerMessage = $null - } - Process { - If (-not $ErrorRecord) { Return } - ForEach ($ErrRecord in $ErrorRecord) { - ## Capture Error Record - If ($GetErrorRecord) { - [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property - $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties - } - - ## Error Invocation Information - If ($GetErrorInvocation) { - If ($ErrRecord.InvocationInfo) { - [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property - $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties - } - } - - ## Capture Error Exception - If ($GetErrorException) { - If ($ErrRecord.Exception) { - [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property - $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties - } - } - - ## Display properties in the correct order - If ($Property -eq '*') { - # If all properties were chosen for display, then arrange them in the order the error object displays them by default. - If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg } - If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg } - If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg } - } - Else { - # Display selected properties in our custom order - If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg } - If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg } - If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg } - } - - If ($LogErrorMessageTmp) { - $LogErrorMessage = 'Error Record:' - $LogErrorMessage += "`n-------------" - $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String - $LogErrorMessage += $LogErrorMsg - } - - ## Capture Error Inner Exception(s) - If ($GetErrorInnerException) { - If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) { - $LogInnerMessage = 'Error Inner Exception(s):' - $LogInnerMessage += "`n-------------------------" - - $ErrorInnerException = $ErrRecord.Exception.InnerException - $Count = 0 - - While ($ErrorInnerException) { - [string]$InnerExceptionSeperator = '~' * 40 - - [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property - $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String - - If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator } - $LogInnerMessage += $LogErrorInnerExceptionMsg - - $Count++ - $ErrorInnerException = $ErrorInnerException.InnerException - } - } - } - - If ($LogErrorMessage) { $Output = $LogErrorMessage } - If ($LogInnerMessage) { $Output += $LogInnerMessage } - - Write-Output -InputObject $Output - - If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' } - If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' } - If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' } - If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' } - } - } - End { - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] + [AllowEmptyCollection()] + [array]$ErrorRecord, + [Parameter(Mandatory=$false,Position=1)] + [ValidateNotNullorEmpty()] + [string[]]$Property = ('Message','InnerException','FullyQualifiedErrorId','ScriptStackTrace','PositionMessage'), + [Parameter(Mandatory=$false,Position=2)] + [switch]$GetErrorRecord = $true, + [Parameter(Mandatory=$false,Position=3)] + [switch]$GetErrorInvocation = $true, + [Parameter(Mandatory=$false,Position=4)] + [switch]$GetErrorException = $true, + [Parameter(Mandatory=$false,Position=5)] + [switch]$GetErrorInnerException = $true + ) + + Begin { + ## If function was called without specifying an error record, then choose the latest error that occurred + If (-not $ErrorRecord) { + If ($global:Error.Count -eq 0) { + #Write-Warning -Message "The `$Error collection is empty" + Return + } + Else { + [array]$ErrorRecord = $global:Error[0] + } + } + + ## Allows selecting and filtering the properties on the error object if they exist + [scriptblock]$SelectProperty = { + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + $InputObject, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string[]]$Property + ) + + [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name' + ForEach ($Prop in $Property) { + If ($Prop -eq '*') { + [string[]]$PropertySelection = $ObjectProperty + Break + } + ElseIf ($ObjectProperty -contains $Prop) { + [string[]]$PropertySelection += $Prop + } + } + Write-Output -InputObject $PropertySelection + } + + # Initialize variables to avoid error if 'Set-StrictMode' is set + $LogErrorRecordMsg = $null + $LogErrorInvocationMsg = $null + $LogErrorExceptionMsg = $null + $LogErrorMessageTmp = $null + $LogInnerMessage = $null + } + Process { + If (-not $ErrorRecord) { Return } + ForEach ($ErrRecord in $ErrorRecord) { + ## Capture Error Record + If ($GetErrorRecord) { + [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property + $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties + } + + ## Error Invocation Information + If ($GetErrorInvocation) { + If ($ErrRecord.InvocationInfo) { + [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property + $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties + } + } + + ## Capture Error Exception + If ($GetErrorException) { + If ($ErrRecord.Exception) { + [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property + $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties + } + } + + ## Display properties in the correct order + If ($Property -eq '*') { + # If all properties were chosen for display, then arrange them in the order the error object displays them by default. + If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg } + If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg } + If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg } + } + Else { + # Display selected properties in our custom order + If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg } + If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg } + If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg } + } + + If ($LogErrorMessageTmp) { + $LogErrorMessage = 'Error Record:' + $LogErrorMessage += "`n-------------" + $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String + $LogErrorMessage += $LogErrorMsg + } + + ## Capture Error Inner Exception(s) + If ($GetErrorInnerException) { + If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) { + $LogInnerMessage = 'Error Inner Exception(s):' + $LogInnerMessage += "`n-------------------------" + + $ErrorInnerException = $ErrRecord.Exception.InnerException + $Count = 0 + + While ($ErrorInnerException) { + [string]$InnerExceptionSeperator = '~' * 40 + + [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property + $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String + + If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator } + $LogInnerMessage += $LogErrorInnerExceptionMsg + + $Count++ + $ErrorInnerException = $ErrorInnerException.InnerException + } + } + } + + If ($LogErrorMessage) { $Output = $LogErrorMessage } + If ($LogInnerMessage) { $Output += $LogInnerMessage } + + Write-Output -InputObject $Output + + If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' } + If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' } + If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' } + If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' } + } + } + End { + } } #endregion @@ -1384,370 +1455,390 @@ Function Resolve-Error { Function Show-InstallationPrompt { <# .SYNOPSIS - Displays a custom installation prompt with the toolkit branding and optional buttons. + Displays a custom installation prompt with the toolkit branding and optional buttons. .DESCRIPTION - Any combination of Left, Middle or Right buttons can be displayed. The return value of the button clicked by the user is the button text specified. + Any combination of Left, Middle or Right buttons can be displayed. The return value of the button clicked by the user is the button text specified. .PARAMETER Title - Title of the prompt. Default: the application installation name. + Title of the prompt. Default: the application installation name. .PARAMETER Message - Message text to be included in the prompt + Message text to be included in the prompt .PARAMETER MessageAlignment - Alignment of the message text. Options: Left, Center, Right. Default: Center. + Alignment of the message text. Options: Left, Center, Right. Default: Center. .PARAMETER ButtonLeftText - Show a button on the left of the prompt with the specified text + Show a button on the left of the prompt with the specified text .PARAMETER ButtonRightText - Show a button on the right of the prompt with the specified text + Show a button on the right of the prompt with the specified text .PARAMETER ButtonMiddleText - Show a button in the middle of the prompt with the specified text + Show a button in the middle of the prompt with the specified text .PARAMETER Icon - Show a system icon in the prompt. Options: Application, Asterisk, Error, Exclamation, Hand, Information, None, Question, Shield, Warning, WinLogo. Default: None. + Show a system icon in the prompt. Options: Application, Asterisk, Error, Exclamation, Hand, Information, None, Question, Shield, Warning, WinLogo. Default: None. .PARAMETER NoWait - Specifies whether to show the prompt asynchronously (i.e. allow the script to continue without waiting for a response). Default: $false. + Specifies whether to show the prompt asynchronously (i.e. allow the script to continue without waiting for a response). Default: $false. .PARAMETER PersistPrompt - Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. The user will have no option but to respond to the prompt - resistance is futile! + Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. The user will have no option but to respond to the prompt - resistance is futile! .PARAMETER MinimizeWindows - Specifies whether to minimize other windows when displaying prompt. Default: $false. + Specifies whether to minimize other windows when displaying prompt. Default: $false. .PARAMETER Timeout - Specifies the time period in seconds after which the prompt should timeout. Default: UI timeout value set in the config XML file. + Specifies the time period in seconds after which the prompt should timeout. Default: UI timeout value set in the config XML file. .PARAMETER ExitOnTimeout - Specifies whether to exit the script if the UI times out. Default: $true. + Specifies whether to exit the script if the UI times out. Default: $true. .EXAMPLE - Show-InstallationPrompt -Message 'Do you want to proceed with the installation?' -ButtonRightText 'Yes' -ButtonLeftText 'No' + Show-InstallationPrompt -Message 'Do you want to proceed with the installation?' -ButtonRightText 'Yes' -ButtonLeftText 'No' .EXAMPLE - Show-InstallationPrompt -Title 'Funny Prompt' -Message 'How are you feeling today?' -ButtonRightText 'Good' -ButtonLeftText 'Bad' -ButtonMiddleText 'Indifferent' + Show-InstallationPrompt -Title 'Funny Prompt' -Message 'How are you feeling today?' -ButtonRightText 'Good' -ButtonLeftText 'Bad' -ButtonMiddleText 'Indifferent' .EXAMPLE - Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install, or remove it completely for unattended installations.' -Icon Information -NoWait + Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install, or remove it completely for unattended installations.' -Icon Information -NoWait .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Title = $installTitle, - [Parameter(Mandatory=$false)] - [string]$Message = '', - [Parameter(Mandatory=$false)] - [ValidateSet('Left','Center','Right')] - [string]$MessageAlignment = 'Center', - [Parameter(Mandatory=$false)] - [string]$ButtonRightText = '', - [Parameter(Mandatory=$false)] - [string]$ButtonLeftText = '', - [Parameter(Mandatory=$false)] - [string]$ButtonMiddleText = '', - [Parameter(Mandatory=$false)] - [ValidateSet('Application','Asterisk','Error','Exclamation','Hand','Information','None','Question','Shield','Warning','WinLogo')] - [string]$Icon = 'None', - [Parameter(Mandatory=$false)] - [switch]$NoWait = $false, - [Parameter(Mandatory=$false)] - [switch]$PersistPrompt = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$MinimizeWindows = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$Timeout = $configInstallationUITimeout, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ExitOnTimeout = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Bypass if in non-interactive mode - If ($deployModeSilent) { - Write-Log -Message "Bypassing Installation Prompt [Mode: $deployMode]... $Message" -Source ${CmdletName} - Return - } - - ## Get parameters for calling function asynchronously - [hashtable]$installPromptParameters = $psBoundParameters - - ## Check if the countdown was specified - If ($timeout -gt $configInstallationUITimeout) { - [string]$CountdownTimeoutErr = "The installation UI dialog timeout cannot be longer than the timeout specified in the XML configuration file." - Write-Log -Message $CountdownTimeoutErr -Severity 3 -Source ${CmdletName} - Throw $CountdownTimeoutErr - } - - [Windows.Forms.Application]::EnableVisualStyles() - $formInstallationPrompt = New-Object -TypeName 'System.Windows.Forms.Form' - $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' - $pictureIcon = New-Object -TypeName 'System.Windows.Forms.PictureBox' - $labelText = New-Object -TypeName 'System.Windows.Forms.Label' - $buttonRight = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonMiddle = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonLeft = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonAbort = New-Object -TypeName 'System.Windows.Forms.Button' - $InitialFormInstallationPromptWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' - - [scriptblock]$Form_Cleanup_FormClosed = { - ## Remove all event handlers from the controls - Try { - $labelText.remove_Click($handler_labelText_Click) - $buttonLeft.remove_Click($buttonLeft_OnClick) - $buttonRight.remove_Click($buttonRight_OnClick) - $buttonMiddle.remove_Click($buttonMiddle_OnClick) - $buttonAbort.remove_Click($buttonAbort_OnClick) - $timer.remove_Tick($timer_Tick) - $timer.Dispose() - $timer = $null - $timerPersist.remove_Tick($timerPersist_Tick) - $timerPersist.Dispose() - $timerPersist = $null - $formInstallationPrompt.remove_Load($Form_StateCorrection_Load) - $formInstallationPrompt.remove_FormClosed($Form_Cleanup_FormClosed) - } - Catch { } - } - - [scriptblock]$Form_StateCorrection_Load = { - ## Correct the initial state of the form to prevent the .NET maximized form issue - $formInstallationPrompt.WindowState = 'Normal' - $formInstallationPrompt.AutoSize = $true - $formInstallationPrompt.TopMost = $true - $formInstallationPrompt.BringToFront() - # Get the start position of the form so we can return the form to this position if PersistPrompt is enabled - Set-Variable -Name 'formInstallationPromptStartPosition' -Value $formInstallationPrompt.Location -Scope 'Script' - } - - ## Form - $formInstallationPrompt.Controls.Add($pictureBanner) - - ##---------------------------------------------- - ## Create padding object - $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' - $paddingNone.Top = 0 - $paddingNone.Bottom = 0 - $paddingNone.Left = 0 - $paddingNone.Right = 0 - - ## Generic Button properties - $buttonWidth = 110 - $buttonHeight = 23 - $buttonPadding = 50 - $buttonSize = New-Object -TypeName 'System.Drawing.Size' - $buttonSize.Width = $buttonWidth - $buttonSize.Height = $buttonHeight - $buttonPadding = New-Object -TypeName 'System.Windows.Forms.Padding' - $buttonPadding.Top = 0 - $buttonPadding.Bottom = 5 - $buttonPadding.Left = 50 - $buttonPadding.Right = 0 - - ## Picture Banner - $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 - $pictureBanner.ImageLocation = $appDeployLogoBanner - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = 0 - $pictureBanner.Location = $System_Drawing_Point - $pictureBanner.Name = 'pictureBanner' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = $appDeployLogoBannerHeight - $System_Drawing_Size.Width = 450 - $pictureBanner.Size = $System_Drawing_Size - $pictureBanner.SizeMode = 'CenterImage' - $pictureBanner.Margin = $paddingNone - $pictureBanner.TabIndex = 0 - $pictureBanner.TabStop = $false - - ## Picture Icon - $pictureIcon.DataBindings.DefaultDataSourceUpdateMode = 0 - If ($icon -ne 'None') { $pictureIcon.Image = ([Drawing.SystemIcons]::$Icon).ToBitmap() } - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 15 - $System_Drawing_Point.Y = 105 + $appDeployLogoBannerHeightDifference - $pictureIcon.Location = $System_Drawing_Point - $pictureIcon.Name = 'pictureIcon' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 32 - $System_Drawing_Size.Width = 32 - $pictureIcon.Size = $System_Drawing_Size - $pictureIcon.AutoSize = $true - $pictureIcon.Margin = $paddingNone - $pictureIcon.TabIndex = 0 - $pictureIcon.TabStop = $false - - ## Label Text - $labelText.DataBindings.DefaultDataSourceUpdateMode = 0 - $labelText.Name = 'labelText' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 148 - $System_Drawing_Size.Width = 385 - $labelText.Size = $System_Drawing_Size - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 25 - $System_Drawing_Point.Y = $appDeployLogoBannerHeight - $labelText.Location = $System_Drawing_Point - $labelText.Margin = '0,0,0,0' - $labelText.Padding = '40,0,20,0' - $labelText.TabIndex = 1 - $labelText.Text = $message - $labelText.TextAlign = "Middle$($MessageAlignment)" - $labelText.Anchor = 'Top' - $labelText.add_Click($handler_labelText_Click) - - # Generic Y location for buttons - $buttonLocationY = 200 + $appDeployLogoBannerHeightDifference - - ## Button Left - $buttonLeft.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonLeft.Location = "15,$buttonLocationY" - $buttonLeft.Name = 'buttonLeft' - $buttonLeft.Size = $buttonSize - $buttonLeft.TabIndex = 5 - $buttonLeft.Text = $buttonLeftText - $buttonLeft.DialogResult = 'No' - $buttonLeft.AutoSize = $false - $buttonLeft.UseVisualStyleBackColor = $true - $buttonLeft.add_Click($buttonLeft_OnClick) - - ## Button Middle - $buttonMiddle.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonMiddle.Location = "170,$buttonLocationY" - $buttonMiddle.Name = 'buttonMiddle' - $buttonMiddle.Size = $buttonSize - $buttonMiddle.TabIndex = 6 - $buttonMiddle.Text = $buttonMiddleText - $buttonMiddle.DialogResult = 'Ignore' - $buttonMiddle.AutoSize = $true - $buttonMiddle.UseVisualStyleBackColor = $true - $buttonMiddle.add_Click($buttonMiddle_OnClick) - - ## Button Right - $buttonRight.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonRight.Location = "325,$buttonLocationY" - $buttonRight.Name = 'buttonRight' - $buttonRight.Size = $buttonSize - $buttonRight.TabIndex = 7 - $buttonRight.Text = $ButtonRightText - $buttonRight.DialogResult = 'Yes' - $buttonRight.AutoSize = $true - $buttonRight.UseVisualStyleBackColor = $true - $buttonRight.add_Click($buttonRight_OnClick) - - ## Button Abort (Hidden) - $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonAbort.Name = 'buttonAbort' - $buttonAbort.Size = '1,1' - $buttonAbort.DialogResult = 'Abort' - $buttonAbort.TabStop = $false - $buttonAbort.UseVisualStyleBackColor = $true - $buttonAbort.add_Click($buttonAbort_OnClick) - - ## Form Installation Prompt - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 270 + $appDeployLogoBannerHeightDifference - $System_Drawing_Size.Width = 450 - $formInstallationPrompt.Size = $System_Drawing_Size - $formInstallationPrompt.Padding = '0,0,0,10' - $formInstallationPrompt.Margin = $paddingNone - $formInstallationPrompt.DataBindings.DefaultDataSourceUpdateMode = 0 - $formInstallationPrompt.Name = 'WelcomeForm' - $formInstallationPrompt.Text = $title - $formInstallationPrompt.StartPosition = 'CenterScreen' - $formInstallationPrompt.FormBorderStyle = 'FixedDialog' - $formInstallationPrompt.MaximizeBox = $false - $formInstallationPrompt.MinimizeBox = $false - $formInstallationPrompt.TopMost = $true - $formInstallationPrompt.TopLevel = $true - $formInstallationPrompt.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - $formInstallationPrompt.Controls.Add($pictureBanner) - $formInstallationPrompt.Controls.Add($pictureIcon) - $formInstallationPrompt.Controls.Add($labelText) - $formInstallationPrompt.Controls.Add($buttonAbort) - If ($buttonLeftText) { $formInstallationPrompt.Controls.Add($buttonLeft) } - If ($buttonMiddleText) { $formInstallationPrompt.Controls.Add($buttonMiddle) } - If ($buttonRightText) { $formInstallationPrompt.Controls.Add($buttonRight) } - - ## Timer - $timer = New-Object -TypeName 'System.Windows.Forms.Timer' - $timer.Interval = ($timeout * 1000) - $timer.Add_Tick({ - Write-Log -Message 'Installation action not taken within a reasonable amount of time.' -Source ${CmdletName} - $buttonAbort.PerformClick() - }) - - ## Save the initial state of the form - $InitialFormInstallationPromptWindowState = $formInstallationPrompt.WindowState - ## Init the OnLoad event to correct the initial state of the form - $formInstallationPrompt.add_Load($Form_StateCorrection_Load) - ## Clean up the control events - $formInstallationPrompt.add_FormClosed($Form_Cleanup_FormClosed) - - ## Start the timer - $timer.Start() - - ## Persistence Timer - [scriptblock]$RefreshInstallationPrompt = { - $formInstallationPrompt.BringToFront() - $formInstallationPrompt.Location = "$($formInstallationPromptStartPosition.X),$($formInstallationPromptStartPosition.Y)" - $formInstallationPrompt.Refresh() - } - If ($persistPrompt) { - $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' - $timerPersist.Interval = ($configInstallationPersistInterval * 1000) - [scriptblock]$timerPersist_Tick = { & $RefreshInstallationPrompt } - $timerPersist.add_Tick($timerPersist_Tick) - $timerPersist.Start() - } - - ## Close the Installation Progress Dialog if running - Close-InstallationProgress - - [string]$installPromptLoggedParameters = ($installPromptParameters.GetEnumerator() | ForEach-Object { If ($_.Value.GetType().Name -eq 'SwitchParameter') { "-$($_.Key):`$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Boolean') { "-$($_.Key) `$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Int32') { "-$($_.Key) $($_.Value)" } Else { "-$($_.Key) `"$($_.Value)`"" } }) -join ' ' - Write-Log -Message "Displaying custom installation prompt with the non-default parameters: [$installPromptLoggedParameters]." -Source ${CmdletName} - - ## If the NoWait parameter is specified, launch a new PowerShell session to show the prompt asynchronously - If ($NoWait) { - # Remove the NoWait parameter so that the script is run synchronously in the new PowerShell session - $installPromptParameters.Remove('NoWait') - # Format the parameters as a string - [string]$installPromptParameters = ($installPromptParameters.GetEnumerator() | ForEach-Object { If ($_.Value.GetType().Name -eq 'SwitchParameter') { "-$($_.Key):`$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Boolean') { "-$($_.Key) `$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Int32') { "-$($_.Key) $($_.Value)" } Else { "-$($_.Key) `"$($_.Value)`"" } }) -join ' ' - Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$scriptPath`" -ReferredInstallTitle `"$Title`" -ReferredInstallName `"$installName`" -ReferredLogName `"$logName`" -ShowInstallationPrompt $installPromptParameters -AsyncToolkitLaunch" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' - } - ## Otherwise, show the prompt synchronously. If user cancels, then keep showing it until user responds using one of the buttons. - Else { - $showDialog = $true - While ($showDialog) { - # Minimize all other windows - If ($minimizeWindows) { $null = $shellApp.MinimizeAll() } - # Show the Form - $result = $formInstallationPrompt.ShowDialog() - If (($result -eq 'Yes') -or ($result -eq 'No') -or ($result -eq 'Ignore') -or ($result -eq 'Abort')) { - $showDialog = $false - } - } - $formInstallationPrompt.Dispose() - - Switch ($result) { - 'Yes' { Write-Output -InputObject $buttonRightText } - 'No' { Write-Output -InputObject $buttonLeftText } - 'Ignore' { Write-Output -InputObject $buttonMiddleText } - 'Abort' { - # Restore minimized windows - $null = $shellApp.UndoMinimizeAll() - If ($ExitOnTimeout) { - Exit-Script -ExitCode $configInstallationUIExitCode - } - Else { - Write-Log -Message 'UI timed out but `$ExitOnTimeout set to `$false. Continue...' -Source ${CmdletName} - } - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Title = $installTitle, + [Parameter(Mandatory=$false)] + [string]$Message = '', + [Parameter(Mandatory=$false)] + [ValidateSet('Left','Center','Right')] + [string]$MessageAlignment = 'Center', + [Parameter(Mandatory=$false)] + [string]$ButtonRightText = '', + [Parameter(Mandatory=$false)] + [string]$ButtonLeftText = '', + [Parameter(Mandatory=$false)] + [string]$ButtonMiddleText = '', + [Parameter(Mandatory=$false)] + [ValidateSet('Application','Asterisk','Error','Exclamation','Hand','Information','None','Question','Shield','Warning','WinLogo')] + [string]$Icon = 'None', + [Parameter(Mandatory=$false)] + [switch]$NoWait = $false, + [Parameter(Mandatory=$false)] + [switch]$PersistPrompt = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$MinimizeWindows = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$Timeout = $configInstallationUITimeout, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ExitOnTimeout = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Bypass if in non-interactive mode + If ($deployModeSilent) { + Write-Log -Message "Bypassing Installation Prompt [Mode: $deployMode]... $Message" -Source ${CmdletName} + Return + } + + ## Get parameters for calling function asynchronously + [hashtable]$installPromptParameters = $psBoundParameters + + ## Check if the countdown was specified + If ($timeout -gt $configInstallationUITimeout) { + [string]$CountdownTimeoutErr = "The installation UI dialog timeout cannot be longer than the timeout specified in the XML configuration file." + Write-Log -Message $CountdownTimeoutErr -Severity 3 -Source ${CmdletName} + Throw $CountdownTimeoutErr + } + + [Windows.Forms.Application]::EnableVisualStyles() + $formInstallationPrompt = New-Object -TypeName 'System.Windows.Forms.Form' + $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' + If ($Icon -ne 'None') { + $pictureIcon = New-Object -TypeName 'System.Windows.Forms.PictureBox' + } + $labelText = New-Object -TypeName 'System.Windows.Forms.Label' + $buttonRight = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonMiddle = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonLeft = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonAbort = New-Object -TypeName 'System.Windows.Forms.Button' + $flowLayoutPanel = New-Object -TypeName 'System.Windows.Forms.FlowLayoutPanel' + $panelButtons = New-Object -TypeName 'System.Windows.Forms.Panel' + $InitialFormInstallationPromptWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' + + [scriptblock]$Form_Cleanup_FormClosed = { + ## Remove all event handlers from the controls + Try { + $labelText.remove_Click($handler_labelText_Click) + $buttonLeft.remove_Click($buttonLeft_OnClick) + $buttonRight.remove_Click($buttonRight_OnClick) + $buttonMiddle.remove_Click($buttonMiddle_OnClick) + $buttonAbort.remove_Click($buttonAbort_OnClick) + $timer.remove_Tick($timer_Tick) + $timer.Dispose() + $timer = $null + $timerPersist.remove_Tick($timerPersist_Tick) + $timerPersist.Dispose() + $timerPersist = $null + $formInstallationPrompt.remove_Load($Form_StateCorrection_Load) + $formInstallationPrompt.remove_FormClosed($Form_Cleanup_FormClosed) + } + Catch { } + } + + [scriptblock]$Form_StateCorrection_Load = { + ## Correct the initial state of the form to prevent the .NET maximized form issue + $formInstallationPrompt.WindowState = 'Normal' + $formInstallationPrompt.AutoSize = $true + $formInstallationPrompt.TopMost = $true + $formInstallationPrompt.BringToFront() + # Get the start position of the form so we can return the form to this position if PersistPrompt is enabled + Set-Variable -Name 'formInstallationPromptStartPosition' -Value $formInstallationPrompt.Location -Scope 'Script' + } + + ## Form + $formInstallationPrompt.Controls.Add($pictureBanner) + + ##---------------------------------------------- + ## Create padding object + $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,0 + + ## Default control size + $DefaultControlSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,0 + + ## Generic Button properties + $buttonSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 110,24 + + ## Picture Banner + $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 + $pictureBanner.ImageLocation = $appDeployLogoBanner + $pictureBanner.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,$appDeployLogoBannerHeight + $pictureBanner.MinimumSize = $DefaultControlSize + $pictureBanner.SizeMode = 'CenterImage' + $pictureBanner.Margin = $paddingNone + $pictureBanner.TabIndex = 0 + $pictureBanner.TabStop = $false + $pictureBanner.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + + ## Picture Icon + If ($Icon -ne 'None') { + $pictureIcon.DataBindings.DefaultDataSourceUpdateMode = 0 + $pictureIcon.Image = ([Drawing.SystemIcons]::$Icon).ToBitmap() + $pictureIcon.Name = 'pictureIcon' + $pictureIcon.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 64,32 + $pictureIcon.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 64,32 + $pictureIcon.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 24,0,8,0 + $pictureIcon.SizeMode = "CenterImage" + $pictureIcon.TabIndex = 0 + $pictureIcon.TabStop = $false + $pictureIcon.Anchor = 'None' + $pictureIcon.Margin = $paddingNone + } + + ## Label Text + $labelText.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelText.Name = 'labelText' + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' 386,0 + $labelText.Size = $System_Drawing_Size + $labelText.MinimumSize = $System_Drawing_Size + $labelText.MaximumSize = $System_Drawing_Size + $labelText.AutoSize = $true + $labelText.Margin = $paddingNone + $labelText.TabIndex = 1 + $labelText.Text = $message + $labelText.TextAlign = "Middle$($MessageAlignment)" + $labelText.Anchor = 'None' + $labelText.add_Click($handler_labelText_Click) + + If ($Icon -ne 'None') { + # Add margin for the icon based on labelText Height so its centered + $pictureIcon.Height = $labelText.Height + } + ## Button Left + $buttonLeft.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonLeft.Name = 'buttonLeft' + $buttonLeft.Size = $buttonSize + $buttonLeft.TabIndex = 5 + $buttonLeft.Text = $buttonLeftText + $buttonLeft.DialogResult = 'No' + $buttonLeft.AutoSize = $false + $buttonLeft.UseVisualStyleBackColor = $true + $buttonLeft.Location = "15,5" + $buttonLeft.add_Click($buttonLeft_OnClick) + + ## Button Middle + $buttonMiddle.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonMiddle.Name = 'buttonMiddle' + $buttonMiddle.Size = $buttonSize + $buttonMiddle.TabIndex = 6 + $buttonMiddle.Text = $buttonMiddleText + $buttonMiddle.DialogResult = 'Ignore' + $buttonMiddle.AutoSize = $true + $buttonMiddle.UseVisualStyleBackColor = $true + $buttonMiddle.Location = "170,5" + $buttonMiddle.add_Click($buttonMiddle_OnClick) + + ## Button Right + $buttonRight.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonRight.Name = 'buttonRight' + $buttonRight.Size = $buttonSize + $buttonRight.TabIndex = 7 + $buttonRight.Text = $ButtonRightText + $buttonRight.DialogResult = 'Yes' + $buttonRight.AutoSize = $true + $buttonRight.UseVisualStyleBackColor = $true + $buttonRight.Location = "325,5" + $buttonRight.add_Click($buttonRight_OnClick) + + ## Button Abort (Hidden) + $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonAbort.Name = 'buttonAbort' + $buttonAbort.Size = '1,1' + $buttonAbort.DialogResult = 'Abort' + $buttonAbort.TabStop = $false + $buttonAbort.Visible = $false + $buttonAbort.UseVisualStyleBackColor = $true + $buttonAbort.Location = "0,0" + $buttonAbort.add_Click($buttonAbort_OnClick) + + ## FlowLayoutPanel + $flowLayoutPanel.MinimumSize = $DefaultControlSize + $flowLayoutPanel.MaximumSize = $DefaultControlSize + $flowLayoutPanel.Size = $DefaultControlSize + $flowLayoutPanel.AutoSize = $true + $flowLayoutPanel.Anchor = 'Top,Left' + $flowLayoutPanel.FlowDirection = 'LeftToRight' + $flowLayoutPanel.WrapContents = $true + $flowLayoutPanel.Margin = $paddingNone + If ($Icon -ne 'None') { + $flowLayoutPanel.Controls.Add($pictureIcon) + } + $flowLayoutPanel.Controls.Add($labelText) + ## Make sure label text is positioned correctly + If ($Icon -ne 'None') { + $labelText.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,5,10,5 + $pictureIcon.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + $labelText.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 64,0 + } else { + $labelText.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,5,10,5 + $labelText.MinimumSize = $DefaultControlSize + $labelText.MaximumSize = $DefaultControlSize + $labelText.Size = $DefaultControlSize + $labelText.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + } + $flowLayoutPanel.Controls.Add($buttonAbort) + $flowLayoutPanel.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,$appDeployLogoBannerHeight + + ## ButtonsPanel + $panelButtons.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.Padding = $paddingNone + $panelButtons.Margin = $paddingNone + $panelButtons.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.AutoSize = $true + If ($buttonLeftText) { $panelButtons.Controls.Add($buttonLeft) } + If ($buttonMiddleText) { $panelButtons.Controls.Add($buttonMiddle) } + If ($buttonRightText) { $panelButtons.Controls.Add($buttonRight) } + ## Add the ButtonsPanel to the flowLayoutPanel if any buttons are present + If ($buttonLeftText -or $buttonMiddleText -or $buttonRightText) { + $flowLayoutPanel.Controls.Add($panelButtons) + } + + ## Form Installation Prompt + $formInstallationPrompt.MinimumSize = $DefaultControlSize + $formInstallationPrompt.Size = $DefaultControlSize + $formInstallationPrompt.Padding = $paddingNone + $formInstallationPrompt.Margin = $paddingNone + $formInstallationPrompt.DataBindings.DefaultDataSourceUpdateMode = 0 + $formInstallationPrompt.Name = 'InstallPromptForm' + $formInstallationPrompt.Text = $title + $formInstallationPrompt.StartPosition = 'CenterScreen' + $formInstallationPrompt.FormBorderStyle = 'FixedDialog' + $formInstallationPrompt.MaximizeBox = $false + $formInstallationPrompt.MinimizeBox = $false + $formInstallationPrompt.TopMost = $true + $formInstallationPrompt.TopLevel = $true + $formInstallationPrompt.AutoSize = $true + $formInstallationPrompt.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + $formInstallationPrompt.Controls.Add($pictureBanner) + $formInstallationPrompt.Controls.Add($flowLayoutPanel) + ## Timer + $timer = New-Object -TypeName 'System.Windows.Forms.Timer' + $timer.Interval = ($timeout * 1000) + $timer.Add_Tick({ + Write-Log -Message 'Installation action not taken within a reasonable amount of time.' -Source ${CmdletName} + $buttonAbort.PerformClick() + }) + + ## Save the initial state of the form + $InitialFormInstallationPromptWindowState = $formInstallationPrompt.WindowState + ## Init the OnLoad event to correct the initial state of the form + $formInstallationPrompt.add_Load($Form_StateCorrection_Load) + ## Clean up the control events + $formInstallationPrompt.add_FormClosed($Form_Cleanup_FormClosed) + + ## Start the timer + $timer.Start() + + ## Persistence Timer + [scriptblock]$RefreshInstallationPrompt = { + $formInstallationPrompt.BringToFront() + $formInstallationPrompt.Location = "$($formInstallationPromptStartPosition.X),$($formInstallationPromptStartPosition.Y)" + $formInstallationPrompt.Refresh() + } + If ($persistPrompt) { + $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' + $timerPersist.Interval = ($configInstallationPersistInterval * 1000) + [scriptblock]$timerPersist_Tick = { & $RefreshInstallationPrompt } + $timerPersist.add_Tick($timerPersist_Tick) + $timerPersist.Start() + } + + ## Close the Installation Progress Dialog if running + Close-InstallationProgress + + [string]$installPromptLoggedParameters = ($installPromptParameters.GetEnumerator() | ForEach-Object { If ($_.Value.GetType().Name -eq 'SwitchParameter') { "-$($_.Key):`$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Boolean') { "-$($_.Key) `$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Int32') { "-$($_.Key) $($_.Value)" } Else { "-$($_.Key) `"$($_.Value)`"" } }) -join ' ' + Write-Log -Message "Displaying custom installation prompt with the non-default parameters: [$installPromptLoggedParameters]." -Source ${CmdletName} + + ## If the NoWait parameter is specified, launch a new PowerShell session to show the prompt asynchronously + If ($NoWait) { + # Remove the NoWait parameter so that the script is run synchronously in the new PowerShell session + $installPromptParameters.Remove('NoWait') + # Format the parameters as a string + [string]$installPromptParameters = ($installPromptParameters.GetEnumerator() | ForEach-Object { If ($_.Value.GetType().Name -eq 'SwitchParameter') { "-$($_.Key):`$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Boolean') { "-$($_.Key) `$" + "$($_.Value)".ToLower() } ElseIf ($_.Value.GetType().Name -eq 'Int32') { "-$($_.Key) $($_.Value)" } Else { "-$($_.Key) `"$($_.Value)`"" } }) -join ' ' + Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$scriptPath`" -ReferredInstallTitle `"$Title`" -ReferredInstallName `"$installName`" -ReferredLogName `"$logName`" -ShowInstallationPrompt $installPromptParameters -AsyncToolkitLaunch" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' + } + ## Otherwise, show the prompt synchronously. If user cancels, then keep showing it until user responds using one of the buttons. + Else { + $showDialog = $true + While ($showDialog) { + # Minimize all other windows + If ($minimizeWindows) { $null = $shellApp.MinimizeAll() } + # Show the Form + $result = $formInstallationPrompt.ShowDialog() + If (($result -eq 'Yes') -or ($result -eq 'No') -or ($result -eq 'Ignore') -or ($result -eq 'Abort')) { + $showDialog = $false + } + } + $formInstallationPrompt.Dispose() + + Switch ($result) { + 'Yes' { Write-Output -InputObject $buttonRightText } + 'No' { Write-Output -InputObject $buttonLeftText } + 'Ignore' { Write-Output -InputObject $buttonMiddleText } + 'Abort' { + # Restore minimized windows + $null = $shellApp.UndoMinimizeAll() + If ($ExitOnTimeout) { + Exit-Script -ExitCode $configInstallationUIExitCode + } + Else { + Write-Log -Message 'UI timed out but `$ExitOnTimeout set to `$false. Continue...' -Source ${CmdletName} + } + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -1756,147 +1847,147 @@ Function Show-InstallationPrompt { Function Show-DialogBox { <# .SYNOPSIS - Display a custom dialog box with optional title, buttons, icon and timeout. - Show-InstallationPrompt is recommended over this function as it provides more customization and uses consistent branding with the other UI components. + Display a custom dialog box with optional title, buttons, icon and timeout. + Show-InstallationPrompt is recommended over this function as it provides more customization and uses consistent branding with the other UI components. .DESCRIPTION - Display a custom dialog box with optional title, buttons, icon and timeout. The default button is "OK", the default Icon is "None", and the default Timeout is none. + Display a custom dialog box with optional title, buttons, icon and timeout. The default button is "OK", the default Icon is "None", and the default Timeout is none. .PARAMETER Text - Text in the message dialog box + Text in the message dialog box .PARAMETER Title - Title of the message dialog box + Title of the message dialog box .PARAMETER Buttons - Buttons to be included on the dialog box. Options: OK, OKCancel, AbortRetryIgnore, YesNoCancel, YesNo, RetryCancel, CancelTryAgainContinue. Default: OK. + Buttons to be included on the dialog box. Options: OK, OKCancel, AbortRetryIgnore, YesNoCancel, YesNo, RetryCancel, CancelTryAgainContinue. Default: OK. .PARAMETER DefaultButton - The Default button that is selected. Options: First, Second, Third. Default: First. + The Default button that is selected. Options: First, Second, Third. Default: First. .PARAMETER Icon - Icon to display on the dialog box. Options: None, Stop, Question, Exclamation, Information. Default: None. + Icon to display on the dialog box. Options: None, Stop, Question, Exclamation, Information. Default: None. .PARAMETER Timeout - Timeout period in seconds before automatically closing the dialog box with the return message "Timeout". Default: UI timeout value set in the config XML file. + Timeout period in seconds before automatically closing the dialog box with the return message "Timeout". Default: UI timeout value set in the config XML file. .PARAMETER TopMost - Specifies whether the message box is a system modal message box and appears in a topmost window. Default: $true. + Specifies whether the message box is a system modal message box and appears in a topmost window. Default: $true. .EXAMPLE - Show-DialogBox -Title 'Installed Complete' -Text 'Installation has completed. Please click OK and restart your computer.' -Icon 'Information' + Show-DialogBox -Title 'Installed Complete' -Text 'Installation has completed. Please click OK and restart your computer.' -Icon 'Information' .EXAMPLE - Show-DialogBox -Title 'Installation Notice' -Text 'Installation will take approximately 30 minutes. Do you wish to proceed?' -Buttons 'OKCancel' -DefaultButton 'Second' -Icon 'Exclamation' -Timeout 600 -Topmost $false + Show-DialogBox -Title 'Installation Notice' -Text 'Installation will take approximately 30 minutes. Do you wish to proceed?' -Buttons 'OKCancel' -DefaultButton 'Second' -Icon 'Exclamation' -Timeout 600 -Topmost $false .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,Position=0,HelpMessage='Enter a message for the dialog box')] - [ValidateNotNullorEmpty()] - [string]$Text, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Title = $installTitle, - [Parameter(Mandatory=$false)] - [ValidateSet('OK','OKCancel','AbortRetryIgnore','YesNoCancel','YesNo','RetryCancel','CancelTryAgainContinue')] - [string]$Buttons = 'OK', - [Parameter(Mandatory=$false)] - [ValidateSet('First','Second','Third')] - [string]$DefaultButton = 'First', - [Parameter(Mandatory=$false)] - [ValidateSet('Exclamation','Information','None','Stop','Question')] - [string]$Icon = 'None', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Timeout = $configInstallationUITimeout, - [Parameter(Mandatory=$false)] - [boolean]$TopMost = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - # Bypass if in non-interactive mode - If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Dialog Box [Mode: $deployMode]: $Text..." -Source ${CmdletName} - Return - } - - Write-Log -Message "Display Dialog Box with message: $Text..." -Source ${CmdletName} - - [hashtable]$dialogButtons = @{ - 'OK' = 0 - 'OKCancel' = 1 - 'AbortRetryIgnore' = 2 - 'YesNoCancel' = 3 - 'YesNo' = 4 - 'RetryCancel' = 5 - 'CancelTryAgainContinue' = 6 - } - - [hashtable]$dialogIcons = @{ - 'None' = 0 - 'Stop' = 16 - 'Question' = 32 - 'Exclamation' = 48 - 'Information' = 64 - } - - [hashtable]$dialogDefaultButton = @{ - 'First' = 0 - 'Second' = 256 - 'Third' = 512 - } - - Switch ($TopMost) { - $true { $dialogTopMost = 4096 } - $false { $dialogTopMost = 0 } - } - - $response = $Shell.Popup($Text, $Timeout, $Title, ($dialogButtons[$Buttons] + $dialogIcons[$Icon] + $dialogDefaultButton[$DefaultButton] + $dialogTopMost)) - - Switch ($response) { - 1 { - Write-Log -Message 'Dialog Box Response: OK' -Source ${CmdletName} - Write-Output -InputObject 'OK' - } - 2 { - Write-Log -Message 'Dialog Box Response: Cancel' -Source ${CmdletName} - Write-Output -InputObject 'Cancel' - } - 3 { - Write-Log -Message 'Dialog Box Response: Abort' -Source ${CmdletName} - Write-Output -InputObject 'Abort' - } - 4 { - Write-Log -Message 'Dialog Box Response: Retry' -Source ${CmdletName} - Write-Output -InputObject 'Retry' - } - 5 { - Write-Log -Message 'Dialog Box Response: Ignore' -Source ${CmdletName} - Write-Output -InputObject 'Ignore' - } - 6 { - Write-Log -Message 'Dialog Box Response: Yes' -Source ${CmdletName} - Write-Output -InputObject 'Yes' - } - 7 { - Write-Log -Message 'Dialog Box Response: No' -Source ${CmdletName} - Write-Output -InputObject 'No' - } - 10 { - Write-Log -Message 'Dialog Box Response: Try Again' -Source ${CmdletName} - Write-Output -InputObject 'Try Again' - } - 11 { - Write-Log -Message 'Dialog Box Response: Continue' -Source ${CmdletName} - Write-Output -InputObject 'Continue' - } - -1 { - Write-Log -Message 'Dialog Box Timed Out...' -Source ${CmdletName} - Write-Output -InputObject 'Timeout' - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,Position=0,HelpMessage='Enter a message for the dialog box')] + [ValidateNotNullorEmpty()] + [string]$Text, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Title = $installTitle, + [Parameter(Mandatory=$false)] + [ValidateSet('OK','OKCancel','AbortRetryIgnore','YesNoCancel','YesNo','RetryCancel','CancelTryAgainContinue')] + [string]$Buttons = 'OK', + [Parameter(Mandatory=$false)] + [ValidateSet('First','Second','Third')] + [string]$DefaultButton = 'First', + [Parameter(Mandatory=$false)] + [ValidateSet('Exclamation','Information','None','Stop','Question')] + [string]$Icon = 'None', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Timeout = $configInstallationUITimeout, + [Parameter(Mandatory=$false)] + [boolean]$TopMost = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + # Bypass if in non-interactive mode + If ($deployModeNonInteractive) { + Write-Log -Message "Bypassing Dialog Box [Mode: $deployMode]: $Text..." -Source ${CmdletName} + Return + } + + Write-Log -Message "Display Dialog Box with message: $Text..." -Source ${CmdletName} + + [hashtable]$dialogButtons = @{ + 'OK' = 0 + 'OKCancel' = 1 + 'AbortRetryIgnore' = 2 + 'YesNoCancel' = 3 + 'YesNo' = 4 + 'RetryCancel' = 5 + 'CancelTryAgainContinue' = 6 + } + + [hashtable]$dialogIcons = @{ + 'None' = 0 + 'Stop' = 16 + 'Question' = 32 + 'Exclamation' = 48 + 'Information' = 64 + } + + [hashtable]$dialogDefaultButton = @{ + 'First' = 0 + 'Second' = 256 + 'Third' = 512 + } + + Switch ($TopMost) { + $true { $dialogTopMost = 4096 } + $false { $dialogTopMost = 0 } + } + + $response = $Shell.Popup($Text, $Timeout, $Title, ($dialogButtons[$Buttons] + $dialogIcons[$Icon] + $dialogDefaultButton[$DefaultButton] + $dialogTopMost)) + + Switch ($response) { + 1 { + Write-Log -Message 'Dialog Box Response: OK' -Source ${CmdletName} + Write-Output -InputObject 'OK' + } + 2 { + Write-Log -Message 'Dialog Box Response: Cancel' -Source ${CmdletName} + Write-Output -InputObject 'Cancel' + } + 3 { + Write-Log -Message 'Dialog Box Response: Abort' -Source ${CmdletName} + Write-Output -InputObject 'Abort' + } + 4 { + Write-Log -Message 'Dialog Box Response: Retry' -Source ${CmdletName} + Write-Output -InputObject 'Retry' + } + 5 { + Write-Log -Message 'Dialog Box Response: Ignore' -Source ${CmdletName} + Write-Output -InputObject 'Ignore' + } + 6 { + Write-Log -Message 'Dialog Box Response: Yes' -Source ${CmdletName} + Write-Output -InputObject 'Yes' + } + 7 { + Write-Log -Message 'Dialog Box Response: No' -Source ${CmdletName} + Write-Output -InputObject 'No' + } + 10 { + Write-Log -Message 'Dialog Box Response: Try Again' -Source ${CmdletName} + Write-Output -InputObject 'Try Again' + } + 11 { + Write-Log -Message 'Dialog Box Response: Continue' -Source ${CmdletName} + Write-Output -InputObject 'Continue' + } + -1 { + Write-Log -Message 'Dialog Box Timed Out...' -Source ${CmdletName} + Write-Output -InputObject 'Timeout' + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -1905,56 +1996,56 @@ Function Show-DialogBox { Function Get-HardwarePlatform { <# .SYNOPSIS - Retrieves information about the hardware platform (physical or virtual) + Retrieves information about the hardware platform (physical or virtual) .DESCRIPTION - Retrieves information about the hardware platform (physical or virtual) + Retrieves information about the hardware platform (physical or virtual) .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-HardwarePlatform + Get-HardwarePlatform .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Retrieve hardware platform information.' -Source ${CmdletName} - $hwBios = Get-WmiObject -Class 'Win32_BIOS' -ErrorAction 'Stop' | Select-Object -Property 'Version', 'SerialNumber' - $hwMakeModel = Get-WMIObject -Class 'Win32_ComputerSystem' -ErrorAction 'Stop' | Select-Object -Property 'Model', 'Manufacturer' - - If ($hwBIOS.Version -match 'VRTUAL') { $hwType = 'Virtual:Hyper-V' } - ElseIf ($hwBIOS.Version -match 'A M I') { $hwType = 'Virtual:Virtual PC' } - ElseIf ($hwBIOS.Version -like '*Xen*') { $hwType = 'Virtual:Xen' } - ElseIf ($hwBIOS.SerialNumber -like '*VMware*') { $hwType = 'Virtual:VMWare' } - ElseIf (($hwMakeModel.Manufacturer -like '*Microsoft*') -and ($hwMakeModel.Model -notlike '*Surface*')) { $hwType = 'Virtual:Hyper-V' } - ElseIf ($hwMakeModel.Manufacturer -like '*VMWare*') { $hwType = 'Virtual:VMWare' } - ElseIf ($hwMakeModel.Model -like '*Virtual*') { $hwType = 'Virtual' } - Else { $hwType = 'Physical' } - - Write-Output -InputObject $hwType - } - Catch { - Write-Log -Message "Failed to retrieve hardware platform information. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to retrieve hardware platform information: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Retrieve hardware platform information.' -Source ${CmdletName} + $hwBios = Get-WmiObject -Class 'Win32_BIOS' -ErrorAction 'Stop' | Select-Object -Property 'Version', 'SerialNumber' + $hwMakeModel = Get-WMIObject -Class 'Win32_ComputerSystem' -ErrorAction 'Stop' | Select-Object -Property 'Model', 'Manufacturer' + + If ($hwBIOS.Version -match 'VRTUAL') { $hwType = 'Virtual:Hyper-V' } + ElseIf ($hwBIOS.Version -match 'A M I') { $hwType = 'Virtual:Virtual PC' } + ElseIf ($hwBIOS.Version -like '*Xen*') { $hwType = 'Virtual:Xen' } + ElseIf ($hwBIOS.SerialNumber -like '*VMware*') { $hwType = 'Virtual:VMWare' } + ElseIf (($hwMakeModel.Manufacturer -like '*Microsoft*') -and ($hwMakeModel.Model -notlike '*Surface*')) { $hwType = 'Virtual:Hyper-V' } + ElseIf ($hwMakeModel.Manufacturer -like '*VMWare*') { $hwType = 'Virtual:VMWare' } + ElseIf ($hwMakeModel.Model -like '*Virtual*') { $hwType = 'Virtual' } + Else { $hwType = 'Physical' } + + Write-Output -InputObject $hwType + } + Catch { + Write-Log -Message "Failed to retrieve hardware platform information. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to retrieve hardware platform information: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -1963,53 +2054,53 @@ Function Get-HardwarePlatform { Function Get-FreeDiskSpace { <# .SYNOPSIS - Retrieves the free disk space in MB on a particular drive (defaults to system drive) + Retrieves the free disk space in MB on a particular drive (defaults to system drive) .DESCRIPTION - Retrieves the free disk space in MB on a particular drive (defaults to system drive) + Retrieves the free disk space in MB on a particular drive (defaults to system drive) .PARAMETER Drive - Drive to check free disk space on + Drive to check free disk space on .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-FreeDiskSpace -Drive 'C:' + Get-FreeDiskSpace -Drive 'C:' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Drive = $envSystemDrive, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Retrieve free disk space for drive [$Drive]." -Source ${CmdletName} - $disk = Get-WmiObject -Class 'Win32_LogicalDisk' -Filter "DeviceID='$Drive'" -ErrorAction 'Stop' - [double]$freeDiskSpace = [math]::Round($disk.FreeSpace / 1MB) - - Write-Log -Message "Free disk space for drive [$Drive]: [$freeDiskSpace MB]." -Source ${CmdletName} - Write-Output -InputObject $freeDiskSpace - } - Catch { - Write-Log -Message "Failed to retrieve free disk space for drive [$Drive]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to retrieve free disk space for drive [$Drive]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Drive = $envSystemDrive, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Retrieve free disk space for drive [$Drive]." -Source ${CmdletName} + $disk = Get-WmiObject -Class 'Win32_LogicalDisk' -Filter "DeviceID='$Drive'" -ErrorAction 'Stop' + [double]$freeDiskSpace = [math]::Round($disk.FreeSpace / 1MB) + + Write-Log -Message "Free disk space for drive [$Drive]: [$freeDiskSpace MB]." -Source ${CmdletName} + Write-Output -InputObject $freeDiskSpace + } + Catch { + Write-Log -Message "Failed to retrieve free disk space for drive [$Drive]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to retrieve free disk space for drive [$Drive]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -2018,197 +2109,197 @@ Function Get-FreeDiskSpace { Function Get-InstalledApplication { <# .SYNOPSIS - Retrieves information about installed applications. + Retrieves information about installed applications. .DESCRIPTION - Retrieves information about installed applications by querying the registry. You can specify an application name, a product code, or both. - Returns information about application publisher, name & version, product code, uninstall string, install source, location, date, and application architecture. + Retrieves information about installed applications by querying the registry. You can specify an application name, a product code, or both. + Returns information about application publisher, name & version, product code, uninstall string, install source, location, date, and application architecture. .PARAMETER Name - The name of the application to retrieve information for. Performs a contains match on the application display name by default. + The name of the application to retrieve information for. Performs a contains match on the application display name by default. .PARAMETER Exact - Specifies that the named application must be matched using the exact name. + Specifies that the named application must be matched using the exact name. .PARAMETER WildCard - Specifies that the named application must be matched using a wildcard search. + Specifies that the named application must be matched using a wildcard search. .PARAMETER RegEx - Specifies that the named application must be matched using a regular expression search. + Specifies that the named application must be matched using a regular expression search. .PARAMETER ProductCode - The product code of the application to retrieve information for. + The product code of the application to retrieve information for. .PARAMETER IncludeUpdatesAndHotfixes - Include matches against updates and hotfixes in results. + Include matches against updates and hotfixes in results. .EXAMPLE - Get-InstalledApplication -Name 'Adobe Flash' + Get-InstalledApplication -Name 'Adobe Flash' .EXAMPLE - Get-InstalledApplication -ProductCode '{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' + Get-InstalledApplication -ProductCode '{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string[]]$Name, - [Parameter(Mandatory=$false)] - [switch]$Exact = $false, - [Parameter(Mandatory=$false)] - [switch]$WildCard = $false, - [Parameter(Mandatory=$false)] - [switch]$RegEx = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$ProductCode, - [Parameter(Mandatory=$false)] - [switch]$IncludeUpdatesAndHotfixes - ) - - 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 ($name) { - Write-Log -Message "Get information for installed Application Name(s) [$($name -join ', ')]..." -Source ${CmdletName} - } - If ($productCode) { - Write-Log -Message "Get information for installed Product Code [$ProductCode]..." -Source ${CmdletName} - } - - ## Enumerate the installed applications from the registry for applications that have the "DisplayName" property - [psobject[]]$regKeyApplication = @() - ForEach ($regKey in $regKeyApplications) { - If (Test-Path -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath') { - [psobject[]]$UninstallKeyApps = Get-ChildItem -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath' - ForEach ($UninstallKeyApp in $UninstallKeyApps) { - Try { - [psobject]$regKeyApplicationProps = Get-ItemProperty -LiteralPath $UninstallKeyApp.PSPath -ErrorAction 'Stop' - If ($regKeyApplicationProps.DisplayName) { [psobject[]]$regKeyApplication += $regKeyApplicationProps } - } - Catch{ - Write-Log -Message "Unable to enumerate properties from registry key path [$($UninstallKeyApp.PSPath)]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - Continue - } - } - } - } - If ($ErrorUninstallKeyPath) { - Write-Log -Message "The following error(s) took place while enumerating installed applications from the registry. `n$(Resolve-Error -ErrorRecord $ErrorUninstallKeyPath)" -Severity 2 -Source ${CmdletName} - } - - $UpdatesSkippedCounter = 0 - ## Create a custom object with the desired properties for the installed applications and sanitize property details - [psobject[]]$installedApplication = @() - ForEach ($regKeyApp in $regKeyApplication) { - Try { - [string]$appDisplayName = '' - [string]$appDisplayVersion = '' - [string]$appPublisher = '' - - ## Bypass any updates or hotfixes - If ((-not $IncludeUpdatesAndHotfixes) -and (($regKeyApp.DisplayName -match '(?i)kb\d+') -or ($regKeyApp.DisplayName -match 'Cumulative Update') -or ($regKeyApp.DisplayName -match 'Security Update') -or ($regKeyApp.DisplayName -match 'Hotfix'))) { - $UpdatesSkippedCounter += 1 - Continue - } - - ## Remove any control characters which may interfere with logging and creating file path names from these variables - $appDisplayName = $regKeyApp.DisplayName -replace '[^\u001F-\u007F]','' - $appDisplayVersion = $regKeyApp.DisplayVersion -replace '[^\u001F-\u007F]','' - $appPublisher = $regKeyApp.Publisher -replace '[^\u001F-\u007F]','' - - - ## Determine if application is a 64-bit application - [boolean]$Is64BitApp = If (($is64Bit) -and ($regKeyApp.PSPath -notmatch '^Microsoft\.PowerShell\.Core\\Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node')) { $true } Else { $false } - - If ($ProductCode) { - ## Verify if there is a match with the product code passed to the script - If ($regKeyApp.PSChildName -match [regex]::Escape($productCode)) { - Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] matching product code [$productCode]." -Source ${CmdletName} - $installedApplication += New-Object -TypeName 'PSObject' -Property @{ - UninstallSubkey = $regKeyApp.PSChildName - ProductCode = If ($regKeyApp.PSChildName -match $MSIProductCodeRegExPattern) { $regKeyApp.PSChildName } Else { [string]::Empty } - DisplayName = $appDisplayName - DisplayVersion = $appDisplayVersion - UninstallString = $regKeyApp.UninstallString - InstallSource = $regKeyApp.InstallSource - InstallLocation = $regKeyApp.InstallLocation - InstallDate = $regKeyApp.InstallDate - Publisher = $appPublisher - Is64BitApplication = $Is64BitApp - } - } - } - - If ($name) { - ## Verify if there is a match with the application name(s) passed to the script - ForEach ($application in $Name) { - $applicationMatched = $false - If ($exact) { - # Check for an exact application name match - If ($regKeyApp.DisplayName -eq $application) { - $applicationMatched = $true - Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using exact name matching for search term [$application]." -Source ${CmdletName} - } - } - ElseIf ($WildCard) { - # Check for wildcard application name match - If ($regKeyApp.DisplayName -like $application) { - $applicationMatched = $true - Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using wildcard matching for search term [$application]." -Source ${CmdletName} - } - } - ElseIf ($RegEx) { - # Check for a regex application name match - If ($regKeyApp.DisplayName -match $application) { - $applicationMatched = $true - Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using regex matching for search term [$application]." -Source ${CmdletName} - } - } - # Check for a contains application name match - ElseIf ($regKeyApp.DisplayName -match [regex]::Escape($application)) { - $applicationMatched = $true - Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using contains matching for search term [$application]." -Source ${CmdletName} - } - - If ($applicationMatched) { - $installedApplication += New-Object -TypeName 'PSObject' -Property @{ - UninstallSubkey = $regKeyApp.PSChildName - ProductCode = If ($regKeyApp.PSChildName -match $MSIProductCodeRegExPattern) { $regKeyApp.PSChildName } Else { [string]::Empty } - DisplayName = $appDisplayName - DisplayVersion = $appDisplayVersion - UninstallString = $regKeyApp.UninstallString - InstallSource = $regKeyApp.InstallSource - InstallLocation = $regKeyApp.InstallLocation - InstallDate = $regKeyApp.InstallDate - Publisher = $appPublisher - Is64BitApplication = $Is64BitApp - } - } - } - } - } - Catch { - Write-Log -Message "Failed to resolve application details from registry for [$appDisplayName]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - Continue - } - } - - If (-not $IncludeUpdatesAndHotfixes) { - ## Write to log the number of entries skipped due to them being considered updates - If ($UpdatesSkippedCounter -eq 1) { - Write-Log -Message "Skipped 1 entry while searching, because it was considered a Microsoft update." -Source ${CmdletName} - } else { - Write-Log -Message "Skipped $UpdatesSkippedCounter entries while searching, because they were considered Microsoft updates." -Source ${CmdletName} - } - } - - If (-not $installedApplication) { - Write-Log -Message "Found no application based on the supplied parameters." -Source ${CmdletName} - } - - Write-Output -InputObject $installedApplication - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string[]]$Name, + [Parameter(Mandatory=$false)] + [switch]$Exact = $false, + [Parameter(Mandatory=$false)] + [switch]$WildCard = $false, + [Parameter(Mandatory=$false)] + [switch]$RegEx = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$ProductCode, + [Parameter(Mandatory=$false)] + [switch]$IncludeUpdatesAndHotfixes + ) + + 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 ($name) { + Write-Log -Message "Get information for installed Application Name(s) [$($name -join ', ')]..." -Source ${CmdletName} + } + If ($productCode) { + Write-Log -Message "Get information for installed Product Code [$ProductCode]..." -Source ${CmdletName} + } + + ## Enumerate the installed applications from the registry for applications that have the "DisplayName" property + [psobject[]]$regKeyApplication = @() + ForEach ($regKey in $regKeyApplications) { + If (Test-Path -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath') { + [psobject[]]$UninstallKeyApps = Get-ChildItem -LiteralPath $regKey -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorUninstallKeyPath' + ForEach ($UninstallKeyApp in $UninstallKeyApps) { + Try { + [psobject]$regKeyApplicationProps = Get-ItemProperty -LiteralPath $UninstallKeyApp.PSPath -ErrorAction 'Stop' + If ($regKeyApplicationProps.DisplayName) { [psobject[]]$regKeyApplication += $regKeyApplicationProps } + } + Catch{ + Write-Log -Message "Unable to enumerate properties from registry key path [$($UninstallKeyApp.PSPath)]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Continue + } + } + } + } + If ($ErrorUninstallKeyPath) { + Write-Log -Message "The following error(s) took place while enumerating installed applications from the registry. `n$(Resolve-Error -ErrorRecord $ErrorUninstallKeyPath)" -Severity 2 -Source ${CmdletName} + } + + $UpdatesSkippedCounter = 0 + ## Create a custom object with the desired properties for the installed applications and sanitize property details + [psobject[]]$installedApplication = @() + ForEach ($regKeyApp in $regKeyApplication) { + Try { + [string]$appDisplayName = '' + [string]$appDisplayVersion = '' + [string]$appPublisher = '' + + ## Bypass any updates or hotfixes + If ((-not $IncludeUpdatesAndHotfixes) -and (($regKeyApp.DisplayName -match '(?i)kb\d+') -or ($regKeyApp.DisplayName -match 'Cumulative Update') -or ($regKeyApp.DisplayName -match 'Security Update') -or ($regKeyApp.DisplayName -match 'Hotfix'))) { + $UpdatesSkippedCounter += 1 + Continue + } + + ## Remove any control characters which may interfere with logging and creating file path names from these variables + $appDisplayName = $regKeyApp.DisplayName -replace '[^\u001F-\u007F]','' + $appDisplayVersion = $regKeyApp.DisplayVersion -replace '[^\u001F-\u007F]','' + $appPublisher = $regKeyApp.Publisher -replace '[^\u001F-\u007F]','' + + + ## Determine if application is a 64-bit application + [boolean]$Is64BitApp = If (($is64Bit) -and ($regKeyApp.PSPath -notmatch '^Microsoft\.PowerShell\.Core\\Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node')) { $true } Else { $false } + + If ($ProductCode) { + ## Verify if there is a match with the product code passed to the script + If ($regKeyApp.PSChildName -match [regex]::Escape($productCode)) { + Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] matching product code [$productCode]." -Source ${CmdletName} + $installedApplication += New-Object -TypeName 'PSObject' -Property @{ + UninstallSubkey = $regKeyApp.PSChildName + ProductCode = If ($regKeyApp.PSChildName -match $MSIProductCodeRegExPattern) { $regKeyApp.PSChildName } Else { [string]::Empty } + DisplayName = $appDisplayName + DisplayVersion = $appDisplayVersion + UninstallString = $regKeyApp.UninstallString + InstallSource = $regKeyApp.InstallSource + InstallLocation = $regKeyApp.InstallLocation + InstallDate = $regKeyApp.InstallDate + Publisher = $appPublisher + Is64BitApplication = $Is64BitApp + } + } + } + + If ($name) { + ## Verify if there is a match with the application name(s) passed to the script + ForEach ($application in $Name) { + $applicationMatched = $false + If ($exact) { + # Check for an exact application name match + If ($regKeyApp.DisplayName -eq $application) { + $applicationMatched = $true + Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using exact name matching for search term [$application]." -Source ${CmdletName} + } + } + ElseIf ($WildCard) { + # Check for wildcard application name match + If ($regKeyApp.DisplayName -like $application) { + $applicationMatched = $true + Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using wildcard matching for search term [$application]." -Source ${CmdletName} + } + } + ElseIf ($RegEx) { + # Check for a regex application name match + If ($regKeyApp.DisplayName -match $application) { + $applicationMatched = $true + Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using regex matching for search term [$application]." -Source ${CmdletName} + } + } + # Check for a contains application name match + ElseIf ($regKeyApp.DisplayName -match [regex]::Escape($application)) { + $applicationMatched = $true + Write-Log -Message "Found installed application [$appDisplayName] version [$appDisplayVersion] using contains matching for search term [$application]." -Source ${CmdletName} + } + + If ($applicationMatched) { + $installedApplication += New-Object -TypeName 'PSObject' -Property @{ + UninstallSubkey = $regKeyApp.PSChildName + ProductCode = If ($regKeyApp.PSChildName -match $MSIProductCodeRegExPattern) { $regKeyApp.PSChildName } Else { [string]::Empty } + DisplayName = $appDisplayName + DisplayVersion = $appDisplayVersion + UninstallString = $regKeyApp.UninstallString + InstallSource = $regKeyApp.InstallSource + InstallLocation = $regKeyApp.InstallLocation + InstallDate = $regKeyApp.InstallDate + Publisher = $appPublisher + Is64BitApplication = $Is64BitApp + } + } + } + } + } + Catch { + Write-Log -Message "Failed to resolve application details from registry for [$appDisplayName]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Continue + } + } + + If (-not $IncludeUpdatesAndHotfixes) { + ## Write to log the number of entries skipped due to them being considered updates + If ($UpdatesSkippedCounter -eq 1) { + Write-Log -Message "Skipped 1 entry while searching, because it was considered a Microsoft update." -Source ${CmdletName} + } else { + Write-Log -Message "Skipped $UpdatesSkippedCounter entries while searching, because they were considered Microsoft updates." -Source ${CmdletName} + } + } + + If (-not $installedApplication) { + Write-Log -Message "Found no application based on the supplied parameters." -Source ${CmdletName} + } + + Write-Output -InputObject $installedApplication + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -2217,349 +2308,351 @@ Function Get-InstalledApplication { Function Execute-MSI { <# .SYNOPSIS - Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. + Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. .DESCRIPTION - Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. - If the -Action parameter is set to "Install" and the MSI is already installed, the function will exit. - Sets default switches to be passed to msiexec based on the preferences in the XML configuration file. - Automatically generates a log file name and creates a verbose log file for all msiexec operations. - Expects the MSI or MSP file to be located in the "Files" sub directory of the App Deploy Toolkit. Expects transform files to be in the same directory as the MSI file. + Executes msiexec.exe to perform the following actions for MSI & MSP files and MSI product codes: install, uninstall, patch, repair, active setup. + If the -Action parameter is set to "Install" and the MSI is already installed, the function will exit. + Sets default switches to be passed to msiexec based on the preferences in the XML configuration file. + Automatically generates a log file name and creates a verbose log file for all msiexec operations. + Expects the MSI or MSP file to be located in the "Files" sub directory of the App Deploy Toolkit. Expects transform files to be in the same directory as the MSI file. .PARAMETER Action - The action to perform. Options: Install, Uninstall, Patch, Repair, ActiveSetup. + The action to perform. Options: Install, Uninstall, Patch, Repair, ActiveSetup. .PARAMETER Path - The path to the MSI/MSP file or the product code of the installed MSI. + The path to the MSI/MSP file or the product code of the installed MSI. .PARAMETER Transform - The name of the transform file(s) to be applied to the MSI. The transform file is expected to be in the same directory as the MSI file. + The name of the transform file(s) to be applied to the MSI. The transform file is expected to be in the same directory as the MSI file. .PARAMETER Patch - The name of the patch (msp) file(s) to be applied to the MSI for use with the "Install" action. The patch file is expected to be in the same directory as the MSI file. + The name of the patch (msp) file(s) to be applied to the MSI for use with the "Install" action. The patch file is expected to be in the same directory as the MSI file. .PARAMETER Parameters - Overrides the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". + Overrides the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER AddParameters - Adds to the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". + Adds to the default parameters specified in the XML configuration file. Install default is: "REBOOT=ReallySuppress /QB!". Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER SecureParameters - Hides all parameters passed to the MSI or MSP file from the toolkit Log file. + Hides all parameters passed to the MSI or MSP file from the toolkit Log file. .PARAMETER LoggingOptions - Overrides the default logging options specified in the XML configuration file. Default options are: "/L*v". + Overrides the default logging options specified in the XML configuration file. Default options are: "/L*v". .PARAMETER LogName - Overrides the default log file name. The default log file name is generated from the MSI file name. If LogName does not end in .log, it will be automatically appended. - For uninstallations, by default the product code is resolved to the DisplayName and version of the application. + Overrides the default log file name. The default log file name is generated from the MSI file name. If LogName does not end in .log, it will be automatically appended. + For uninstallations, by default the product code is resolved to the DisplayName and version of the application. .PARAMETER WorkingDirectory - Overrides the working directory. The working directory is set to the location of the MSI file. + Overrides the working directory. The working directory is set to the location of the MSI file. .PARAMETER SkipMSIAlreadyInstalledCheck - Skips the check to determine if the MSI is already installed on the system. Default is: $false. + Skips the check to determine if the MSI is already installed on the system. Default is: $false. .PARAMETER IncludeUpdatesAndHotfixes - Include matches against updates and hotfixes in results. + Include matches against updates and hotfixes in results. .PARAMETER NoWait - Immediately continue after executing the process. + Immediately continue after executing the process. .PARAMETER PassThru - Returns ExitCode, STDOut, and STDErr output from the process. + Returns ExitCode, STDOut, and STDErr output from the process. .PARAMETER IgnoreExitCodes - List the exit codes to ignore or * to ignore all exit codes. + List the exit codes to ignore or * to ignore all exit codes. .PARAMETER PriorityClass - Specifies priority class for the process. Options: Idle, Normal, High, AboveNormal, BelowNormal, RealTime. Default: Normal + Specifies priority class for the process. Options: Idle, Normal, High, AboveNormal, BelowNormal, RealTime. Default: Normal .PARAMETER ExitOnProcessFailure - Specifies whether the function should call Exit-Script when the process returns an exit code that is considered an error/failure. Default: $true + Specifies whether the function should call Exit-Script when the process returns an exit code that is considered an error/failure. Default: $true .PARAMETER RepairFromSource - Specifies whether we should repair from source. Also rewrites local cache. Default: $false + Specifies whether we should repair from source. Also rewrites local cache. Default: $false .PARAMETER ContinueOnError - Continue if an error occured while trying to start the process. Default: $false. + Continue if an error occured while trying to start the process. Default: $false. .EXAMPLE - Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' - Installs an MSI + Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' + Installs an MSI .EXAMPLE - Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -Transform 'Adobe_FlashPlayer_11.2.202.233_x64_EN_01.mst' -Parameters '/QN' - Installs an MSI, applying a transform and overriding the default MSI toolkit parameters + Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -Transform 'Adobe_FlashPlayer_11.2.202.233_x64_EN_01.mst' -Parameters '/QN' + Installs an MSI, applying a transform and overriding the default MSI toolkit parameters .EXAMPLE - [psobject]$ExecuteMSIResult = Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -PassThru - Installs an MSI and stores the result of the execution into a variable by using the -PassThru option + [psobject]$ExecuteMSIResult = Execute-MSI -Action 'Install' -Path 'Adobe_FlashPlayer_11.2.202.233_x64_EN.msi' -PassThru + Installs an MSI and stores the result of the execution into a variable by using the -PassThru option .EXAMPLE - Execute-MSI -Action 'Uninstall' -Path '{26923b43-4d38-484f-9b9e-de460746276c}' - Uninstalls an MSI using a product code + Execute-MSI -Action 'Uninstall' -Path '{26923b43-4d38-484f-9b9e-de460746276c}' + Uninstalls an MSI using a product code .EXAMPLE - Execute-MSI -Action 'Patch' -Path 'Adobe_Reader_11.0.3_EN.msp' - Installs an MSP + Execute-MSI -Action 'Patch' -Path 'Adobe_Reader_11.0.3_EN.msp' + Installs an MSP .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateSet('Install','Uninstall','Patch','Repair','ActiveSetup')] - [string]$Action = 'Install', - [Parameter(Mandatory=$true,HelpMessage='Please enter either the path to the MSI/MSP file or the ProductCode')] - [ValidateScript({($_ -match $MSIProductCodeRegExPattern) -or ('.msi','.msp' -contains [IO.Path]::GetExtension($_))})] - [Alias('FilePath')] - [string]$Path, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Transform, - [Parameter(Mandatory=$false)] - [Alias('Arguments')] - [ValidateNotNullorEmpty()] - [string]$Parameters, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$AddParameters, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$SecureParameters = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Patch, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$LoggingOptions, - [Parameter(Mandatory=$false)] - [Alias('LogName')] - [string]$private:LogName, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$WorkingDirectory, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$SkipMSIAlreadyInstalledCheck = $false, - [Parameter(Mandatory=$false)] - [switch]$IncludeUpdatesAndHotfixes = $false, - [Parameter(Mandatory=$false)] - [switch]$NoWait = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$PassThru = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$IgnoreExitCodes, - [Parameter(Mandatory=$false)] - [ValidateSet('Idle', 'Normal', 'High', 'AboveNormal', 'BelowNormal', 'RealTime')] - [Diagnostics.ProcessPriorityClass]$PriorityClass = 'Normal', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ExitOnProcessFailure = $true, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$RepairFromSource = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Initialize variable indicating whether $Path variable is a Product Code or not - [boolean]$PathIsProductCode = $false - - ## If the path matches a product code - If ($Path -match $MSIProductCodeRegExPattern) { - # Set variable indicating that $Path variable is a Product Code - [boolean]$PathIsProductCode = $true - - # Resolve the product code to a publisher, application name, and version - Write-Log -Message 'Resolve product code to a publisher, application name, and version.' -Source ${CmdletName} - - If ($IncludeUpdatesAndHotfixes) { - [psobject]$productCodeNameVersion = Get-InstalledApplication -ProductCode $path -IncludeUpdatesAndHotfixes | Select-Object -Property 'Publisher', 'DisplayName', 'DisplayVersion' -First 1 -ErrorAction 'SilentlyContinue' - } - Else { - [psobject]$productCodeNameVersion = Get-InstalledApplication -ProductCode $path | Select-Object -Property 'Publisher', 'DisplayName', 'DisplayVersion' -First 1 -ErrorAction 'SilentlyContinue' - } - - # Build the log file name - If (-not $logName) { - If ($productCodeNameVersion) { - If ($productCodeNameVersion.Publisher) { - $logName = (Remove-InvalidFileNameChars -Name ($productCodeNameVersion.Publisher + '_' + $productCodeNameVersion.DisplayName + '_' + $productCodeNameVersion.DisplayVersion)) -replace ' ','' - } - Else { - $logName = (Remove-InvalidFileNameChars -Name ($productCodeNameVersion.DisplayName + '_' + $productCodeNameVersion.DisplayVersion)) -replace ' ','' - } - } - Else { - # Out of other options, make the Product Code the name of the log file - $logName = $Path - } - } - } - Else { - # Get the log file name without file extension - If (-not $logName) { $logName = ([IO.FileInfo]$path).BaseName } ElseIf ('.log','.txt' -contains [IO.Path]::GetExtension($logName)) { $logName = [IO.Path]::GetFileNameWithoutExtension($logName) } - } - - If ($configToolkitCompressLogs) { - ## Build the log file path - [string]$logPath = Join-Path -Path $logTempFolder -ChildPath $logName - } - Else { - ## Create the Log directory if it doesn't already exist - If (-not (Test-Path -LiteralPath $configMSILogDir -PathType 'Container' -ErrorAction 'SilentlyContinue')) { - $null = New-Item -Path $configMSILogDir -ItemType 'Directory' -ErrorAction 'SilentlyContinue' - } - ## Build the log file path - [string]$logPath = Join-Path -Path $configMSILogDir -ChildPath $logName - } - - ## Set the installation Parameters - If ($deployModeSilent) { - $msiInstallDefaultParams = $configMSISilentParams - $msiUninstallDefaultParams = $configMSISilentParams - } - Else { - $msiInstallDefaultParams = $configMSIInstallParams - $msiUninstallDefaultParams = $configMSIUninstallParams - } - - ## Build the MSI Parameters - Switch ($action) { - 'Install' { $option = '/i'; [string]$msiLogFile = "$logPath" + '_Install'; $msiDefaultParams = $msiInstallDefaultParams } - 'Uninstall' { $option = '/x'; [string]$msiLogFile = "$logPath" + '_Uninstall'; $msiDefaultParams = $msiUninstallDefaultParams } - 'Patch' { $option = '/update'; [string]$msiLogFile = "$logPath" + '_Patch'; $msiDefaultParams = $msiInstallDefaultParams } - 'Repair' { $option = '/f'; If ($RepairFromSource) { $option += "v" } [string]$msiLogFile = "$logPath" + '_Repair'; $msiDefaultParams = $msiInstallDefaultParams } - 'ActiveSetup' { $option = '/fups'; [string]$msiLogFile = "$logPath" + '_ActiveSetup' } - } - - ## Append ".log" to the MSI logfile path and enclose in quotes - If ([IO.Path]::GetExtension($msiLogFile) -ne '.log') { - [string]$msiLogFile = $msiLogFile + '.log' - [string]$msiLogFile = "`"$msiLogFile`"" - } - - ## If the MSI is in the Files directory, set the full path to the MSI - If (Test-Path -LiteralPath (Join-Path -Path $dirFiles -ChildPath $path -ErrorAction 'SilentlyContinue') -PathType 'Leaf' -ErrorAction 'SilentlyContinue') { - [string]$msiFile = Join-Path -Path $dirFiles -ChildPath $path - } - ElseIf (Test-Path -LiteralPath $Path -ErrorAction 'SilentlyContinue') { - [string]$msiFile = (Get-Item -LiteralPath $Path).FullName - } - ElseIf ($PathIsProductCode) { - [string]$msiFile = $Path - } - Else { - Write-Log -Message "Failed to find MSI file [$path]." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to find MSI file [$path]." - } - Continue - } - - ## Set the working directory of the MSI - If ((-not $PathIsProductCode) -and (-not $workingDirectory)) { [string]$workingDirectory = Split-Path -Path $msiFile -Parent } - - ## Enumerate all transforms specified, qualify the full path if possible and enclose in quotes - If ($transform) { - [string[]]$transforms = $transform -split ',' - 0..($transforms.Length - 1) | ForEach-Object { - If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_]) -PathType 'Leaf') { - $transforms[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_].Replace('.\','') - } - Else { - $transforms[$_] = $transforms[$_] - } - } - [string]$mstFile = "`"$($transforms -join ';')`"" - } - - ## Enumerate all patches specified, qualify the full path if possible and enclose in quotes - If ($patch) { - [string[]]$patches = $patch -split ',' - 0..($patches.Length - 1) | ForEach-Object { - If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_]) -PathType 'Leaf') { - $patches[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_].Replace('.\','') - } - Else { - $patches[$_] = $patches[$_] - } - } - [string]$mspFile = "`"$($patches -join ';')`"" - } - - ## Get the ProductCode of the MSI - If ($PathIsProductCode) { - [string]$MSIProductCode = $path - } - ElseIf ([IO.Path]::GetExtension($msiFile) -eq '.msi') { - Try { - [hashtable]$GetMsiTablePropertySplat = @{ Path = $msiFile; Table = 'Property'; ContinueOnError = $false } - If ($transforms) { $GetMsiTablePropertySplat.Add( 'TransformPath', $transforms ) } - [string]$MSIProductCode = Get-MsiTableProperty @GetMsiTablePropertySplat | Select-Object -ExpandProperty 'ProductCode' -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to get the ProductCode from the MSI file. Continue with requested action [$Action]..." -Source ${CmdletName} - } - } - - ## Enclose the MSI file in quotes to avoid issues with spaces when running msiexec - [string]$msiFile = "`"$msiFile`"" - - ## Start building the MsiExec command line starting with the base action and file - [string]$argsMSI = "$option $msiFile" - # Add MST - If ($transform) { $argsMSI = "$argsMSI TRANSFORMS=$mstFile TRANSFORMSSECURE=1" } - # Add MSP - If ($patch) { $argsMSI = "$argsMSI PATCH=$mspFile" } - # Replace default parameters if specified. - If ($Parameters) { $argsMSI = "$argsMSI $Parameters" } Else { $argsMSI = "$argsMSI $msiDefaultParams" } - # Append parameters to default parameters if specified. - If ($AddParameters) { $argsMSI = "$argsMSI $AddParameters" } - # Add custom Logging Options if specified, otherwise, add default Logging Options from Config file - If ($LoggingOptions) { $argsMSI = "$argsMSI $LoggingOptions $msiLogFile" } Else { $argsMSI = "$argsMSI $configMSILoggingOptions $msiLogFile" } - - ## Check if the MSI is already installed. If no valid ProductCode to check, then continue with requested MSI action. - If ($MSIProductCode) { - If ($SkipMSIAlreadyInstalledCheck) { - [boolean]$IsMsiInstalled = $false - } - Else { - If ($IncludeUpdatesAndHotfixes) { - [psobject]$MsiInstalled = Get-InstalledApplication -ProductCode $MSIProductCode -IncludeUpdatesAndHotfixes - } - Else { - [psobject]$MsiInstalled = Get-InstalledApplication -ProductCode $MSIProductCode - } - If ($MsiInstalled) { [boolean]$IsMsiInstalled = $true } - } - } - Else { - If ($Action -eq 'Install') { [boolean]$IsMsiInstalled = $false } Else { [boolean]$IsMsiInstalled = $true } - } - - If (($IsMsiInstalled) -and ($Action -eq 'Install')) { - Write-Log -Message "The MSI is already installed on this system. Skipping action [$Action]..." -Source ${CmdletName} - } - ElseIf (((-not $IsMsiInstalled) -and ($Action -eq 'Install')) -or ($IsMsiInstalled)) { - Write-Log -Message "Executing MSI action [$Action]..." -Source ${CmdletName} - # Build the hashtable with the options that will be passed to Execute-Process using splatting - [hashtable]$ExecuteProcessSplat = @{ Path = $exeMsiexec - Parameters = $argsMSI - WindowStyle = 'Normal' } - If ($WorkingDirectory) { $ExecuteProcessSplat.Add( 'WorkingDirectory', $WorkingDirectory) } - If ($ContinueOnError) { $ExecuteProcessSplat.Add( 'ContinueOnError', $ContinueOnError) } - If ($SecureParameters) { $ExecuteProcessSplat.Add( 'SecureParameters', $SecureParameters) } - If ($PassThru) { $ExecuteProcessSplat.Add( 'PassThru', $PassThru) } - If ($IgnoreExitCodes) { $ExecuteProcessSplat.Add( 'IgnoreExitCodes', $IgnoreExitCodes) } - If ($PriorityClass) { $ExecuteProcessSplat.Add( 'PriorityClass', $PriorityClass) } - If ($ExitOnProcessFailure) { $ExecuteProcessSplat.Add( 'ExitOnProcessFailure', $ExitOnProcessFailure) } - If ($NoWait) { $ExecuteProcessSplat.Add( 'NoWait', $NoWait) } - # Call the Execute-Process function - If ($PassThru) { - [psobject]$ExecuteResults = Execute-Process @ExecuteProcessSplat - } - Else { - Execute-Process @ExecuteProcessSplat - } - # Refresh environment variables for Windows Explorer process as Windows does not consistently update environment variables created by MSIs - Update-Desktop - } - Else { - Write-Log -Message "The MSI is not installed on this system. Skipping action [$Action]..." -Source ${CmdletName} - } - } - End { - If ($PassThru) { Write-Output -InputObject $ExecuteResults } - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Patch','Repair','ActiveSetup')] + [string]$Action = 'Install', + [Parameter(Mandatory=$true,HelpMessage='Please enter either the path to the MSI/MSP file or the ProductCode')] + [ValidateScript({($_ -match $MSIProductCodeRegExPattern) -or ('.msi','.msp' -contains [IO.Path]::GetExtension($_))})] + [Alias('FilePath')] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Transform, + [Parameter(Mandatory=$false)] + [Alias('Arguments')] + [ValidateNotNullorEmpty()] + [string]$Parameters, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$AddParameters, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$SecureParameters = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Patch, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$LoggingOptions, + [Parameter(Mandatory=$false)] + [Alias('LogName')] + [string]$private:LogName, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$WorkingDirectory, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$SkipMSIAlreadyInstalledCheck = $false, + [Parameter(Mandatory=$false)] + [switch]$IncludeUpdatesAndHotfixes = $false, + [Parameter(Mandatory=$false)] + [switch]$NoWait = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$PassThru = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$IgnoreExitCodes, + [Parameter(Mandatory=$false)] + [ValidateSet('Idle', 'Normal', 'High', 'AboveNormal', 'BelowNormal', 'RealTime')] + [Diagnostics.ProcessPriorityClass]$PriorityClass = 'Normal', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ExitOnProcessFailure = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$RepairFromSource = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Initialize variable indicating whether $Path variable is a Product Code or not + [boolean]$PathIsProductCode = $false + + ## If the path matches a product code + If ($Path -match $MSIProductCodeRegExPattern) { + # Set variable indicating that $Path variable is a Product Code + [boolean]$PathIsProductCode = $true + + # Resolve the product code to a publisher, application name, and version + Write-Log -Message 'Resolve product code to a publisher, application name, and version.' -Source ${CmdletName} + + If ($IncludeUpdatesAndHotfixes) { + [psobject]$productCodeNameVersion = Get-InstalledApplication -ProductCode $path -IncludeUpdatesAndHotfixes | Select-Object -Property 'Publisher', 'DisplayName', 'DisplayVersion' -First 1 -ErrorAction 'SilentlyContinue' + } + Else { + [psobject]$productCodeNameVersion = Get-InstalledApplication -ProductCode $path | Select-Object -Property 'Publisher', 'DisplayName', 'DisplayVersion' -First 1 -ErrorAction 'SilentlyContinue' + } + + # Build the log file name + If (-not $logName) { + If ($productCodeNameVersion) { + If ($productCodeNameVersion.Publisher) { + $logName = (Remove-InvalidFileNameChars -Name ($productCodeNameVersion.Publisher + '_' + $productCodeNameVersion.DisplayName + '_' + $productCodeNameVersion.DisplayVersion)) -replace ' ','' + } + Else { + $logName = (Remove-InvalidFileNameChars -Name ($productCodeNameVersion.DisplayName + '_' + $productCodeNameVersion.DisplayVersion)) -replace ' ','' + } + } + Else { + # Out of other options, make the Product Code the name of the log file + $logName = $Path + } + } + } + Else { + # Get the log file name without file extension + If (-not $logName) { $logName = ([IO.FileInfo]$path).BaseName } ElseIf ('.log','.txt' -contains [IO.Path]::GetExtension($logName)) { $logName = [IO.Path]::GetFileNameWithoutExtension($logName) } + } + + If ($configToolkitCompressLogs) { + ## Build the log file path + [string]$logPath = Join-Path -Path $logTempFolder -ChildPath $logName + } + Else { + ## Create the Log directory if it doesn't already exist + If (-not (Test-Path -LiteralPath $configMSILogDir -PathType 'Container' -ErrorAction 'SilentlyContinue')) { + $null = New-Item -Path $configMSILogDir -ItemType 'Directory' -ErrorAction 'SilentlyContinue' + } + ## Build the log file path + [string]$logPath = Join-Path -Path $configMSILogDir -ChildPath $logName + } + + ## Set the installation Parameters + If ($deployModeSilent) { + $msiInstallDefaultParams = $configMSISilentParams + $msiUninstallDefaultParams = $configMSISilentParams + } + Else { + $msiInstallDefaultParams = $configMSIInstallParams + $msiUninstallDefaultParams = $configMSIUninstallParams + } + + ## Build the MSI Parameters + Switch ($action) { + 'Install' { $option = '/i'; [string]$msiLogFile = "$logPath" + '_Install'; $msiDefaultParams = $msiInstallDefaultParams } + 'Uninstall' { $option = '/x'; [string]$msiLogFile = "$logPath" + '_Uninstall'; $msiDefaultParams = $msiUninstallDefaultParams } + 'Patch' { $option = '/update'; [string]$msiLogFile = "$logPath" + '_Patch'; $msiDefaultParams = $msiInstallDefaultParams } + 'Repair' { $option = '/f'; If ($RepairFromSource) { $option += "v" } [string]$msiLogFile = "$logPath" + '_Repair'; $msiDefaultParams = $msiInstallDefaultParams } + 'ActiveSetup' { $option = '/fups'; [string]$msiLogFile = "$logPath" + '_ActiveSetup' } + } + + ## Append ".log" to the MSI logfile path and enclose in quotes + If ([IO.Path]::GetExtension($msiLogFile) -ne '.log') { + [string]$msiLogFile = $msiLogFile + '.log' + [string]$msiLogFile = "`"$msiLogFile`"" + } + + ## If the MSI is in the Files directory, set the full path to the MSI + If (Test-Path -LiteralPath (Join-Path -Path $dirFiles -ChildPath $path -ErrorAction 'SilentlyContinue') -PathType 'Leaf' -ErrorAction 'SilentlyContinue') { + [string]$msiFile = Join-Path -Path $dirFiles -ChildPath $path + } + ElseIf (Test-Path -LiteralPath $Path -ErrorAction 'SilentlyContinue') { + [string]$msiFile = (Get-Item -LiteralPath $Path).FullName + } + ElseIf ($PathIsProductCode) { + [string]$msiFile = $Path + } + Else { + Write-Log -Message "Failed to find MSI file [$path]." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to find MSI file [$path]." + } + Continue + } + + ## Set the working directory of the MSI + If ((-not $PathIsProductCode) -and (-not $workingDirectory)) { [string]$workingDirectory = Split-Path -Path $msiFile -Parent } + + ## Enumerate all transforms specified, qualify the full path if possible and enclose in quotes + If ($transform) { + [string[]]$transforms = $transform -split ',' + 0..($transforms.Length - 1) | ForEach-Object { + If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_]) -PathType 'Leaf') { + $transforms[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $transforms[$_].Replace('.\','') + } + Else { + $transforms[$_] = $transforms[$_] + } + } + [string]$mstFile = "`"$($transforms -join ';')`"" + } + + ## Enumerate all patches specified, qualify the full path if possible and enclose in quotes + If ($patch) { + [string[]]$patches = $patch -split ',' + 0..($patches.Length - 1) | ForEach-Object { + If (Test-Path -LiteralPath (Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_]) -PathType 'Leaf') { + $patches[$_] = Join-Path -Path (Split-Path -Path $msiFile -Parent) -ChildPath $patches[$_].Replace('.\','') + } + Else { + $patches[$_] = $patches[$_] + } + } + [string]$mspFile = "`"$($patches -join ';')`"" + } + + ## Get the ProductCode of the MSI + If ($PathIsProductCode) { + [string]$MSIProductCode = $path + } + ElseIf ([IO.Path]::GetExtension($msiFile) -eq '.msi') { + Try { + [hashtable]$GetMsiTablePropertySplat = @{ Path = $msiFile; Table = 'Property'; ContinueOnError = $false } + If ($transforms) { $GetMsiTablePropertySplat.Add( 'TransformPath', $transforms ) } + [string]$MSIProductCode = Get-MsiTableProperty @GetMsiTablePropertySplat | Select-Object -ExpandProperty 'ProductCode' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to get the ProductCode from the MSI file. Continue with requested action [$Action]..." -Source ${CmdletName} + } + } + + ## Enclose the MSI file in quotes to avoid issues with spaces when running msiexec + [string]$msiFile = "`"$msiFile`"" + + ## Start building the MsiExec command line starting with the base action and file + [string]$argsMSI = "$option $msiFile" + # Add MST + If ($transform) { $argsMSI = "$argsMSI TRANSFORMS=$mstFile TRANSFORMSSECURE=1" } + # Add MSP + If ($patch) { $argsMSI = "$argsMSI PATCH=$mspFile" } + # Replace default parameters if specified. + If ($Parameters) { $argsMSI = "$argsMSI $Parameters" } Else { $argsMSI = "$argsMSI $msiDefaultParams" } + # Add reinstallmode and reinstall variable for Patch + If ($action -eq 'Patch') {$argsMSI += " REINSTALLMODE=ecmus REINSTALL=ALL"} + # Append parameters to default parameters if specified. + If ($AddParameters) { $argsMSI = "$argsMSI $AddParameters" } + # Add custom Logging Options if specified, otherwise, add default Logging Options from Config file + If ($LoggingOptions) { $argsMSI = "$argsMSI $LoggingOptions $msiLogFile" } Else { $argsMSI = "$argsMSI $configMSILoggingOptions $msiLogFile" } + + ## Check if the MSI is already installed. If no valid ProductCode to check, then continue with requested MSI action. + If ($MSIProductCode) { + If ($SkipMSIAlreadyInstalledCheck) { + [boolean]$IsMsiInstalled = $false + } + Else { + If ($IncludeUpdatesAndHotfixes) { + [psobject]$MsiInstalled = Get-InstalledApplication -ProductCode $MSIProductCode -IncludeUpdatesAndHotfixes + } + Else { + [psobject]$MsiInstalled = Get-InstalledApplication -ProductCode $MSIProductCode + } + If ($MsiInstalled) { [boolean]$IsMsiInstalled = $true } + } + } + Else { + If ($Action -eq 'Install') { [boolean]$IsMsiInstalled = $false } Else { [boolean]$IsMsiInstalled = $true } + } + + If (($IsMsiInstalled) -and ($Action -eq 'Install')) { + Write-Log -Message "The MSI is already installed on this system. Skipping action [$Action]..." -Source ${CmdletName} + } + ElseIf (((-not $IsMsiInstalled) -and ($Action -eq 'Install')) -or ($IsMsiInstalled)) { + Write-Log -Message "Executing MSI action [$Action]..." -Source ${CmdletName} + # Build the hashtable with the options that will be passed to Execute-Process using splatting + [hashtable]$ExecuteProcessSplat = @{ Path = $exeMsiexec + Parameters = $argsMSI + WindowStyle = 'Normal' } + If ($WorkingDirectory) { $ExecuteProcessSplat.Add( 'WorkingDirectory', $WorkingDirectory) } + If ($ContinueOnError) { $ExecuteProcessSplat.Add( 'ContinueOnError', $ContinueOnError) } + If ($SecureParameters) { $ExecuteProcessSplat.Add( 'SecureParameters', $SecureParameters) } + If ($PassThru) { $ExecuteProcessSplat.Add( 'PassThru', $PassThru) } + If ($IgnoreExitCodes) { $ExecuteProcessSplat.Add( 'IgnoreExitCodes', $IgnoreExitCodes) } + If ($PriorityClass) { $ExecuteProcessSplat.Add( 'PriorityClass', $PriorityClass) } + If ($ExitOnProcessFailure) { $ExecuteProcessSplat.Add( 'ExitOnProcessFailure', $ExitOnProcessFailure) } + If ($NoWait) { $ExecuteProcessSplat.Add( 'NoWait', $NoWait) } + # Call the Execute-Process function + If ($PassThru) { + [psobject]$ExecuteResults = Execute-Process @ExecuteProcessSplat + } + Else { + Execute-Process @ExecuteProcessSplat + } + # Refresh environment variables for Windows Explorer process as Windows does not consistently update environment variables created by MSIs + Update-Desktop + } + Else { + Write-Log -Message "The MSI is not installed on this system. Skipping action [$Action]..." -Source ${CmdletName} + } + } + End { + If ($PassThru) { Write-Output -InputObject $ExecuteResults } + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -2568,232 +2661,232 @@ Function Execute-MSI { Function Remove-MSIApplications { <# .SYNOPSIS - Removes all MSI applications matching the specified application name. + Removes all MSI applications matching the specified application name. .DESCRIPTION - Removes all MSI applications matching the specified application name. - Enumerates the registry for installed applications matching the specified application name and uninstalls that application using the product code, provided the uninstall string matches "msiexec". + Removes all MSI applications matching the specified application name. + Enumerates the registry for installed applications matching the specified application name and uninstalls that application using the product code, provided the uninstall string matches "msiexec". .PARAMETER Name - The name of the application to uninstall. Performs a contains match on the application display name by default. + The name of the application to uninstall. Performs a contains match on the application display name by default. .PARAMETER Exact - Specifies that the named application must be matched using the exact name. + Specifies that the named application must be matched using the exact name. .PARAMETER WildCard - Specifies that the named application must be matched using a wildcard search. + Specifies that the named application must be matched using a wildcard search. .PARAMETER Parameters - Overrides the default parameters specified in the XML configuration file. Uninstall default is: "REBOOT=ReallySuppress /QN". + Overrides the default parameters specified in the XML configuration file. Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER AddParameters - Adds to the default parameters specified in the XML configuration file. Uninstall default is: "REBOOT=ReallySuppress /QN". + Adds to the default parameters specified in the XML configuration file. Uninstall default is: "REBOOT=ReallySuppress /QN". .PARAMETER FilterApplication - Two-dimensional array that contains one or more (property, value, match-type) sets that should be used to filter the list of results returned by Get-InstalledApplication to only those that should be uninstalled. - Properties that can be filtered upon: ProductCode, DisplayName, DisplayVersion, UninstallString, InstallSource, InstallLocation, InstallDate, Publisher, Is64BitApplication + Two-dimensional array that contains one or more (property, value, match-type) sets that should be used to filter the list of results returned by Get-InstalledApplication to only those that should be uninstalled. + Properties that can be filtered upon: ProductCode, DisplayName, DisplayVersion, UninstallString, InstallSource, InstallLocation, InstallDate, Publisher, Is64BitApplication .PARAMETER ExcludeFromUninstall - Two-dimensional array that contains one or more (property, value, match-type) sets that should be excluded from uninstall if found. - Properties that can be excluded: ProductCode, DisplayName, DisplayVersion, UninstallString, InstallSource, InstallLocation, InstallDate, Publisher, Is64BitApplication + Two-dimensional array that contains one or more (property, value, match-type) sets that should be excluded from uninstall if found. + Properties that can be excluded: ProductCode, DisplayName, DisplayVersion, UninstallString, InstallSource, InstallLocation, InstallDate, Publisher, Is64BitApplication .PARAMETER IncludeUpdatesAndHotfixes - Include matches against updates and hotfixes in results. + Include matches against updates and hotfixes in results. .PARAMETER LoggingOptions - Overrides the default logging options specified in the XML configuration file. Default options are: "/L*v". + Overrides the default logging options specified in the XML configuration file. Default options are: "/L*v". .PARAMETER LogName - Overrides the default log file name. The default log file name is generated from the MSI file name. If LogName does not end in .log, it will be automatically appended. - For uninstallations, by default the product code is resolved to the DisplayName and version of the application. + Overrides the default log file name. The default log file name is generated from the MSI file name. If LogName does not end in .log, it will be automatically appended. + For uninstallations, by default the product code is resolved to the DisplayName and version of the application. .PARAMETER PassThru - Returns ExitCode, STDOut, and STDErr output from the process. + Returns ExitCode, STDOut, and STDErr output from the process. .PARAMETER ContinueOnError - Continue if an error occured while trying to start the processes. Default: $true. + Continue if an error occured while trying to start the processes. Default: $true. .EXAMPLE - Remove-MSIApplications -Name 'Adobe Flash' - Removes all versions of software that match the name "Adobe Flash" + Remove-MSIApplications -Name 'Adobe Flash' + Removes all versions of software that match the name "Adobe Flash" .EXAMPLE - Remove-MSIApplications -Name 'Adobe' - Removes all versions of software that match the name "Adobe" + Remove-MSIApplications -Name 'Adobe' + Removes all versions of software that match the name "Adobe" .EXAMPLE - Remove-MSIApplications -Name 'Java 8 Update' -FilterApplication ('Is64BitApplication', $false, 'Exact'),('Publisher', 'Oracle Corporation', 'Exact') - Removes all versions of software that match the name "Java 8 Update" where the software is 32-bits and the publisher is "Oracle Corporation". + Remove-MSIApplications -Name 'Java 8 Update' -FilterApplication ('Is64BitApplication', $false, 'Exact'),('Publisher', 'Oracle Corporation', 'Exact') + Removes all versions of software that match the name "Java 8 Update" where the software is 32-bits and the publisher is "Oracle Corporation". .EXAMPLE - Remove-MSIApplications -Name 'Java 8 Update' -FilterApplication (,('Publisher', 'Oracle Corporation', 'Exact')) -ExcludeFromUninstall (,('DisplayName', 'Java 8 Update 45', 'Contains')) - Removes all versions of software that match the name "Java 8 Update" and also have "Oracle Corporation" as the Publisher; however, it does not uninstall "Java 8 Update 45" of the software. - NOTE: if only specifying a single row in the two-dimensional arrays, the array must have the extra parentheses and leading comma as in this example. + Remove-MSIApplications -Name 'Java 8 Update' -FilterApplication (,('Publisher', 'Oracle Corporation', 'Exact')) -ExcludeFromUninstall (,('DisplayName', 'Java 8 Update 45', 'Contains')) + Removes all versions of software that match the name "Java 8 Update" and also have "Oracle Corporation" as the Publisher; however, it does not uninstall "Java 8 Update 45" of the software. + NOTE: if only specifying a single row in the two-dimensional arrays, the array must have the extra parentheses and leading comma as in this example. .EXAMPLE - Remove-MSIApplications -Name 'Java 8 Update' -ExcludeFromUninstall (,('DisplayName', 'Java 8 Update 45', 'Contains')) - Removes all versions of software that match the name "Java 8 Update"; however, it does not uninstall "Java 8 Update 45" of the software. - NOTE: if only specifying a single row in the two-dimensional array, the array must have the extra parentheses and leading comma as in this example. + Remove-MSIApplications -Name 'Java 8 Update' -ExcludeFromUninstall (,('DisplayName', 'Java 8 Update 45', 'Contains')) + Removes all versions of software that match the name "Java 8 Update"; however, it does not uninstall "Java 8 Update 45" of the software. + NOTE: if only specifying a single row in the two-dimensional array, the array must have the extra parentheses and leading comma as in this example. .EXAMPLE - Remove-MSIApplications -Name 'Java 8 Update' -ExcludeFromUninstall - ('Is64BitApplication', $true, 'Exact'), - ('DisplayName', 'Java 8 Update 45', 'Exact'), - ('DisplayName', 'Java 8 Update 4*', 'WildCard'), + Remove-MSIApplications -Name 'Java 8 Update' -ExcludeFromUninstall + ('Is64BitApplication', $true, 'Exact'), + ('DisplayName', 'Java 8 Update 45', 'Exact'), + ('DisplayName', 'Java 8 Update 4*', 'WildCard'), ('DisplayName', 'Java \d Update \d{3}', 'RegEx'), - ('DisplayName', 'Java 8 Update', 'Contains') - Removes all versions of software that match the name "Java 8 Update"; however, it does not uninstall 64-bit versions of the software, Update 45 of the software, or any Update that starts with 4. + ('DisplayName', 'Java 8 Update', 'Contains') + Removes all versions of software that match the name "Java 8 Update"; however, it does not uninstall 64-bit versions of the software, Update 45 of the software, or any Update that starts with 4. .NOTES - More reading on how to create arrays if having trouble with -FilterApplication or -ExcludeFromUninstall parameter: http://blogs.msdn.com/b/powershell/archive/2007/01/23/array-literals-in-powershell.aspx + More reading on how to create arrays if having trouble with -FilterApplication or -ExcludeFromUninstall parameter: http://blogs.msdn.com/b/powershell/archive/2007/01/23/array-literals-in-powershell.aspx .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [switch]$Exact = $false, - [Parameter(Mandatory=$false)] - [switch]$WildCard = $false, - [Parameter(Mandatory=$false)] - [Alias('Arguments')] - [ValidateNotNullorEmpty()] - [string]$Parameters, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$AddParameters, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [array]$FilterApplication = @(@()), - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [array]$ExcludeFromUninstall = @(@()), - [Parameter(Mandatory=$false)] - [switch]$IncludeUpdatesAndHotfixes = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$LoggingOptions, - [Parameter(Mandatory=$false)] - [Alias('LogName')] - [string]$private:LogName, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$PassThru = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Build the hashtable with the options that will be passed to Get-InstalledApplication using splatting - [hashtable]$GetInstalledApplicationSplat = @{ Name = $name } - If ($Exact) { $GetInstalledApplicationSplat.Add( 'Exact', $Exact) } - ElseIf ($WildCard) { $GetInstalledApplicationSplat.Add( 'WildCard', $WildCard) } - If ($IncludeUpdatesAndHotfixes) { $GetInstalledApplicationSplat.Add( 'IncludeUpdatesAndHotfixes', $IncludeUpdatesAndHotfixes) } - - [psobject[]]$installedApplications = Get-InstalledApplication @GetInstalledApplicationSplat - - Write-Log -Message "Found [$($installedApplications.Count)] application(s) that matched the specified criteria [$Name]." -Source ${CmdletName} - - ## Filter the results from Get-InstalledApplication - [Collections.ArrayList]$removeMSIApplications = New-Object -TypeName 'System.Collections.ArrayList' - If (($null -ne $installedApplications) -and ($installedApplications.Count)) { - ForEach ($installedApplication in $installedApplications) { - If ([string]::IsNullOrEmpty($installedApplication.ProductCode)) { - Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName)] because unable to discover MSI ProductCode from application's registry Uninstall subkey [$($installedApplication.UninstallSubkey)]." -Severity 2 -Source ${CmdletName} - Continue - } - - # Filter the results from Get-InstalledApplication to only those that should be uninstalled - If (($null -ne $FilterApplication) -and ($FilterApplication.Count)) { - Write-Log -Message "Filter the results to only those that should be uninstalled as specified in parameter [-FilterApplication]." -Source ${CmdletName} - [boolean]$addAppToRemoveList = $false - ForEach ($Filter in $FilterApplication) { - If ($Filter[2] -eq 'RegEx') { - If ($installedApplication.($Filter[0]) -match $Filter[1]) { - [boolean]$addAppToRemoveList = $true - Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of regex match against [-FilterApplication] criteria." -Source ${CmdletName} - } - } - ElseIf ($Filter[2] -eq 'Contains') { - If ($installedApplication.($Filter[0]) -match [regex]::Escape($Filter[1])) { - [boolean]$addAppToRemoveList = $true - Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of contains match against [-FilterApplication] criteria." -Source ${CmdletName} - } - } - ElseIf ($Filter[2] -eq 'WildCard') { - If ($installedApplication.($Filter[0]) -like $Filter[1]) { - [boolean]$addAppToRemoveList = $true - Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of wildcard match against [-FilterApplication] criteria." -Source ${CmdletName} - } - } - ElseIf ($Filter[2] -eq 'Exact') { - If ($installedApplication.($Filter[0]) -eq $Filter[1]) { - [boolean]$addAppToRemoveList = $true - Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of exact match against [-FilterApplication] criteria." -Source ${CmdletName} - } - } - } - } - Else { - [boolean]$addAppToRemoveList = $true - } - - # Filter the results from Get-InstalledApplication to remove those that should never be uninstalled - If (($null -ne $ExcludeFromUninstall) -and ($ExcludeFromUninstall.Count)) { - ForEach ($Exclude in $ExcludeFromUninstall) { - If ($Exclude[2] -eq 'RegEx') { - If ($installedApplication.($Exclude[0]) -match $Exclude[1]) { - [boolean]$addAppToRemoveList = $false - Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of regex match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} - } - } - ElseIf ($Exclude[2] -eq 'Contains') { - If ($installedApplication.($Exclude[0]) -match [regex]::Escape($Exclude[1])) { - [boolean]$addAppToRemoveList = $false - Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of contains match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} - } - } - ElseIf ($Exclude[2] -eq 'WildCard') { - If ($installedApplication.($Exclude[0]) -like $Exclude[1]) { - [boolean]$addAppToRemoveList = $false - Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of wildcard match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} - } - } - ElseIf ($Exclude[2] -eq 'Exact') { - If ($installedApplication.($Exclude[0]) -eq $Exclude[1]) { - [boolean]$addAppToRemoveList = $false - Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of exact match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} - } - } - } - } - - If ($addAppToRemoveList) { - Write-Log -Message "Adding application to list for removal: [$($installedApplication.DisplayName) $($installedApplication.Version)]." -Source ${CmdletName} - $removeMSIApplications.Add($installedApplication) - } - } - } - - ## Build the hashtable with the options that will be passed to Execute-MSI using splatting - [hashtable]$ExecuteMSISplat = @{ Action = 'Uninstall'; Path = '' } - If ($ContinueOnError) { $ExecuteMSISplat.Add( 'ContinueOnError', $ContinueOnError) } - If ($Parameters) { $ExecuteMSISplat.Add( 'Parameters', $Parameters) } - ElseIf ($AddParameters) { $ExecuteMSISplat.Add( 'AddParameters', $AddParameters) } - If ($LoggingOptions) { $ExecuteMSISplat.Add( 'LoggingOptions', $LoggingOptions) } - If ($LogName) { $ExecuteMSISplat.Add( 'LogName', $LogName) } - If ($PassThru) { $ExecuteMSISplat.Add( 'PassThru', $PassThru) } - If ($IncludeUpdatesAndHotfixes) { $ExecuteMSISplat.Add( 'IncludeUpdatesAndHotfixes', $IncludeUpdatesAndHotfixes) } - - If (($null -ne $removeMSIApplications) -and ($removeMSIApplications.Count)) { - ForEach ($removeMSIApplication in $removeMSIApplications) { - Write-Log -Message "Remove application [$($removeMSIApplication.DisplayName) $($removeMSIApplication.Version)]." -Source ${CmdletName} - $ExecuteMSISplat.Path = $removeMSIApplication.ProductCode - If ($PassThru) { - [psobject[]]$ExecuteResults += Execute-MSI @ExecuteMSISplat - } - Else { - Execute-MSI @ExecuteMSISplat - } - } - } - Else { - Write-Log -Message 'No applications found for removal. Continue...' -Source ${CmdletName} - } - } - End { - If ($PassThru) { Write-Output -InputObject $ExecuteResults } - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [switch]$Exact = $false, + [Parameter(Mandatory=$false)] + [switch]$WildCard = $false, + [Parameter(Mandatory=$false)] + [Alias('Arguments')] + [ValidateNotNullorEmpty()] + [string]$Parameters, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$AddParameters, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [array]$FilterApplication = @(@()), + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [array]$ExcludeFromUninstall = @(@()), + [Parameter(Mandatory=$false)] + [switch]$IncludeUpdatesAndHotfixes = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$LoggingOptions, + [Parameter(Mandatory=$false)] + [Alias('LogName')] + [string]$private:LogName, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$PassThru = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Build the hashtable with the options that will be passed to Get-InstalledApplication using splatting + [hashtable]$GetInstalledApplicationSplat = @{ Name = $name } + If ($Exact) { $GetInstalledApplicationSplat.Add( 'Exact', $Exact) } + ElseIf ($WildCard) { $GetInstalledApplicationSplat.Add( 'WildCard', $WildCard) } + If ($IncludeUpdatesAndHotfixes) { $GetInstalledApplicationSplat.Add( 'IncludeUpdatesAndHotfixes', $IncludeUpdatesAndHotfixes) } + + [psobject[]]$installedApplications = Get-InstalledApplication @GetInstalledApplicationSplat + + Write-Log -Message "Found [$($installedApplications.Count)] application(s) that matched the specified criteria [$Name]." -Source ${CmdletName} + + ## Filter the results from Get-InstalledApplication + [Collections.ArrayList]$removeMSIApplications = New-Object -TypeName 'System.Collections.ArrayList' + If (($null -ne $installedApplications) -and ($installedApplications.Count)) { + ForEach ($installedApplication in $installedApplications) { + If ([string]::IsNullOrEmpty($installedApplication.ProductCode)) { + Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName)] because unable to discover MSI ProductCode from application's registry Uninstall subkey [$($installedApplication.UninstallSubkey)]." -Severity 2 -Source ${CmdletName} + Continue + } + + # Filter the results from Get-InstalledApplication to only those that should be uninstalled + If (($null -ne $FilterApplication) -and ($FilterApplication.Count)) { + Write-Log -Message "Filter the results to only those that should be uninstalled as specified in parameter [-FilterApplication]." -Source ${CmdletName} + [boolean]$addAppToRemoveList = $false + ForEach ($Filter in $FilterApplication) { + If ($Filter[2] -eq 'RegEx') { + If ($installedApplication.($Filter[0]) -match $Filter[1]) { + [boolean]$addAppToRemoveList = $true + Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of regex match against [-FilterApplication] criteria." -Source ${CmdletName} + } + } + ElseIf ($Filter[2] -eq 'Contains') { + If ($installedApplication.($Filter[0]) -match [regex]::Escape($Filter[1])) { + [boolean]$addAppToRemoveList = $true + Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of contains match against [-FilterApplication] criteria." -Source ${CmdletName} + } + } + ElseIf ($Filter[2] -eq 'WildCard') { + If ($installedApplication.($Filter[0]) -like $Filter[1]) { + [boolean]$addAppToRemoveList = $true + Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of wildcard match against [-FilterApplication] criteria." -Source ${CmdletName} + } + } + ElseIf ($Filter[2] -eq 'Exact') { + If ($installedApplication.($Filter[0]) -eq $Filter[1]) { + [boolean]$addAppToRemoveList = $true + Write-Log -Message "Preserve removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of exact match against [-FilterApplication] criteria." -Source ${CmdletName} + } + } + } + } + Else { + [boolean]$addAppToRemoveList = $true + } + + # Filter the results from Get-InstalledApplication to remove those that should never be uninstalled + If (($null -ne $ExcludeFromUninstall) -and ($ExcludeFromUninstall.Count)) { + ForEach ($Exclude in $ExcludeFromUninstall) { + If ($Exclude[2] -eq 'RegEx') { + If ($installedApplication.($Exclude[0]) -match $Exclude[1]) { + [boolean]$addAppToRemoveList = $false + Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of regex match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} + } + } + ElseIf ($Exclude[2] -eq 'Contains') { + If ($installedApplication.($Exclude[0]) -match [regex]::Escape($Exclude[1])) { + [boolean]$addAppToRemoveList = $false + Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of contains match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} + } + } + ElseIf ($Exclude[2] -eq 'WildCard') { + If ($installedApplication.($Exclude[0]) -like $Exclude[1]) { + [boolean]$addAppToRemoveList = $false + Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of wildcard match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} + } + } + ElseIf ($Exclude[2] -eq 'Exact') { + If ($installedApplication.($Exclude[0]) -eq $Exclude[1]) { + [boolean]$addAppToRemoveList = $false + Write-Log -Message "Skipping removal of application [$($installedApplication.DisplayName) $($installedApplication.Version)] because of exact match against [-ExcludeFromUninstall] criteria." -Source ${CmdletName} + } + } + } + } + + If ($addAppToRemoveList) { + Write-Log -Message "Adding application to list for removal: [$($installedApplication.DisplayName) $($installedApplication.Version)]." -Source ${CmdletName} + $removeMSIApplications.Add($installedApplication) + } + } + } + + ## Build the hashtable with the options that will be passed to Execute-MSI using splatting + [hashtable]$ExecuteMSISplat = @{ Action = 'Uninstall'; Path = '' } + If ($ContinueOnError) { $ExecuteMSISplat.Add( 'ContinueOnError', $ContinueOnError) } + If ($Parameters) { $ExecuteMSISplat.Add( 'Parameters', $Parameters) } + ElseIf ($AddParameters) { $ExecuteMSISplat.Add( 'AddParameters', $AddParameters) } + If ($LoggingOptions) { $ExecuteMSISplat.Add( 'LoggingOptions', $LoggingOptions) } + If ($LogName) { $ExecuteMSISplat.Add( 'LogName', $LogName) } + If ($PassThru) { $ExecuteMSISplat.Add( 'PassThru', $PassThru) } + If ($IncludeUpdatesAndHotfixes) { $ExecuteMSISplat.Add( 'IncludeUpdatesAndHotfixes', $IncludeUpdatesAndHotfixes) } + + If (($null -ne $removeMSIApplications) -and ($removeMSIApplications.Count)) { + ForEach ($removeMSIApplication in $removeMSIApplications) { + Write-Log -Message "Remove application [$($removeMSIApplication.DisplayName) $($removeMSIApplication.Version)]." -Source ${CmdletName} + $ExecuteMSISplat.Path = $removeMSIApplication.ProductCode + If ($PassThru) { + [psobject[]]$ExecuteResults += Execute-MSI @ExecuteMSISplat + } + Else { + Execute-MSI @ExecuteMSISplat + } + } + } + Else { + Write-Log -Message 'No applications found for removal. Continue...' -Source ${CmdletName} + } + } + End { + If ($PassThru) { Write-Output -InputObject $ExecuteResults } + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -2802,369 +2895,406 @@ Function Remove-MSIApplications { Function Execute-Process { <# .SYNOPSIS - Execute a process with optional arguments, working directory, window style. + Execute a process with optional arguments, working directory, window style. .DESCRIPTION - Executes a process, e.g. a file included in the Files directory of the App Deploy Toolkit, or a file on the local machine. - Provides various options for handling the return codes (see Parameters). + Executes a process, e.g. a file included in the Files directory of the App Deploy Toolkit, or a file on the local machine. + Provides various options for handling the return codes (see Parameters). .PARAMETER Path - Path to the file to be executed. If the file is located directly in the "Files" directory of the App Deploy Toolkit, only the file name needs to be specified. - Otherwise, the full path of the file must be specified. If the files is in a subdirectory of "Files", use the "$dirFiles" variable as shown in the example. + Path to the file to be executed. If the file is located directly in the "Files" directory of the App Deploy Toolkit, only the file name needs to be specified. + Otherwise, the full path of the file must be specified. If the files is in a subdirectory of "Files", use the "$dirFiles" variable as shown in the example. .PARAMETER Parameters - Arguments to be passed to the executable + Arguments to be passed to the executable .PARAMETER SecureParameters - Hides all parameters passed to the executable from the Toolkit log file + Hides all parameters passed to the executable from the Toolkit log file .PARAMETER WindowStyle - Style of the window of the process executed. Options: Normal, Hidden, Maximized, Minimized. Default: Normal. - Note: Not all processes honor the "Hidden" flag. If it it not working, then check the command line options for the process being executed to see it has a silent option. + Style of the window of the process executed. Options: Normal, Hidden, Maximized, Minimized. Default: Normal. + Note: Not all processes honor WindowStyle. WindowStyle is a recommendation passed to the process. They can choose to ignore it. + Only works for native Windows GUI applications. If the WindowStyle is set to Hidden, UseShellExecute should be set to $true. .PARAMETER CreateNoWindow - Specifies whether the process should be started with a new window to contain it. Default is false. + Specifies whether the process should be started with a new window to contain it. Only works for Console mode applications. UseShellExecute should be set to $false. + Default is false. .PARAMETER WorkingDirectory - The working directory used for executing the process. Defaults to the directory of the file being executed. + The working directory used for executing the process. Defaults to the directory of the file being executed. + Parameter UseShellExecute affects this parameter. .PARAMETER NoWait - Immediately continue after executing the process. + Immediately continue after executing the process. .PARAMETER PassThru - Returns ExitCode, STDOut, and STDErr output from the process. + If NoWait is not specified, returns an object with ExitCode, STDOut and STDErr output from the process. If NoWait is specified, returns an object with Id, Handle and ProcessName. .PARAMETER WaitForMsiExec - Sometimes an EXE bootstrapper will launch an MSI install. In such cases, this variable will ensure that - this function waits for the msiexec engine to become available before starting the install. + Sometimes an EXE bootstrapper will launch an MSI install. In such cases, this variable will ensure that + this function waits for the msiexec engine to become available before starting the install. .PARAMETER MsiExecWaitTime - Specify the length of time in seconds to wait for the msiexec engine to become available. Default: 600 seconds (10 minutes). + Specify the length of time in seconds to wait for the msiexec engine to become available. Default: 600 seconds (10 minutes). .PARAMETER IgnoreExitCodes - List the exit codes to ignore or * to ignore all exit codes. + List the exit codes to ignore or * to ignore all exit codes. .PARAMETER PriorityClass - Specifies priority class for the process. Options: Idle, Normal, High, AboveNormal, BelowNormal, RealTime. Default: Normal + Specifies priority class for the process. Options: Idle, Normal, High, AboveNormal, BelowNormal, RealTime. Default: Normal .PARAMETER ExitOnProcessFailure - Specifies whether the function should call Exit-Script when the process returns an exit code that is considered an error/failure. Default: $true + Specifies whether the function should call Exit-Script when the process returns an exit code that is considered an error/failure. Default: $true +.PARAMETER UseShellExecute + Specifies whether to use the operating system shell to start the process. $true if the shell should be used when starting the process; $false if the process should be created directly from the executable file. + The word "Shell" in this context refers to a graphical shell (similar to the Windows shell) rather than command shells (for example, bash or sh) and lets users launch graphical applications or open documents. + It lets you open a file or a url and the Shell will figure out the program to open it with. + The WorkingDirectory property behaves differently depending on the value of the UseShellExecute property. When UseShellExecute is true, the WorkingDirectory property specifies the location of the executable. + When UseShellExecute is false, the WorkingDirectory property is not used to find the executable. Instead, it is used only by the process that is started and has meaning only within the context of the new process. + If you set UseShellExecute to $true, there will be no available output from the process. + Default: $false .PARAMETER ContinueOnError - Continue if an error occured while trying to start the process. Default: $false. + Continue if an error occured while trying to start the process. Default: $false. .EXAMPLE - Execute-Process -Path 'uninstall_flash_player_64bit.exe' -Parameters '/uninstall' -WindowStyle 'Hidden' - If the file is in the "Files" directory of the App Deploy Toolkit, only the file name needs to be specified. + Execute-Process -Path 'uninstall_flash_player_64bit.exe' -Parameters '/uninstall' -WindowStyle 'Hidden' + If the file is in the "Files" directory of the App Deploy Toolkit, only the file name needs to be specified. .EXAMPLE - Execute-Process -Path "$dirFiles\Bin\setup.exe" -Parameters '/S' -WindowStyle 'Hidden' + Execute-Process -Path "$dirFiles\Bin\setup.exe" -Parameters '/S' -WindowStyle 'Hidden' .EXAMPLE - Execute-Process -Path 'setup.exe' -Parameters '/S' -IgnoreExitCodes '1,2' + Execute-Process -Path 'setup.exe' -Parameters '/S' -IgnoreExitCodes '1,2' .EXAMPLE - Execute-Process -Path 'setup.exe' -Parameters "-s -f2`"$configToolkitLogDir\$installName.log`"" - Launch InstallShield "setup.exe" from the ".\Files" sub-directory and force log files to the logging folder. + Execute-Process -Path 'setup.exe' -Parameters "-s -f2`"$configToolkitLogDir\$installName.log`"" + Launch InstallShield "setup.exe" from the ".\Files" sub-directory and force log files to the logging folder. .EXAMPLE - Execute-Process -Path 'setup.exe' -Parameters "/s /v`"ALLUSERS=1 /qn /L* \`"$configToolkitLogDir\$installName.log`"`"" - Launch InstallShield "setup.exe" with embedded MSI and force log files to the logging folder. + Execute-Process -Path 'setup.exe' -Parameters "/s /v`"ALLUSERS=1 /qn /L* \`"$configToolkitLogDir\$installName.log`"`"" + Launch InstallShield "setup.exe" with embedded MSI and force log files to the logging folder. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [Alias('FilePath')] - [ValidateNotNullorEmpty()] - [string]$Path, - [Parameter(Mandatory=$false)] - [Alias('Arguments')] - [ValidateNotNullorEmpty()] - [string[]]$Parameters, - [Parameter(Mandatory=$false)] - [switch]$SecureParameters = $false, - [Parameter(Mandatory=$false)] - [ValidateSet('Normal','Hidden','Maximized','Minimized')] - [Diagnostics.ProcessWindowStyle]$WindowStyle = 'Normal', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$CreateNoWindow = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$WorkingDirectory, - [Parameter(Mandatory=$false)] - [switch]$NoWait = $false, - [Parameter(Mandatory=$false)] - [switch]$PassThru = $false, - [Parameter(Mandatory=$false)] - [switch]$WaitForMsiExec = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int]$MsiExecWaitTime = $configMSIMutexWaitTime, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$IgnoreExitCodes, - [Parameter(Mandatory=$false)] - [ValidateSet('Idle', 'Normal', 'High', 'AboveNormal', 'BelowNormal', 'RealTime')] - [Diagnostics.ProcessPriorityClass]$PriorityClass = 'Normal', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ExitOnProcessFailure = $true, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - $private:returnCode = $null - - ## Validate and find the fully qualified path for the $Path variable. - If (([IO.Path]::IsPathRooted($Path)) -and ([IO.Path]::HasExtension($Path))) { - Write-Log -Message "[$Path] is a valid fully qualified path, continue." -Source ${CmdletName} - If (-not (Test-Path -LiteralPath $Path -PathType 'Leaf' -ErrorAction 'Stop')) { - Write-Log -Message "File [$Path] not found." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "File [$Path] not found." - } - Return - } - } - Else { - # The first directory to search will be the 'Files' subdirectory of the script directory - [string]$PathFolders = $dirFiles - # Add the current location of the console (Windows always searches this location first) - [string]$PathFolders = $PathFolders + ';' + (Get-Location -PSProvider 'FileSystem').Path - # Add the new path locations to the PATH environment variable - $env:PATH = $PathFolders + ';' + $env:PATH - - # Get the fully qualified path for the file. Get-Command searches PATH environment variable to find this value. - [string]$FullyQualifiedPath = Get-Command -Name $Path -CommandType 'Application' -TotalCount 1 -Syntax -ErrorAction 'Stop' - - # Revert the PATH environment variable to it's original value - $env:PATH = $env:PATH -replace [regex]::Escape($PathFolders + ';'), '' - - If ($FullyQualifiedPath) { - Write-Log -Message "[$Path] successfully resolved to fully qualified path [$FullyQualifiedPath]." -Source ${CmdletName} - $Path = $FullyQualifiedPath - } - Else { - Write-Log -Message "[$Path] contains an invalid path or file name." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "[$Path] contains an invalid path or file name." - } - Return - } - } - - ## Set the Working directory (if not specified) - If (-not $WorkingDirectory) { $WorkingDirectory = Split-Path -Path $Path -Parent -ErrorAction 'Stop' } - - ## If MSI install, check to see if the MSI installer service is available or if another MSI install is already underway. - ## Please note that a race condition is possible after this check where another process waiting for the MSI installer - ## to become available grabs the MSI Installer mutex before we do. Not too concerned about this possible race condition. - If (($Path -match 'msiexec') -or ($WaitForMsiExec)) { - [timespan]$MsiExecWaitTimeSpan = New-TimeSpan -Seconds $MsiExecWaitTime - [boolean]$MsiExecAvailable = Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds $MsiExecWaitTimeSpan.TotalMilliseconds - Start-Sleep -Seconds 1 - If (-not $MsiExecAvailable) { - # Default MSI exit code for install already in progress - [int32]$returnCode = 1618 - Write-Log -Message "Another MSI installation is already in progress and needs to be completed before proceeding with this installation." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw 'Another MSI installation is already in progress and needs to be completed before proceeding with this installation.' - } - Return - } - } - - Try { - ## Disable Zone checking to prevent warnings when running executables - $env:SEE_MASK_NOZONECHECKS = 1 - - ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function. - $private:previousErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Stop' - - ## Define process - $processStartInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' -ErrorAction 'Stop' - $processStartInfo.FileName = $Path - $processStartInfo.WorkingDirectory = $WorkingDirectory - $processStartInfo.UseShellExecute = $false - $processStartInfo.ErrorDialog = $false - $processStartInfo.RedirectStandardOutput = $true - $processStartInfo.RedirectStandardError = $true - $processStartInfo.CreateNoWindow = $CreateNoWindow - If ($Parameters) { $processStartInfo.Arguments = $Parameters } - If ($windowStyle) { $processStartInfo.WindowStyle = $WindowStyle } - $process = New-Object -TypeName 'System.Diagnostics.Process' -ErrorAction 'Stop' - $process.StartInfo = $processStartInfo - - ## Add event handler to capture process's standard output redirection - [scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } } - $stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' - $stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop' - $stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' - $stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop' - - ## Start Process - Write-Log -Message "Working Directory is [$WorkingDirectory]." -Source ${CmdletName} - If ($Parameters) { - If ($Parameters -match '-Command \&') { - Write-Log -Message "Executing [$Path [PowerShell ScriptBlock]]..." -Source ${CmdletName} - } - Else { - If ($SecureParameters) { - Write-Log -Message "Executing [$Path (Parameters Hidden)]..." -Source ${CmdletName} - } - Else { - Write-Log -Message "Executing [$Path $Parameters]..." -Source ${CmdletName} - } - } - } - Else { - Write-Log -Message "Executing [$Path]..." -Source ${CmdletName} - } - - $null = $process.Start() - - If ($PriorityClass -ne "Normal") { - try { - If ($process.HasExited -eq $False) { - Write-Log -Message "Changing the priority class for the process to [$PriorityClass]" -Source ${CmdletName} - $process.PriorityClass = $PriorityClass - } - Else { - Write-Log -Message "Cannot change the priority class for the process to [$PriorityClass], because the process has exited already." -Severity 2 -Source ${CmdletName} - } - - } - catch { - Write-Log -Message "Failed to change the priority class for the process." -Severity 2 -Source ${CmdletName} - } - } - - If ($NoWait) { - Write-Log -Message 'NoWait parameter specified. Continuing without waiting for exit code...' -Source ${CmdletName} - } - Else { - $process.BeginOutputReadLine() - $process.BeginErrorReadLine() - - ## Instructs the Process component to wait indefinitely for the associated process to exit. - $process.WaitForExit() - - ## HasExited indicates that the associated process has terminated, either normally or abnormally. Wait until HasExited returns $true. - While (-not ($process.HasExited)) { $process.Refresh(); Start-Sleep -Seconds 1 } - - ## Get the exit code for the process - Try { - [int32]$returnCode = $process.ExitCode - } - Catch [System.Management.Automation.PSInvalidCastException] { - # Catch exit codes that are out of int32 range - [int32]$returnCode = 60013 - } - - ## Unregister standard output and error event to retrieve process output - If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } - If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } - $stdOut = $stdOutBuilder.ToString() -replace $null,'' - $stdErr = $stdErrBuilder.ToString() -replace $null,'' - - If ($stdErr.Length -gt 0) { - Write-Log -Message "Standard error output from the process: $stdErr" -Severity 3 -Source ${CmdletName} - } - } - } - Finally { - ## Make sure the standard output and error event is unregistered - If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } - If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } - ## Free resources associated with the process, this does not cause process to exit - If ($process) { $process.Dispose() } - - ## Re-enable Zone checking - Remove-Item -LiteralPath 'env:SEE_MASK_NOZONECHECKS' -ErrorAction 'SilentlyContinue' - - If ($private:previousErrorActionPreference) { $ErrorActionPreference = $private:previousErrorActionPreference } - } - - If (-not $NoWait) { - ## Check to see whether we should ignore exit codes - $ignoreExitCodeMatch = $false - If ($ignoreExitCodes) { - ## Check whether * was specified, which would tell us to ignore all exit codes - If ($ignoreExitCodes.Trim() -eq "*") { - $ignoreExitCodeMatch = $true - } - Else { - ## Split the processes on a comma - [int32[]]$ignoreExitCodesArray = $ignoreExitCodes -split ',' - ForEach ($ignoreCode in $ignoreExitCodesArray) { - If ($returnCode -eq $ignoreCode) { $ignoreExitCodeMatch = $true } - } - } - } - - ## If the passthru switch is specified, return the exit code and any output from process - If ($PassThru) { - Write-Log -Message "-PassThru parameter specified, returning execution results object." -Source ${CmdletName} - [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = $stdOut; StdErr = $stdErr } - Write-Output -InputObject $ExecutionResults - } - - If ($ignoreExitCodeMatch) { - Write-Log -Message "Execution completed and the exit code [$returncode] is being ignored." -Source ${CmdletName} - } - ElseIf (($returnCode -eq 3010) -or ($returnCode -eq 1641)) { - Write-Log -Message "Execution completed successfully with exit code [$returnCode]. A reboot is required." -Severity 2 -Source ${CmdletName} - Set-Variable -Name 'msiRebootDetected' -Value $true -Scope 'Script' - } - ElseIf (($returnCode -eq 1605) -and ($Path -match 'msiexec')) { - Write-Log -Message "Execution failed with exit code [$returnCode] because the product is not currently installed." -Severity 3 -Source ${CmdletName} - } - ElseIf (($returnCode -eq -2145124329) -and ($Path -match 'wusa')) { - Write-Log -Message "Execution failed with exit code [$returnCode] because the Windows Update is not applicable to this system." -Severity 3 -Source ${CmdletName} - } - ElseIf (($returnCode -eq 17025) -and ($Path -match 'fullfile')) { - Write-Log -Message "Execution failed with exit code [$returnCode] because the Office Update is not applicable to this system." -Severity 3 -Source ${CmdletName} - } - ElseIf ($returnCode -eq 0) { - Write-Log -Message "Execution completed successfully with exit code [$returnCode]." -Source ${CmdletName} - } - Else { - [string]$MsiExitCodeMessage = '' - If ($Path -match 'msiexec') { - [string]$MsiExitCodeMessage = Get-MsiExitCodeMessage -MsiExitCode $returnCode - } - - If ($MsiExitCodeMessage) { - Write-Log -Message "Execution failed with exit code [$returnCode]: $MsiExitCodeMessage" -Severity 3 -Source ${CmdletName} - } - Else { - Write-Log -Message "Execution failed with exit code [$returnCode]." -Severity 3 -Source ${CmdletName} - } - - If ($ExitOnProcessFailure) { - Exit-Script -ExitCode $returnCode - } - } - } - } - Catch { - If ([string]::IsNullOrEmpty([string]$returnCode)) { - [int32]$returnCode = 60002 - Write-Log -Message "Function failed, setting exit code to [$returnCode]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Function failed, setting exit code to [$returnCode]. `n$(Resolve-Error)" - } - } - Else { - Write-Log -Message "Execution completed with exit code [$returnCode]. Function failed. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - If ($PassThru) { - [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = If ($stdOut) { $stdOut } Else { '' }; StdErr = If ($stdErr) { $stdErr } Else { '' } } - Write-Output -InputObject $ExecutionResults - } - - If ($ExitOnProcessFailure) { - Exit-Script -ExitCode $returnCode - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [Alias('FilePath')] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [Alias('Arguments')] + [ValidateNotNullorEmpty()] + [string[]]$Parameters, + [Parameter(Mandatory=$false)] + [switch]$SecureParameters = $false, + [Parameter(Mandatory=$false)] + [ValidateSet('Normal','Hidden','Maximized','Minimized')] + [Diagnostics.ProcessWindowStyle]$WindowStyle = 'Normal', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$CreateNoWindow = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$WorkingDirectory, + [Parameter(Mandatory=$false)] + [switch]$NoWait = $false, + [Parameter(Mandatory=$false)] + [switch]$PassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$WaitForMsiExec = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int]$MsiExecWaitTime = $configMSIMutexWaitTime, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$IgnoreExitCodes, + [Parameter(Mandatory=$false)] + [ValidateSet('Idle', 'Normal', 'High', 'AboveNormal', 'BelowNormal', 'RealTime')] + [Diagnostics.ProcessPriorityClass]$PriorityClass = 'Normal', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ExitOnProcessFailure = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$UseShellExecute = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + $private:returnCode = $null + + ## Validate and find the fully qualified path for the $Path variable. + If (([IO.Path]::IsPathRooted($Path)) -and ([IO.Path]::HasExtension($Path))) { + Write-Log -Message "[$Path] is a valid fully qualified path, continue." -Source ${CmdletName} + If (-not (Test-Path -LiteralPath $Path -PathType 'Leaf' -ErrorAction 'Stop')) { + Write-Log -Message "File [$Path] not found." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "File [$Path] not found." + } + Return + } + } + Else { + # The first directory to search will be the 'Files' subdirectory of the script directory + [string]$PathFolders = $dirFiles + # Add the current location of the console (Windows always searches this location first) + [string]$PathFolders = $PathFolders + ';' + (Get-Location -PSProvider 'FileSystem').Path + # Add the new path locations to the PATH environment variable + $env:PATH = $PathFolders + ';' + $env:PATH + + # Get the fully qualified path for the file. Get-Command searches PATH environment variable to find this value. + [string]$FullyQualifiedPath = Get-Command -Name $Path -CommandType 'Application' -TotalCount 1 -Syntax -ErrorAction 'Stop' + + # Revert the PATH environment variable to it's original value + $env:PATH = $env:PATH -replace [regex]::Escape($PathFolders + ';'), '' + + If ($FullyQualifiedPath) { + Write-Log -Message "[$Path] successfully resolved to fully qualified path [$FullyQualifiedPath]." -Source ${CmdletName} + $Path = $FullyQualifiedPath + } + Else { + Write-Log -Message "[$Path] contains an invalid path or file name." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "[$Path] contains an invalid path or file name." + } + Return + } + } + + ## Set the Working directory (if not specified) + If (-not $WorkingDirectory) { $WorkingDirectory = Split-Path -Path $Path -Parent -ErrorAction 'Stop' } + + ## If MSI install, check to see if the MSI installer service is available or if another MSI install is already underway. + ## Please note that a race condition is possible after this check where another process waiting for the MSI installer + ## to become available grabs the MSI Installer mutex before we do. Not too concerned about this possible race condition. + If (($Path -match 'msiexec') -or ($WaitForMsiExec)) { + [timespan]$MsiExecWaitTimeSpan = New-TimeSpan -Seconds $MsiExecWaitTime + [boolean]$MsiExecAvailable = Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds $MsiExecWaitTimeSpan.TotalMilliseconds + Start-Sleep -Seconds 1 + If (-not $MsiExecAvailable) { + # Default MSI exit code for install already in progress + [int32]$returnCode = 1618 + Write-Log -Message "Another MSI installation is already in progress and needs to be completed before proceeding with this installation." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw 'Another MSI installation is already in progress and needs to be completed before proceeding with this installation.' + } + Return + } + } + + Try { + ## Disable Zone checking to prevent warnings when running executables + $env:SEE_MASK_NOZONECHECKS = 1 + + ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function. + $private:previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + + ## Define process + $processStartInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' -ErrorAction 'Stop' + $processStartInfo.FileName = $Path + $processStartInfo.WorkingDirectory = $WorkingDirectory + $processStartInfo.UseShellExecute = $UseShellExecute + $processStartInfo.ErrorDialog = $false + $processStartInfo.RedirectStandardOutput = $true + $processStartInfo.RedirectStandardError = $true + $processStartInfo.CreateNoWindow = $CreateNoWindow + If ($Parameters) { $processStartInfo.Arguments = $Parameters } + $processStartInfo.WindowStyle = $WindowStyle + If ($processStartInfo.UseShellExecute -eq $true) { + Write-Log -Message "UseShellExecute is set to true, standard output and error will not be available." -Source ${CmdletName} + $processStartInfo.RedirectStandardOutput = $false + $processStartInfo.RedirectStandardError = $false + } + $process = New-Object -TypeName 'System.Diagnostics.Process' -ErrorAction 'Stop' + $process.StartInfo = $processStartInfo + + If ($processStartInfo.UseShellExecute -eq $false) { + ## Add event handler to capture process's standard output redirection + [scriptblock]$processEventHandler = { If (-not [string]::IsNullOrEmpty($EventArgs.Data)) { $Event.MessageData.AppendLine($EventArgs.Data) } } + $stdOutBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' + $stdOutEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'OutputDataReceived' -MessageData $stdOutBuilder -ErrorAction 'Stop' + $stdErrBuilder = New-Object -TypeName 'System.Text.StringBuilder' -ArgumentList '' + $stdErrEvent = Register-ObjectEvent -InputObject $process -Action $processEventHandler -EventName 'ErrorDataReceived' -MessageData $stdErrBuilder -ErrorAction 'Stop' + } + + ## Start Process + Write-Log -Message "Working Directory is [$WorkingDirectory]." -Source ${CmdletName} + If ($Parameters) { + If ($Parameters -match '-Command \&') { + Write-Log -Message "Executing [$Path [PowerShell ScriptBlock]]..." -Source ${CmdletName} + } + Else { + If ($SecureParameters) { + Write-Log -Message "Executing [$Path (Parameters Hidden)]..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Executing [$Path $Parameters]..." -Source ${CmdletName} + } + } + } + Else { + Write-Log -Message "Executing [$Path]..." -Source ${CmdletName} + } + + $null = $process.Start() + ## Set priority + If ($PriorityClass -ne "Normal") { + try { + If ($process.HasExited -eq $False) { + Write-Log -Message "Changing the priority class for the process to [$PriorityClass]" -Source ${CmdletName} + $process.PriorityClass = $PriorityClass + } + Else { + Write-Log -Message "Cannot change the priority class for the process to [$PriorityClass], because the process has exited already." -Severity 2 -Source ${CmdletName} + } + + } + catch { + Write-Log -Message "Failed to change the priority class for the process." -Severity 2 -Source ${CmdletName} + } + } + ## NoWait specified, return process details. If it isnt specified, start reading standard Output and Error streams + If ($NoWait) { + Write-Log -Message 'NoWait parameter specified. Continuing without waiting for exit code...' -Source ${CmdletName} + + If ($PassThru) { + If ($process.HasExited -eq $false) { + Write-Log -Message "PassThru parameter specified, returning process details object." -Source ${CmdletName} + [psobject]$ProcessDetails = New-Object -TypeName 'PSObject' -Property @{ Id = If ($process.Id) {$process.Id} Else { $null } ; Handle = If ($process.Handle) { $process.Handle } Else { [IntPtr]::Zero }; ProcessName = If ($process.ProcessName) { $process.ProcessName } Else { '' } } + Write-Output -InputObject $ProcessDetails + } + Else { + Write-Log -Message "PassThru parameter specified, however the process has already exited." -Source ${CmdletName} + } + } + } + Else { + If ($processStartInfo.UseShellExecute -eq $false) { + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() + } + ## Instructs the Process component to wait indefinitely for the associated process to exit. + $process.WaitForExit() + + ## HasExited indicates that the associated process has terminated, either normally or abnormally. Wait until HasExited returns $true. + While (-not ($process.HasExited)) { $process.Refresh(); Start-Sleep -Seconds 1 } + + ## Get the exit code for the process + Try { + [int32]$returnCode = $process.ExitCode + } + Catch [System.Management.Automation.PSInvalidCastException] { + # Catch exit codes that are out of int32 range + [int32]$returnCode = 60013 + } + + If ($processStartInfo.UseShellExecute -eq $false) { + ## Unregister standard output and error event to retrieve process output + If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } + If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } + $stdOut = $stdOutBuilder.ToString() -replace $null,'' + $stdErr = $stdErrBuilder.ToString() -replace $null,'' + + If ($stdErr.Length -gt 0) { + Write-Log -Message "Standard error output from the process: $stdErr" -Severity 3 -Source ${CmdletName} + } + } + } + } + Finally { + If ($processStartInfo.UseShellExecute -eq $false) { + ## Make sure the standard output and error event is unregistered + If ($stdOutEvent) { Unregister-Event -SourceIdentifier $stdOutEvent.Name -ErrorAction 'Stop'; $stdOutEvent = $null } + If ($stdErrEvent) { Unregister-Event -SourceIdentifier $stdErrEvent.Name -ErrorAction 'Stop'; $stdErrEvent = $null } + } + ## Free resources associated with the process, this does not cause process to exit + If ($process) { $process.Dispose() } + + ## Re-enable Zone checking + Remove-Item -LiteralPath 'env:SEE_MASK_NOZONECHECKS' -ErrorAction 'SilentlyContinue' + + If ($private:previousErrorActionPreference) { $ErrorActionPreference = $private:previousErrorActionPreference } + } + + If (-not $NoWait) { + ## Check to see whether we should ignore exit codes + $ignoreExitCodeMatch = $false + If ($ignoreExitCodes) { + ## Check whether * was specified, which would tell us to ignore all exit codes + If ($ignoreExitCodes.Trim() -eq "*") { + $ignoreExitCodeMatch = $true + } + Else { + ## Split the processes on a comma + [int32[]]$ignoreExitCodesArray = $ignoreExitCodes -split ',' + ForEach ($ignoreCode in $ignoreExitCodesArray) { + If ($returnCode -eq $ignoreCode) { $ignoreExitCodeMatch = $true } + } + } + } + + ## If the passthru switch is specified, return the exit code and any output from process + If ($PassThru) { + Write-Log -Message "PassThru parameter specified, returning execution results object." -Source ${CmdletName} + [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = If ($stdOut) { $stdOut } Else { '' }; StdErr = If ($stdErr) { $stdErr } Else { '' } } + Write-Output -InputObject $ExecutionResults + } + + If ($ignoreExitCodeMatch) { + Write-Log -Message "Execution completed and the exit code [$returncode] is being ignored." -Source ${CmdletName} + } + ElseIf (($returnCode -eq 3010) -or ($returnCode -eq 1641)) { + Write-Log -Message "Execution completed successfully with exit code [$returnCode]. A reboot is required." -Severity 2 -Source ${CmdletName} + Set-Variable -Name 'msiRebootDetected' -Value $true -Scope 'Script' + } + ElseIf (($returnCode -eq 1605) -and ($Path -match 'msiexec')) { + Write-Log -Message "Execution failed with exit code [$returnCode] because the product is not currently installed." -Severity 3 -Source ${CmdletName} + } + ElseIf (($returnCode -eq -2145124329) -and ($Path -match 'wusa')) { + Write-Log -Message "Execution failed with exit code [$returnCode] because the Windows Update is not applicable to this system." -Severity 3 -Source ${CmdletName} + } + ElseIf (($returnCode -eq 17025) -and ($Path -match 'fullfile')) { + Write-Log -Message "Execution failed with exit code [$returnCode] because the Office Update is not applicable to this system." -Severity 3 -Source ${CmdletName} + } + ElseIf ($returnCode -eq 0) { + Write-Log -Message "Execution completed successfully with exit code [$returnCode]." -Source ${CmdletName} + } + Else { + [string]$MsiExitCodeMessage = '' + If ($Path -match 'msiexec') { + [string]$MsiExitCodeMessage = Get-MsiExitCodeMessage -MsiExitCode $returnCode + } + + If ($MsiExitCodeMessage) { + Write-Log -Message "Execution failed with exit code [$returnCode]: $MsiExitCodeMessage" -Severity 3 -Source ${CmdletName} + } + Else { + Write-Log -Message "Execution failed with exit code [$returnCode]." -Severity 3 -Source ${CmdletName} + } + + If ($ExitOnProcessFailure) { + Exit-Script -ExitCode $returnCode + } + } + } + } + Catch { + If ([string]::IsNullOrEmpty([string]$returnCode)) { + [int32]$returnCode = 60002 + Write-Log -Message "Function failed, setting exit code to [$returnCode]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Function failed, setting exit code to [$returnCode]. `n$(Resolve-Error)" + } + } + Else { + Write-Log -Message "Execution completed with exit code [$returnCode]. Function failed. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + If ($PassThru) { + [psobject]$ExecutionResults = New-Object -TypeName 'PSObject' -Property @{ ExitCode = $returnCode; StdOut = If ($stdOut) { $stdOut } Else { '' }; StdErr = If ($stdErr) { $stdErr } Else { '' } } + Write-Output -InputObject $ExecutionResults + } + + If ($ExitOnProcessFailure) { + Exit-Script -ExitCode $returnCode + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3173,44 +3303,44 @@ Function Execute-Process { Function Get-MsiExitCodeMessage { <# .SYNOPSIS - Get message for MSI error code + Get message for MSI error code .DESCRIPTION - Get message for MSI error code by reading it from msimsg.dll + Get message for MSI error code by reading it from msimsg.dll .PARAMETER MsiErrorCode - MSI error code + MSI error code .EXAMPLE - Get-MsiExitCodeMessage -MsiErrorCode 1618 + Get-MsiExitCodeMessage -MsiErrorCode 1618 .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://msdn.microsoft.com/en-us/library/aa368542(v=vs.85).aspx - http://psappdeploytoolkit.com + http://msdn.microsoft.com/en-us/library/aa368542(v=vs.85).aspx + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [int32]$MsiExitCode - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Get message for exit code [$MsiExitCode]." -Source ${CmdletName} - [string]$MsiExitCodeMsg = [PSADT.Msi]::GetMessageFromMsiExitCode($MsiExitCode) - Write-Output -InputObject $MsiExitCodeMsg - } - Catch { - Write-Log -Message "Failed to get message for exit code [$MsiExitCode]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [int32]$MsiExitCode + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Get message for exit code [$MsiExitCode]." -Source ${CmdletName} + [string]$MsiExitCodeMsg = [PSADT.Msi]::GetMessageFromMsiExitCode($MsiExitCode) + Write-Output -InputObject $MsiExitCodeMsg + } + Catch { + Write-Log -Message "Failed to get message for exit code [$MsiExitCode]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3219,124 +3349,124 @@ Function Get-MsiExitCodeMessage { Function Test-IsMutexAvailable { <# .SYNOPSIS - Wait, up to a timeout value, to check if current thread is able to acquire an exclusive lock on a system mutex. + Wait, up to a timeout value, to check if current thread is able to acquire an exclusive lock on a system mutex. .DESCRIPTION - A mutex can be used to serialize applications and prevent multiple instances from being opened at the same time. - Wait, up to a timeout (default is 1 millisecond), for the mutex to become available for an exclusive lock. + A mutex can be used to serialize applications and prevent multiple instances from being opened at the same time. + Wait, up to a timeout (default is 1 millisecond), for the mutex to become available for an exclusive lock. .PARAMETER MutexName - The name of the system mutex. + The name of the system mutex. .PARAMETER MutexWaitTime - The number of milliseconds the current thread should wait to acquire an exclusive lock of a named mutex. Default is: 1 millisecond. - A wait time of -1 milliseconds means to wait indefinitely. A wait time of zero does not acquire an exclusive lock but instead tests the state of the wait handle and returns immediately. + The number of milliseconds the current thread should wait to acquire an exclusive lock of a named mutex. Default is: 1 millisecond. + A wait time of -1 milliseconds means to wait indefinitely. A wait time of zero does not acquire an exclusive lock but instead tests the state of the wait handle and returns immediately. .EXAMPLE - Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds 500 + Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds 500 .EXAMPLE - Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Minutes 5).TotalMilliseconds + Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Minutes 5).TotalMilliseconds .EXAMPLE - Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Seconds 60).TotalMilliseconds + Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Seconds 60).TotalMilliseconds .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://msdn.microsoft.com/en-us/library/aa372909(VS.85).asp - http://psappdeploytoolkit.com + http://msdn.microsoft.com/en-us/library/aa372909(VS.85).asp + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateLength(1,260)] - [string]$MutexName, - [Parameter(Mandatory=$false)] - [ValidateScript({($_ -ge -1) -and ($_ -le [int32]::MaxValue)})] - [int32]$MutexWaitTimeInMilliseconds = 1 - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Initialize Variables - [timespan]$MutexWaitTime = [timespan]::FromMilliseconds($MutexWaitTimeInMilliseconds) - If ($MutexWaitTime.TotalMinutes -ge 1) { - [string]$WaitLogMsg = "$($MutexWaitTime.TotalMinutes) minute(s)" - } - ElseIf ($MutexWaitTime.TotalSeconds -ge 1) { - [string]$WaitLogMsg = "$($MutexWaitTime.TotalSeconds) second(s)" - } - Else { - [string]$WaitLogMsg = "$($MutexWaitTime.Milliseconds) millisecond(s)" - } - [boolean]$IsUnhandledException = $false - [boolean]$IsMutexFree = $false - [Threading.Mutex]$OpenExistingMutex = $null - } - Process { - Write-Log -Message "Check to see if mutex [$MutexName] is available. Wait up to [$WaitLogMsg] for the mutex to become available." -Source ${CmdletName} - Try { - ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function. - $private:previousErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Stop' - - ## Open the specified named mutex, if it already exists, without acquiring an exclusive lock on it. If the system mutex does not exist, this method throws an exception instead of creating the system object. - [Threading.Mutex]$OpenExistingMutex = [Threading.Mutex]::OpenExisting($MutexName) - ## Attempt to acquire an exclusive lock on the mutex. Use a Timespan to specify a timeout value after which no further attempt is made to acquire a lock on the mutex. - $IsMutexFree = $OpenExistingMutex.WaitOne($MutexWaitTime, $false) - } - Catch [Threading.WaitHandleCannotBeOpenedException] { - ## The named mutex does not exist - $IsMutexFree = $true - } - Catch [ObjectDisposedException] { - ## Mutex was disposed between opening it and attempting to wait on it - $IsMutexFree = $true - } - Catch [UnauthorizedAccessException] { - ## The named mutex exists, but the user does not have the security access required to use it - $IsMutexFree = $false - } - Catch [Threading.AbandonedMutexException] { - ## The wait completed because a thread exited without releasing a mutex. This exception is thrown when one thread acquires a mutex object that another thread has abandoned by exiting without releasing it. - $IsMutexFree = $true - } - Catch { - $IsUnhandledException = $true - ## Return $true, to signify that mutex is available, because function was unable to successfully complete a check due to an unhandled exception. Default is to err on the side of the mutex being available on a hard failure. - Write-Log -Message "Unable to check if mutex [$MutexName] is available due to an unhandled exception. Will default to return value of [$true]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - $IsMutexFree = $true - } - Finally { - If ($IsMutexFree) { - If (-not $IsUnhandledException) { - Write-Log -Message "Mutex [$MutexName] is available for an exclusive lock." -Source ${CmdletName} - } - } - Else { - If ($MutexName -eq 'Global\_MSIExecute') { - ## Get the command line for the MSI installation in progress - Try { - [string]$msiInProgressCmdLine = Get-WmiObject -Class 'Win32_Process' -Filter "name = 'msiexec.exe'" -ErrorAction 'Stop' | Where-Object { $_.CommandLine } | Select-Object -ExpandProperty 'CommandLine' | Where-Object { $_ -match '\.msi' } | ForEach-Object { $_.Trim() } - } - Catch { } - Write-Log -Message "Mutex [$MutexName] is not available for an exclusive lock because the following MSI installation is in progress [$msiInProgressCmdLine]." -Severity 2 -Source ${CmdletName} - } - Else { - Write-Log -Message "Mutex [$MutexName] is not available because another thread already has an exclusive lock on it." -Source ${CmdletName} - } - } - - If (($null -ne $OpenExistingMutex) -and ($IsMutexFree)) { - ## Release exclusive lock on the mutex - $null = $OpenExistingMutex.ReleaseMutex() - $OpenExistingMutex.Close() - } - If ($private:previousErrorActionPreference) { $ErrorActionPreference = $private:previousErrorActionPreference } - } - } - End { - Write-Output -InputObject $IsMutexFree - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateLength(1,260)] + [string]$MutexName, + [Parameter(Mandatory=$false)] + [ValidateScript({($_ -ge -1) -and ($_ -le [int32]::MaxValue)})] + [int32]$MutexWaitTimeInMilliseconds = 1 + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Initialize Variables + [timespan]$MutexWaitTime = [timespan]::FromMilliseconds($MutexWaitTimeInMilliseconds) + If ($MutexWaitTime.TotalMinutes -ge 1) { + [string]$WaitLogMsg = "$($MutexWaitTime.TotalMinutes) minute(s)" + } + ElseIf ($MutexWaitTime.TotalSeconds -ge 1) { + [string]$WaitLogMsg = "$($MutexWaitTime.TotalSeconds) second(s)" + } + Else { + [string]$WaitLogMsg = "$($MutexWaitTime.Milliseconds) millisecond(s)" + } + [boolean]$IsUnhandledException = $false + [boolean]$IsMutexFree = $false + [Threading.Mutex]$OpenExistingMutex = $null + } + Process { + Write-Log -Message "Check to see if mutex [$MutexName] is available. Wait up to [$WaitLogMsg] for the mutex to become available." -Source ${CmdletName} + Try { + ## Using this variable allows capture of exceptions from .NET methods. Private scope only changes value for current function. + $private:previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + + ## Open the specified named mutex, if it already exists, without acquiring an exclusive lock on it. If the system mutex does not exist, this method throws an exception instead of creating the system object. + [Threading.Mutex]$OpenExistingMutex = [Threading.Mutex]::OpenExisting($MutexName) + ## Attempt to acquire an exclusive lock on the mutex. Use a Timespan to specify a timeout value after which no further attempt is made to acquire a lock on the mutex. + $IsMutexFree = $OpenExistingMutex.WaitOne($MutexWaitTime, $false) + } + Catch [Threading.WaitHandleCannotBeOpenedException] { + ## The named mutex does not exist + $IsMutexFree = $true + } + Catch [ObjectDisposedException] { + ## Mutex was disposed between opening it and attempting to wait on it + $IsMutexFree = $true + } + Catch [UnauthorizedAccessException] { + ## The named mutex exists, but the user does not have the security access required to use it + $IsMutexFree = $false + } + Catch [Threading.AbandonedMutexException] { + ## The wait completed because a thread exited without releasing a mutex. This exception is thrown when one thread acquires a mutex object that another thread has abandoned by exiting without releasing it. + $IsMutexFree = $true + } + Catch { + $IsUnhandledException = $true + ## Return $true, to signify that mutex is available, because function was unable to successfully complete a check due to an unhandled exception. Default is to err on the side of the mutex being available on a hard failure. + Write-Log -Message "Unable to check if mutex [$MutexName] is available due to an unhandled exception. Will default to return value of [$true]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + $IsMutexFree = $true + } + Finally { + If ($IsMutexFree) { + If (-not $IsUnhandledException) { + Write-Log -Message "Mutex [$MutexName] is available for an exclusive lock." -Source ${CmdletName} + } + } + Else { + If ($MutexName -eq 'Global\_MSIExecute') { + ## Get the command line for the MSI installation in progress + Try { + [string]$msiInProgressCmdLine = Get-WmiObject -Class 'Win32_Process' -Filter "name = 'msiexec.exe'" -ErrorAction 'Stop' | Where-Object { $_.CommandLine } | Select-Object -ExpandProperty 'CommandLine' | Where-Object { $_ -match '\.msi' } | ForEach-Object { $_.Trim() } + } + Catch { } + Write-Log -Message "Mutex [$MutexName] is not available for an exclusive lock because the following MSI installation is in progress [$msiInProgressCmdLine]." -Severity 2 -Source ${CmdletName} + } + Else { + Write-Log -Message "Mutex [$MutexName] is not available because another thread already has an exclusive lock on it." -Source ${CmdletName} + } + } + + If (($null -ne $OpenExistingMutex) -and ($IsMutexFree)) { + ## Release exclusive lock on the mutex + $null = $OpenExistingMutex.ReleaseMutex() + $OpenExistingMutex.Close() + } + If ($private:previousErrorActionPreference) { $ErrorActionPreference = $private:previousErrorActionPreference } + } + } + End { + Write-Output -InputObject $IsMutexFree + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3345,54 +3475,54 @@ Function Test-IsMutexAvailable { Function New-Folder { <# .SYNOPSIS - Create a new folder. + Create a new folder. .DESCRIPTION - Create a new folder if it does not exist. + Create a new folder if it does not exist. .PARAMETER Path - Path to the new folder to create. + Path to the new folder to create. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - New-Folder -Path "$envWinDir\System32" + New-Folder -Path "$envWinDir\System32" .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Path, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - If (-not (Test-Path -LiteralPath $Path -PathType 'Container')) { - Write-Log -Message "Create folder [$Path]." -Source ${CmdletName} - $null = New-Item -Path $Path -ItemType 'Directory' -ErrorAction 'Stop' - } - Else { - Write-Log -Message "Folder [$Path] already exists." -Source ${CmdletName} - } - } - Catch { - Write-Log -Message "Failed to create folder [$Path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to create folder [$Path]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + If (-not (Test-Path -LiteralPath $Path -PathType 'Container')) { + Write-Log -Message "Create folder [$Path]." -Source ${CmdletName} + $null = New-Item -Path $Path -ItemType 'Directory' -ErrorAction 'Stop' + } + Else { + Write-Log -Message "Folder [$Path] already exists." -Source ${CmdletName} + } + } + Catch { + Write-Log -Message "Failed to create folder [$Path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to create folder [$Path]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3401,67 +3531,67 @@ Function New-Folder { Function Remove-Folder { <# .SYNOPSIS - Remove folder and files if they exist. + Remove folder and files if they exist. .DESCRIPTION - Remove folder and all files with or without recursion in a given path. + Remove folder and all files with or without recursion in a given path. .PARAMETER Path - Path to the folder to remove. + Path to the folder to remove. .PARAMETER DisableRecursion - Disables recursion while deleting. + Disables recursion while deleting. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Remove-Folder -Path "$envWinDir\Downloaded Program Files" + Remove-Folder -Path "$envWinDir\Downloaded Program Files" .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Path, - [Parameter(Mandatory=$false)] - [switch]$DisableRecursion, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - 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 (Test-Path -LiteralPath $Path -PathType 'Container') { - Try { - If ($DisableRecursion) { - Write-Log -Message "Delete folder [$path] without recursion..." -Source ${CmdletName} - Remove-Item -LiteralPath $Path -Force -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorRemoveFolder' - } else { - Write-Log -Message "Delete folder [$path] recursively..." -Source ${CmdletName} - Remove-Item -LiteralPath $Path -Force -Recurse -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorRemoveFolder' - } - - If ($ErrorRemoveFolder) { - Write-Log -Message "The following error(s) took place while deleting folder(s) and file(s) recursively from path [$path]. `n$(Resolve-Error -ErrorRecord $ErrorRemoveFolder)" -Severity 2 -Source ${CmdletName} - } - } - Catch { - Write-Log -Message "Failed to delete folder(s) and file(s) recursively from path [$path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to delete folder(s) and file(s) recursively from path [$path]: $($_.Exception.Message)" - } - } - } - Else { - Write-Log -Message "Folder [$Path] does not exists..." -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [switch]$DisableRecursion, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + 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 (Test-Path -LiteralPath $Path -PathType 'Container') { + Try { + If ($DisableRecursion) { + Write-Log -Message "Delete folder [$path] without recursion..." -Source ${CmdletName} + Remove-Item -LiteralPath $Path -Force -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorRemoveFolder' + } else { + Write-Log -Message "Delete folder [$path] recursively..." -Source ${CmdletName} + Remove-Item -LiteralPath $Path -Force -Recurse -ErrorAction 'SilentlyContinue' -ErrorVariable '+ErrorRemoveFolder' + } + + If ($ErrorRemoveFolder) { + Write-Log -Message "The following error(s) took place while deleting folder(s) and file(s) recursively from path [$path]. `n$(Resolve-Error -ErrorRecord $ErrorRemoveFolder)" -Severity 2 -Source ${CmdletName} + } + } + Catch { + Write-Log -Message "Failed to delete folder(s) and file(s) recursively from path [$path]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to delete folder(s) and file(s) recursively from path [$path]: $($_.Exception.Message)" + } + } + } + Else { + Write-Log -Message "Folder [$Path] does not exists..." -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3470,125 +3600,125 @@ Function Remove-Folder { Function Copy-File { <# .SYNOPSIS - Copy a file or group of files to a destination path. + Copy a file or group of files to a destination path. .DESCRIPTION - Copy a file or group of files to a destination path. + Copy a file or group of files to a destination path. .PARAMETER Path - Path of the file to copy. + Path of the file to copy. .PARAMETER Destination - Destination Path of the file to copy. + Destination Path of the file to copy. .PARAMETER Recurse - Copy files in subdirectories. + Copy files in subdirectories. .PARAMETER Flatten - Flattens the files into the root destination directory. + Flattens the files into the root destination directory. .PARAMETER ContinueOnError - Continue if an error is encountered. This will continue the deployment script, but will not continue copying files if an error is encountered. Default is: $true. + Continue if an error is encountered. This will continue the deployment script, but will not continue copying files if an error is encountered. Default is: $true. .PARAMETER ContinueFileCopyOnError - Continue copying files if an error is encountered. This will continue the deployment script and will warn about files that failed to be copied. Default is: $false. + Continue copying files if an error is encountered. This will continue the deployment script and will warn about files that failed to be copied. Default is: $false. .EXAMPLE - Copy-File -Path "$dirSupportFiles\MyApp.ini" -Destination "$envWindir\MyApp.ini" + Copy-File -Path "$dirSupportFiles\MyApp.ini" -Destination "$envWinDir\MyApp.ini" .EXAMPLE - Copy-File -Path "$dirSupportFiles\*.*" -Destination "$envTemp\tempfiles" - Copy all of the files in a folder to a destination folder. + Copy-File -Path "$dirSupportFiles\*.*" -Destination "$envTemp\tempfiles" + Copy all of the files in a folder to a destination folder. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string[]]$Path, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Destination, - [Parameter(Mandatory=$false)] - [switch]$Recurse = $false, - [Parameter(Mandatory=$false)] - [switch]$Flatten, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true, - [ValidateNotNullOrEmpty()] - [boolean]$ContinueFileCopyOnError = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - $null = $fileCopyError - If ((-not ([IO.Path]::HasExtension($Destination))) -and (-not (Test-Path -LiteralPath $Destination -PathType 'Container'))) { - Write-Log -Message "Destination folder does not exist, creating destination folder [$destination]." -Source ${CmdletName} - $null = New-Item -Path $Destination -Type 'Directory' -Force -ErrorAction 'Stop' - } - - if ($Flatten) { - If ($Recurse) { - Write-Log -Message "Copy file(s) recursively in path [$path] to destination [$destination] root folder, flattened." -Source ${CmdletName} - If (-not $ContinueFileCopyOnError) { - $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { - Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'Stop' - } - } - Else { - $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { - Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError - } - } - } - Else { - Write-Log -Message "Copy file in path [$path] to destination [$destination]." -Source ${CmdletName} - If (-not $ContinueFileCopyOnError) { - $null = Copy-Item -Path $path -Destination $destination -Force -ErrorAction 'Stop' - } - Else { - $null = Copy-Item -Path $path -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError - } - } - } - Else { - $null = $FileCopyError - If ($Recurse) { - Write-Log -Message "Copy file(s) recursively in path [$path] to destination [$destination]." -Source ${CmdletName} - If (-not $ContinueFileCopyOnError) { - $null = Copy-Item -Path $Path -Destination $Destination -Force -Recurse -ErrorAction 'Stop' - } - Else { - $null = Copy-Item -Path $Path -Destination $Destination -Force -Recurse -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError - } - } - Else { - Write-Log -Message "Copy file in path [$path] to destination [$destination]." -Source ${CmdletName} - If (-not $ContinueFileCopyOnError) { - $null = Copy-Item -Path $Path -Destination $Destination -Force -ErrorAction 'Stop' - } - Else { - $null = Copy-Item -Path $Path -Destination $Destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError - } - } - } - - If ($fileCopyError) { - Write-Log -Message "The following warnings were detected while copying file(s) in path [$path] to destination [$destination]. `n$FileCopyError" -Severity 2 -Source ${CmdletName} - } - Else { - Write-Log -Message "File copy completed successfully." -Source ${CmdletName} - } - } - Catch { - Write-Log -Message "Failed to copy file(s) in path [$path] to destination [$destination]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to copy file(s) in path [$path] to destination [$destination]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string[]]$Path, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Destination, + [Parameter(Mandatory=$false)] + [switch]$Recurse = $false, + [Parameter(Mandatory=$false)] + [switch]$Flatten, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true, + [ValidateNotNullOrEmpty()] + [boolean]$ContinueFileCopyOnError = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + $null = $fileCopyError + If ((-not ([IO.Path]::HasExtension($Destination))) -and (-not (Test-Path -LiteralPath $Destination -PathType 'Container'))) { + Write-Log -Message "Destination folder does not exist, creating destination folder [$destination]." -Source ${CmdletName} + $null = New-Item -Path $Destination -Type 'Directory' -Force -ErrorAction 'Stop' + } + + if ($Flatten) { + If ($Recurse) { + Write-Log -Message "Copy file(s) recursively in path [$path] to destination [$destination] root folder, flattened." -Source ${CmdletName} + If (-not $ContinueFileCopyOnError) { + $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { + Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'Stop' + } + } + Else { + $null = Get-ChildItem -Path $path -Recurse | Where-Object {!($_.PSIsContainer)} | ForEach-Object { + Copy-Item -Path ($_.FullName) -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + } + } + } + Else { + Write-Log -Message "Copy file in path [$path] to destination [$destination]." -Source ${CmdletName} + If (-not $ContinueFileCopyOnError) { + $null = Copy-Item -Path $path -Destination $destination -Force -ErrorAction 'Stop' + } + Else { + $null = Copy-Item -Path $path -Destination $destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + } + } + } + Else { + $null = $FileCopyError + If ($Recurse) { + Write-Log -Message "Copy file(s) recursively in path [$path] to destination [$destination]." -Source ${CmdletName} + If (-not $ContinueFileCopyOnError) { + $null = Copy-Item -Path $Path -Destination $Destination -Force -Recurse -ErrorAction 'Stop' + } + Else { + $null = Copy-Item -Path $Path -Destination $Destination -Force -Recurse -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + } + } + Else { + Write-Log -Message "Copy file in path [$path] to destination [$destination]." -Source ${CmdletName} + If (-not $ContinueFileCopyOnError) { + $null = Copy-Item -Path $Path -Destination $Destination -Force -ErrorAction 'Stop' + } + Else { + $null = Copy-Item -Path $Path -Destination $Destination -Force -ErrorAction 'SilentlyContinue' -ErrorVariable FileCopyError + } + } + } + + If ($fileCopyError) { + Write-Log -Message "The following warnings were detected while copying file(s) in path [$path] to destination [$destination]. `n$FileCopyError" -Severity 2 -Source ${CmdletName} + } + Else { + Write-Log -Message "File copy completed successfully." -Source ${CmdletName} + } + } + Catch { + Write-Log -Message "Failed to copy file(s) in path [$path] to destination [$destination]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to copy file(s) in path [$path] to destination [$destination]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3597,112 +3727,112 @@ Function Copy-File { Function Remove-File { <# .SYNOPSIS - Removes one or more items from a given path on the filesystem. + Removes one or more items from a given path on the filesystem. .DESCRIPTION - Removes one or more items from a given path on the filesystem. + Removes one or more items from a given path on the filesystem. .PARAMETER Path - Specifies the path on the filesystem to be resolved. The value of Path will accept wildcards. Will accept an array of values. + Specifies the path on the filesystem to be resolved. The value of Path will accept wildcards. Will accept an array of values. .PARAMETER LiteralPath - Specifies the path on the filesystem to be resolved. The value of LiteralPath is used exactly as it is typed; no characters are interpreted as wildcards. Will accept an array of values. + Specifies the path on the filesystem to be resolved. The value of LiteralPath is used exactly as it is typed; no characters are interpreted as wildcards. Will accept an array of values. .PARAMETER Recurse - Deletes the files in the specified location(s) and in all child items of the location(s). + Deletes the files in the specified location(s) and in all child items of the location(s). .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Remove-File -Path 'C:\Windows\Downloaded Program Files\Temp.inf' + Remove-File -Path 'C:\Windows\Downloaded Program Files\Temp.inf' .EXAMPLE - Remove-File -LiteralPath 'C:\Windows\Downloaded Program Files' -Recurse + Remove-File -LiteralPath 'C:\Windows\Downloaded Program Files' -Recurse .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,ParameterSetName='Path')] - [ValidateNotNullorEmpty()] - [string[]]$Path, - [Parameter(Mandatory=$true,ParameterSetName='LiteralPath')] - [ValidateNotNullorEmpty()] - [string[]]$LiteralPath, - [Parameter(Mandatory=$false)] - [switch]$Recurse = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Build hashtable of parameters/value pairs to be passed to Remove-Item cmdlet - [hashtable]$RemoveFileSplat = @{ 'Recurse' = $Recurse - 'Force' = $true - 'ErrorVariable' = '+ErrorRemoveItem' - } - If ($ContinueOnError) { - $RemoveFileSplat.Add('ErrorAction', 'SilentlyContinue') - } - Else { - $RemoveFileSplat.Add('ErrorAction', 'Stop') - } - - ## Resolve the specified path, if the path does not exist, display a warning instead of an error - If ($PSCmdlet.ParameterSetName -eq 'Path') { [string[]]$SpecifiedPath = $Path } Else { [string[]]$SpecifiedPath = $LiteralPath } - ForEach ($Item in $SpecifiedPath) { - Try { - If ($PSCmdlet.ParameterSetName -eq 'Path') { - [string[]]$ResolvedPath += Resolve-Path -Path $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' - } - Else { - [string[]]$ResolvedPath += Resolve-Path -LiteralPath $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' - } - } - Catch [System.Management.Automation.ItemNotFoundException] { - Write-Log -Message "Unable to resolve file(s) for deletion in path [$Item] because path does not exist." -Severity 2 -Source ${CmdletName} - } - Catch { - Write-Log -Message "Failed to resolve file(s) for deletion in path [$Item]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to resolve file(s) for deletion in path [$Item]: $($_.Exception.Message)" - } - } - } - - ## Delete specified path if it was successfully resolved - If ($ResolvedPath) { - ForEach ($Item in $ResolvedPath) { - Try { - If (($Recurse) -and (Test-Path -LiteralPath $Item -PathType 'Container')) { - Write-Log -Message "Delete file(s) recursively in path [$Item]..." -Source ${CmdletName} - } - ElseIf ((-not $Recurse) -and (Test-Path -LiteralPath $Item -PathType 'Container')) { - Write-Log -Message "Skipping folder [$Item] because the Recurse switch was not specified" - Continue - } - Else { - Write-Log -Message "Delete file in path [$Item]..." -Source ${CmdletName} - } - $null = Remove-Item @RemoveFileSplat -LiteralPath $Item - } - Catch { - Write-Log -Message "Failed to delete file(s) in path [$Item]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to delete file(s) in path [$Item]: $($_.Exception.Message)" - } - } - } - } - - If ($ErrorRemoveItem) { - Write-Log -Message "The following error(s) took place while removing file(s) in path [$SpecifiedPath]. `n$(Resolve-Error -ErrorRecord $ErrorRemoveItem)" -Severity 2 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,ParameterSetName='Path')] + [ValidateNotNullorEmpty()] + [string[]]$Path, + [Parameter(Mandatory=$true,ParameterSetName='LiteralPath')] + [ValidateNotNullorEmpty()] + [string[]]$LiteralPath, + [Parameter(Mandatory=$false)] + [switch]$Recurse = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Build hashtable of parameters/value pairs to be passed to Remove-Item cmdlet + [hashtable]$RemoveFileSplat = @{ 'Recurse' = $Recurse + 'Force' = $true + 'ErrorVariable' = '+ErrorRemoveItem' + } + If ($ContinueOnError) { + $RemoveFileSplat.Add('ErrorAction', 'SilentlyContinue') + } + Else { + $RemoveFileSplat.Add('ErrorAction', 'Stop') + } + + ## Resolve the specified path, if the path does not exist, display a warning instead of an error + If ($PSCmdlet.ParameterSetName -eq 'Path') { [string[]]$SpecifiedPath = $Path } Else { [string[]]$SpecifiedPath = $LiteralPath } + ForEach ($Item in $SpecifiedPath) { + Try { + If ($PSCmdlet.ParameterSetName -eq 'Path') { + [string[]]$ResolvedPath += Resolve-Path -Path $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' + } + Else { + [string[]]$ResolvedPath += Resolve-Path -LiteralPath $Item -ErrorAction 'Stop' | Where-Object { $_.Path } | Select-Object -ExpandProperty 'Path' -ErrorAction 'Stop' + } + } + Catch [System.Management.Automation.ItemNotFoundException] { + Write-Log -Message "Unable to resolve file(s) for deletion in path [$Item] because path does not exist." -Severity 2 -Source ${CmdletName} + } + Catch { + Write-Log -Message "Failed to resolve file(s) for deletion in path [$Item]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to resolve file(s) for deletion in path [$Item]: $($_.Exception.Message)" + } + } + } + + ## Delete specified path if it was successfully resolved + If ($ResolvedPath) { + ForEach ($Item in $ResolvedPath) { + Try { + If (($Recurse) -and (Test-Path -LiteralPath $Item -PathType 'Container')) { + Write-Log -Message "Delete file(s) recursively in path [$Item]..." -Source ${CmdletName} + } + ElseIf ((-not $Recurse) -and (Test-Path -LiteralPath $Item -PathType 'Container')) { + Write-Log -Message "Skipping folder [$Item] because the Recurse switch was not specified" -Source ${CmdletName} + Continue + } + Else { + Write-Log -Message "Delete file in path [$Item]..." -Source ${CmdletName} + } + $null = Remove-Item @RemoveFileSplat -LiteralPath $Item + } + Catch { + Write-Log -Message "Failed to delete file(s) in path [$Item]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to delete file(s) in path [$Item]: $($_.Exception.Message)" + } + } + } + } + + If ($ErrorRemoveItem) { + Write-Log -Message "The following error(s) took place while removing file(s) in path [$SpecifiedPath]. `n$(Resolve-Error -ErrorRecord $ErrorRemoveItem)" -Severity 2 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3711,89 +3841,90 @@ Function Remove-File { Function Convert-RegistryPath { <# .SYNOPSIS - Converts the specified registry key path to a format that is compatible with built-in PowerShell cmdlets. + Converts the specified registry key path to a format that is compatible with built-in PowerShell cmdlets. .DESCRIPTION - Converts the specified registry key path to a format that is compatible with built-in PowerShell cmdlets. - Converts registry key hives to their full paths. Example: HKLM is converted to "Registry::HKEY_LOCAL_MACHINE". + Converts the specified registry key path to a format that is compatible with built-in PowerShell cmdlets. + Converts registry key hives to their full paths. Example: HKLM is converted to "Registry::HKEY_LOCAL_MACHINE". .PARAMETER Key - Path to the registry key to convert (can be a registry hive or fully qualified path) + Path to the registry key to convert (can be a registry hive or fully qualified path) .PARAMETER SID - The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. - Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. + The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. + Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. +.PARAMETER DisableFunctionLogging + Disables logging of this function. Default: $true .EXAMPLE - Convert-RegistryPath -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' + Convert-RegistryPath -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .EXAMPLE - Convert-RegistryPath -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' + Convert-RegistryPath -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$SID - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Convert the registry key hive to the full path, only match if at the beginning of the line - If ($Key -match '^HKLM:\\|^HKCU:\\|^HKCR:\\|^HKU:\\|^HKCC:\\|^HKPD:\\') { - # Converts registry paths that start with, e.g.: HKLM:\ - $key = $key -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR:\\', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU:\\', 'HKEY_USERS\' - $key = $key -replace '^HKCC:\\', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD:\\', 'HKEY_PERFORMANCE_DATA\' - } - ElseIf ($Key -match '^HKLM:|^HKCU:|^HKCR:|^HKU:|^HKCC:|^HKPD:') { - # Converts registry paths that start with, e.g.: HKLM: - $key = $key -replace '^HKLM:', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR:', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU:', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU:', 'HKEY_USERS\' - $key = $key -replace '^HKCC:', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD:', 'HKEY_PERFORMANCE_DATA\' - } - ElseIf ($Key -match '^HKLM\\|^HKCU\\|^HKCR\\|^HKU\\|^HKCC\\|^HKPD\\') { - # Converts registry paths that start with, e.g.: HKLM\ - $key = $key -replace '^HKLM\\', 'HKEY_LOCAL_MACHINE\' - $key = $key -replace '^HKCR\\', 'HKEY_CLASSES_ROOT\' - $key = $key -replace '^HKCU\\', 'HKEY_CURRENT_USER\' - $key = $key -replace '^HKU\\', 'HKEY_USERS\' - $key = $key -replace '^HKCC\\', 'HKEY_CURRENT_CONFIG\' - $key = $key -replace '^HKPD\\', 'HKEY_PERFORMANCE_DATA\' - } - - If ($PSBoundParameters.ContainsKey('SID')) { - ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - If ($key -match '^HKEY_CURRENT_USER\\') { $key = $key -replace '^HKEY_CURRENT_USER\\', "HKEY_USERS\$SID\" } - } - - ## Append the PowerShell drive to the registry key path - If ($key -notmatch '^Registry::') {[string]$key = "Registry::$key" } - - If($Key -match '^Registry::HKEY_LOCAL_MACHINE|^Registry::HKEY_CLASSES_ROOT|^Registry::HKEY_CURRENT_USER|^Registry::HKEY_USERS|^Registry::HKEY_CURRENT_CONFIG|^Registry::HKEY_PERFORMANCE_DATA') { - ## Check for expected key string format - Write-Log -Message "Return fully qualified registry key path [$key]." -Source ${CmdletName} - Write-Output -InputObject $key - } - Else{ - # If key string is not properly formatted, throw an error - Throw "Unable to detect target registry hive in string [$key]." - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$SID, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [bool]$DisableFunctionLogging = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Convert the registry key hive to the full path, only match if at the beginning of the line + If ($Key -match '^HKLM') { + $Key = $Key -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' -replace '^HKLM:', 'HKEY_LOCAL_MACHINE\' -replace '^HKLM\\', 'HKEY_LOCAL_MACHINE\' + } + elseif ($Key -match '^HKCR') { + $Key = $Key -replace '^HKCR:\\', 'HKEY_CLASSES_ROOT\' -replace '^HKCR:', 'HKEY_CLASSES_ROOT\' -replace '^HKCR\\', 'HKEY_CLASSES_ROOT\' + } + elseif ($Key -match '^HKCU') { + $Key = $Key -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' -replace '^HKCU:', 'HKEY_CURRENT_USER\' -replace '^HKCU\\', 'HKEY_CURRENT_USER\' + } + elseif ($Key -match '^HKU') { + $Key = $Key -replace '^HKU:\\', 'HKEY_USERS\' -replace '^HKU:', 'HKEY_USERS\' -replace '^HKU\\', 'HKEY_USERS\' + } + elseif ($Key -match '^HKCC') { + $Key = $Key -replace '^HKCC:\\', 'HKEY_CURRENT_CONFIG\' -replace '^HKCC:', 'HKEY_CURRENT_CONFIG\' -replace '^HKCC\\', 'HKEY_CURRENT_CONFIG\' + } + elseif ($Key -match '^HKPD') { + $Key = $Key -replace '^HKPD:\\', 'HKEY_PERFORMANCE_DATA\' -replace '^HKPD:', 'HKEY_PERFORMANCE_DATA\' -replace '^HKPD\\', 'HKEY_PERFORMANCE_DATA\' + } + + ## Append the PowerShell provider to the registry key path + If ($key -notmatch '^Registry::') {[string]$key = "Registry::$key" } + + If ($PSBoundParameters.ContainsKey('SID')) { + ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID + If ($key -match '^Registry::HKEY_CURRENT_USER\\') { $key = $key -replace '^Registry::HKEY_CURRENT_USER\\', "Registry::HKEY_USERS\$SID\" } + Elseif (-not ($DisableFunctionLogging)) { + Write-Log -Message "SID parameter specified but the registry hive of the key is not HKEY_CURRENT_USER." -Source ${CmdletName} -Severity 2 + } + } + + If($Key -match '^Registry::HKEY_LOCAL_MACHINE|^Registry::HKEY_CLASSES_ROOT|^Registry::HKEY_CURRENT_USER|^Registry::HKEY_USERS|^Registry::HKEY_CURRENT_CONFIG|^Registry::HKEY_PERFORMANCE_DATA') { + ## Check for expected key string format + If (-not ($DisableFunctionLogging)) { + Write-Log -Message "Return fully qualified registry key path [$key]." -Source ${CmdletName} + } + Write-Output -InputObject $key + } + Else{ + # If key string is not properly formatted, throw an error + Throw "Unable to detect target registry hive in string [$key]." + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3802,72 +3933,72 @@ Function Convert-RegistryPath { Function Test-RegistryValue { <# .SYNOPSIS - Test if a registry value exists. + Test if a registry value exists. .DESCRIPTION - Checks a registry key path to see if it has a value with a given name. Can correctly handle cases where a value simply has an empty or null value. + Checks a registry key path to see if it has a value with a given name. Can correctly handle cases where a value simply has an empty or null value. .PARAMETER Key - Path of the registry key. + Path of the registry key. .PARAMETER Value - Specify the registry key value to check the existence of. + Specify the registry key value to check the existence of. .PARAMETER SID - The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. - Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. + The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. + Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. .EXAMPLE - Test-RegistryValue -Key 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations' + Test-RegistryValue -Key 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations' .NOTES - To test if registry key exists, use Test-Path function like so: - Test-Path -Path $Key -PathType 'Container' + To test if registry key exists, use Test-Path function like so: + Test-Path -Path $Key -PathType 'Container' .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - Param ( - [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()]$Key, - [Parameter(Mandatory=$true,Position=1)] - [ValidateNotNullOrEmpty()]$Value, - [Parameter(Mandatory=$false,Position=2)] - [ValidateNotNullorEmpty()] - [string]$SID - ) - - 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 SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - Try { - If ($PSBoundParameters.ContainsKey('SID')) { - [string]$Key = Convert-RegistryPath -Key $Key -SID $SID - } - Else { - [string]$Key = Convert-RegistryPath -Key $Key - } - } - Catch { - Throw - } - [boolean]$IsRegistryValueExists = $false - Try { - If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { - [string[]]$PathProperties = Get-Item -LiteralPath $Key -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Property' -ErrorAction 'Stop' - If ($PathProperties -contains $Value) { $IsRegistryValueExists = $true } - } - } - Catch { } - - If ($IsRegistryValueExists) { - Write-Log -Message "Registry key value [$Key] [$Value] does exist." -Source ${CmdletName} - } - Else { - Write-Log -Message "Registry key value [$Key] [$Value] does not exist." -Source ${CmdletName} - } - Write-Output -InputObject $IsRegistryValueExists - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + Param ( + [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] + [ValidateNotNullOrEmpty()]$Key, + [Parameter(Mandatory=$true,Position=1)] + [ValidateNotNullOrEmpty()]$Value, + [Parameter(Mandatory=$false,Position=2)] + [ValidateNotNullorEmpty()] + [string]$SID + ) + + 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 SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID + Try { + If ($PSBoundParameters.ContainsKey('SID')) { + [string]$Key = Convert-RegistryPath -Key $Key -SID $SID + } + Else { + [string]$Key = Convert-RegistryPath -Key $Key + } + } + Catch { + Throw + } + [boolean]$IsRegistryValueExists = $false + Try { + If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { + [string[]]$PathProperties = Get-Item -LiteralPath $Key -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Property' -ErrorAction 'Stop' + If ($PathProperties -contains $Value) { $IsRegistryValueExists = $true } + } + } + Catch { } + + If ($IsRegistryValueExists) { + Write-Log -Message "Registry key value [$Key] [$Value] does exist." -Source ${CmdletName} + } + Else { + Write-Log -Message "Registry key value [$Key] [$Value] does not exist." -Source ${CmdletName} + } + Write-Output -InputObject $IsRegistryValueExists + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -3876,160 +4007,160 @@ Function Test-RegistryValue { Function Get-RegistryKey { <# .SYNOPSIS - Retrieves value names and value data for a specified registry key or optionally, a specific value. + Retrieves value names and value data for a specified registry key or optionally, a specific value. .DESCRIPTION - Retrieves value names and value data for a specified registry key or optionally, a specific value. - If the registry key does not exist or contain any values, the function will return $null by default. To test for existence of a registry key path, use built-in Test-Path cmdlet. + Retrieves value names and value data for a specified registry key or optionally, a specific value. + If the registry key does not exist or contain any values, the function will return $null by default. To test for existence of a registry key path, use built-in Test-Path cmdlet. .PARAMETER Key - Path of the registry key. + Path of the registry key. .PARAMETER Value - Value to retrieve (optional). + Value to retrieve (optional). .PARAMETER SID - The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. - Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. + The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. + Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. .PARAMETER ReturnEmptyKeyIfExists - Return the registry key if it exists but it has no property/value pairs underneath it. Default is: $false. + Return the registry key if it exists but it has no property/value pairs underneath it. Default is: $false. .PARAMETER DoNotExpandEnvironmentNames - Return unexpanded REG_EXPAND_SZ values. Default is: $false. + Return unexpanded REG_EXPAND_SZ values. Default is: $false. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' + Get-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{1AD147D0-BE0E-3D6C-AC11-64F6DC4163F1}' .EXAMPLE - Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\iexplore.exe' + Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\iexplore.exe' .EXAMPLE - Get-RegistryKey -Key 'HKLM:Software\Wow6432Node\Microsoft\Microsoft SQL Server Compact Edition\v3.5' -Value 'Version' + Get-RegistryKey -Key 'HKLM:Software\Wow6432Node\Microsoft\Microsoft SQL Server Compact Edition\v3.5' -Value 'Version' .EXAMPLE - Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Value 'Path' -DoNotExpandEnvironmentNames - Returns %ProgramFiles%\Java instead of C:\Program Files\Java + Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Value 'Path' -DoNotExpandEnvironmentNames + Returns %ProgramFiles%\Java instead of C:\Program Files\Java .EXAMPLE - Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Value '(Default)' + Get-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Value '(Default)' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Value, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$SID, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$ReturnEmptyKeyIfExists = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$DoNotExpandEnvironmentNames = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - If ($PSBoundParameters.ContainsKey('SID')) { - [string]$key = Convert-RegistryPath -Key $key -SID $SID - } - Else { - [string]$key = Convert-RegistryPath -Key $key - } - - ## Check if the registry key exists - If (-not (Test-Path -LiteralPath $key -ErrorAction 'Stop')) { - Write-Log -Message "Registry key [$key] does not exist. Return `$null." -Severity 2 -Source ${CmdletName} - $regKeyValue = $null - } - Else { - If ($PSBoundParameters.ContainsKey('Value')) { - Write-Log -Message "Get registry key [$key] value [$value]." -Source ${CmdletName} - } - Else { - Write-Log -Message "Get registry key [$key] and all property values." -Source ${CmdletName} - } - - ## Get all property values for registry key - $regKeyValue = Get-ItemProperty -LiteralPath $key -ErrorAction 'Stop' - [int32]$regKeyValuePropertyCount = $regKeyValue | Measure-Object | Select-Object -ExpandProperty 'Count' - - ## Select requested property - If ($PSBoundParameters.ContainsKey('Value')) { - # Check if registry value exists - [boolean]$IsRegistryValueExists = $false - If ($regKeyValuePropertyCount -gt 0) { - Try { - [string[]]$PathProperties = Get-Item -LiteralPath $Key -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Property' -ErrorAction 'Stop' - If ($PathProperties -contains $Value) { $IsRegistryValueExists = $true } - } - Catch { } - } - - # Get the Value (do not make a strongly typed variable because it depends entirely on what kind of value is being read) - If ($IsRegistryValueExists) { - If ($DoNotExpandEnvironmentNames) { #Only useful on 'ExpandString' values - If ($Value -like '(Default)') { - $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($null,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) - } - Else { - $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($Value,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) - } - } - ElseIf ($Value -like '(Default)') { - $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($null) - } - Else { - $regKeyValue = $regKeyValue | Select-Object -ExpandProperty $Value -ErrorAction 'SilentlyContinue' - } - } - Else { - Write-Log -Message "Registry key value [$Key] [$Value] does not exist. Return `$null." -Source ${CmdletName} - $regKeyValue = $null - } - } - ## Select all properties or return empty key object - Else { - If ($regKeyValuePropertyCount -eq 0) { - If ($ReturnEmptyKeyIfExists) { - Write-Log -Message "No property values found for registry key. Return empty registry key object [$key]." -Source ${CmdletName} - $regKeyValue = Get-Item -LiteralPath $key -Force -ErrorAction 'Stop' - } - Else { - Write-Log -Message "No property values found for registry key. Return `$null." -Source ${CmdletName} - $regKeyValue = $null - } - } - } - } - Write-Output -InputObject ($regKeyValue) - } - Catch { - If (-not $Value) { - Write-Log -Message "Failed to read registry key [$key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to read registry key [$key]: $($_.Exception.Message)" - } - } - Else { - Write-Log -Message "Failed to read registry key [$key] value [$value]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to read registry key [$key] value [$value]: $($_.Exception.Message)" - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Value, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$SID, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$ReturnEmptyKeyIfExists = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$DoNotExpandEnvironmentNames = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID + If ($PSBoundParameters.ContainsKey('SID')) { + [string]$key = Convert-RegistryPath -Key $key -SID $SID + } + Else { + [string]$key = Convert-RegistryPath -Key $key + } + + ## Check if the registry key exists + If (-not (Test-Path -LiteralPath $key -ErrorAction 'Stop')) { + Write-Log -Message "Registry key [$key] does not exist. Return `$null." -Severity 2 -Source ${CmdletName} + $regKeyValue = $null + } + Else { + If ($PSBoundParameters.ContainsKey('Value')) { + Write-Log -Message "Get registry key [$key] value [$value]." -Source ${CmdletName} + } + Else { + Write-Log -Message "Get registry key [$key] and all property values." -Source ${CmdletName} + } + + ## Get all property values for registry key + $regKeyValue = Get-ItemProperty -LiteralPath $key -ErrorAction 'Stop' + [int32]$regKeyValuePropertyCount = $regKeyValue | Measure-Object | Select-Object -ExpandProperty 'Count' + + ## Select requested property + If ($PSBoundParameters.ContainsKey('Value')) { + # Check if registry value exists + [boolean]$IsRegistryValueExists = $false + If ($regKeyValuePropertyCount -gt 0) { + Try { + [string[]]$PathProperties = Get-Item -LiteralPath $Key -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Property' -ErrorAction 'Stop' + If ($PathProperties -contains $Value) { $IsRegistryValueExists = $true } + } + Catch { } + } + + # Get the Value (do not make a strongly typed variable because it depends entirely on what kind of value is being read) + If ($IsRegistryValueExists) { + If ($DoNotExpandEnvironmentNames) { #Only useful on 'ExpandString' values + If ($Value -like '(Default)') { + $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($null,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + } + Else { + $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($Value,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + } + } + ElseIf ($Value -like '(Default)') { + $regKeyValue = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').GetValue($null) + } + Else { + $regKeyValue = $regKeyValue | Select-Object -ExpandProperty $Value -ErrorAction 'SilentlyContinue' + } + } + Else { + Write-Log -Message "Registry key value [$Key] [$Value] does not exist. Return `$null." -Source ${CmdletName} + $regKeyValue = $null + } + } + ## Select all properties or return empty key object + Else { + If ($regKeyValuePropertyCount -eq 0) { + If ($ReturnEmptyKeyIfExists) { + Write-Log -Message "No property values found for registry key. Return empty registry key object [$key]." -Source ${CmdletName} + $regKeyValue = Get-Item -LiteralPath $key -Force -ErrorAction 'Stop' + } + Else { + Write-Log -Message "No property values found for registry key. Return `$null." -Source ${CmdletName} + $regKeyValue = $null + } + } + } + } + Write-Output -InputObject ($regKeyValue) + } + Catch { + If (-not $Value) { + Write-Log -Message "Failed to read registry key [$key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to read registry key [$key]: $($_.Exception.Message)" + } + } + Else { + Write-Log -Message "Failed to read registry key [$key] value [$value]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to read registry key [$key] value [$value]: $($_.Exception.Message)" + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4038,137 +4169,137 @@ Function Get-RegistryKey { Function Set-RegistryKey { <# .SYNOPSIS - Creates a registry key name, value, and value data; it sets the same if it already exists. + Creates a registry key name, value, and value data; it sets the same if it already exists. .DESCRIPTION - Creates a registry key name, value, and value data; it sets the same if it already exists. + Creates a registry key name, value, and value data; it sets the same if it already exists. .PARAMETER Key - The registry key path. + The registry key path. .PARAMETER Name - The value name. + The value name. .PARAMETER Value - The value data. + The value data. .PARAMETER Type - The type of registry value to create or set. Options: 'Binary','DWord','ExpandString','MultiString','None','QWord','String','Unknown'. Default: String. - Dword should be specified as a decimal. + The type of registry value to create or set. Options: 'Binary','DWord','ExpandString','MultiString','None','QWord','String','Unknown'. Default: String. + Dword should be specified as a decimal. .PARAMETER SID - The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. - Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. + The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. + Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Set-RegistryKey -Key $blockedAppPath -Name 'Debugger' -Value $blockedAppDebuggerValue + Set-RegistryKey -Key $blockedAppPath -Name 'Debugger' -Value $blockedAppDebuggerValue .EXAMPLE - Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE' -Name 'Application' -Type 'Dword' -Value '1' + Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE' -Name 'Application' -Type 'Dword' -Value '1' .EXAMPLE - Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'Debugger' -Value $blockedAppDebuggerValue -Type String + Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'Debugger' -Value $blockedAppDebuggerValue -Type String .EXAMPLE - Set-RegistryKey -Key 'HKCU\Software\Microsoft\Example' -Name 'Data' -Value (0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x02,0x02,0x02) -Type 'Binary' + Set-RegistryKey -Key 'HKCU\Software\Microsoft\Example' -Name 'Data' -Value (0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x01,0x01,0x02,0x02,0x02) -Type 'Binary' .EXAMPLE Set-RegistryKey -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Example' -Value '(Default)' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - $Value, - [Parameter(Mandatory=$false)] - [ValidateSet('Binary','DWord','ExpandString','MultiString','None','QWord','String','Unknown')] - [Microsoft.Win32.RegistryValueKind]$Type = 'String', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$SID, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - [string]$RegistryValueWriteAction = 'set' - - ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - If ($PSBoundParameters.ContainsKey('SID')) { - [string]$key = Convert-RegistryPath -Key $key -SID $SID - } - Else { - [string]$key = Convert-RegistryPath -Key $key - } - - ## Create registry key if it doesn't exist - If (-not (Test-Path -LiteralPath $key -ErrorAction 'Stop')) { - Try { - Write-Log -Message "Create registry key [$key]." -Source ${CmdletName} - # No forward slash found in Key. Use New-Item cmdlet to create registry key - If ((($Key -split '/').Count - 1) -eq 0) - { - $null = New-Item -Path $key -ItemType 'Registry' -Force -ErrorAction 'Stop' - } - # Forward slash was found in Key. Use REG.exe ADD to create registry key - Else - { - [string]$CreateRegkeyResult = & reg.exe Add "$($Key.Substring($Key.IndexOf('::') + 2))" - If ($global:LastExitCode -ne 0) - { - Throw "Failed to create registry key [$Key]" - } - } - } - Catch { - Throw - } - } - - If ($Name) { - ## Set registry value if it doesn't exist - If (-not (Get-ItemProperty -LiteralPath $key -Name $Name -ErrorAction 'SilentlyContinue')) { - Write-Log -Message "Set registry key value: [$key] [$name = $value]." -Source ${CmdletName} - $null = New-ItemProperty -LiteralPath $key -Name $name -Value $value -PropertyType $Type -ErrorAction 'Stop' - } - ## Update registry value if it does exist - Else { - [string]$RegistryValueWriteAction = 'update' - If ($Name -eq '(Default)') { - ## Set Default registry key value with the following workaround, because Set-ItemProperty contains a bug and cannot set Default registry key value - $null = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').OpenSubKey('','ReadWriteSubTree').SetValue($null,$value) - } - Else { - Write-Log -Message "Update registry key value: [$key] [$name = $value]." -Source ${CmdletName} - $null = Set-ItemProperty -LiteralPath $key -Name $name -Value $value -ErrorAction 'Stop' - } - } - } - } - Catch { - If ($Name) { - Write-Log -Message "Failed to $RegistryValueWriteAction value [$value] for registry key [$key] [$name]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to $RegistryValueWriteAction value [$value] for registry key [$key] [$name]: $($_.Exception.Message)" - } - } - Else { - Write-Log -Message "Failed to set registry key [$key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to set registry key [$key]: $($_.Exception.Message)" - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + $Value, + [Parameter(Mandatory=$false)] + [ValidateSet('Binary','DWord','ExpandString','MultiString','None','QWord','String','Unknown')] + [Microsoft.Win32.RegistryValueKind]$Type = 'String', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$SID, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + [string]$RegistryValueWriteAction = 'set' + + ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID + If ($PSBoundParameters.ContainsKey('SID')) { + [string]$key = Convert-RegistryPath -Key $key -SID $SID + } + Else { + [string]$key = Convert-RegistryPath -Key $key + } + + ## Create registry key if it doesn't exist + If (-not (Test-Path -LiteralPath $key -ErrorAction 'Stop')) { + Try { + Write-Log -Message "Create registry key [$key]." -Source ${CmdletName} + # No forward slash found in Key. Use New-Item cmdlet to create registry key + If ((($Key -split '/').Count - 1) -eq 0) + { + $null = New-Item -Path $key -ItemType 'Registry' -Force -ErrorAction 'Stop' + } + # Forward slash was found in Key. Use REG.exe ADD to create registry key + Else + { + [string]$CreateRegkeyResult = & "$envWinDir\System32\reg.exe" Add "$($Key.Substring($Key.IndexOf('::') + 2))" + If ($global:LastExitCode -ne 0) + { + Throw "Failed to create registry key [$Key]" + } + } + } + Catch { + Throw + } + } + + If ($Name) { + ## Set registry value if it doesn't exist + If (-not (Get-ItemProperty -LiteralPath $key -Name $Name -ErrorAction 'SilentlyContinue')) { + Write-Log -Message "Set registry key value: [$key] [$name = $value]." -Source ${CmdletName} + $null = New-ItemProperty -LiteralPath $key -Name $name -Value $value -PropertyType $Type -ErrorAction 'Stop' + } + ## Update registry value if it does exist + Else { + [string]$RegistryValueWriteAction = 'update' + If ($Name -eq '(Default)') { + ## Set Default registry key value with the following workaround, because Set-ItemProperty contains a bug and cannot set Default registry key value + $null = $(Get-Item -LiteralPath $key -ErrorAction 'Stop').OpenSubKey('','ReadWriteSubTree').SetValue($null,$value) + } + Else { + Write-Log -Message "Update registry key value: [$key] [$name = $value]." -Source ${CmdletName} + $null = Set-ItemProperty -LiteralPath $key -Name $name -Value $value -ErrorAction 'Stop' + } + } + } + } + Catch { + If ($Name) { + Write-Log -Message "Failed to $RegistryValueWriteAction value [$value] for registry key [$key] [$name]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to $RegistryValueWriteAction value [$value] for registry key [$key] [$name]: $($_.Exception.Message)" + } + } + Else { + Write-Log -Message "Failed to set registry key [$key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to set registry key [$key]: $($_.Exception.Message)" + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4177,120 +4308,120 @@ Function Set-RegistryKey { Function Remove-RegistryKey { <# .SYNOPSIS - Deletes the specified registry key or value. + Deletes the specified registry key or value. .DESCRIPTION - Deletes the specified registry key or value. + Deletes the specified registry key or value. .PARAMETER Key - Path of the registry key to delete. + Path of the registry key to delete. .PARAMETER Name - Name of the registry value to delete. + Name of the registry value to delete. .PARAMETER Recurse - Delete registry key recursively. + Delete registry key recursively. .PARAMETER SID - The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. - Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. + The security identifier (SID) for a user. Specifying this parameter will convert a HKEY_CURRENT_USER registry key to the HKEY_USERS\$SID format. + Specify this parameter from the Invoke-HKCURegistrySettingsForAllUsers function to read/edit HKCU registry settings for all users on the system. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Remove-RegistryKey -Key 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce' + Remove-RegistryKey -Key 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce' .EXAMPLE - Remove-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name 'RunAppInstall' + Remove-RegistryKey -Key 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Run' -Name 'RunAppInstall' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [switch]$Recurse, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$SID, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID - If ($PSBoundParameters.ContainsKey('SID')) { - [string]$Key = Convert-RegistryPath -Key $Key -SID $SID - } - Else { - [string]$Key = Convert-RegistryPath -Key $Key - } - - If (-not ($Name)) { - If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { - If ($Recurse) { - Write-Log -Message "Delete registry key recursively [$Key]." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $Key -Force -Recurse -ErrorAction 'Stop' - } - Else { - If ($null -eq (Get-ChildItem -LiteralPath $Key -ErrorAction 'Stop')){ - ## Check if there are subkeys of $Key, if so, executing Remove-Item will hang. Avoiding this with Get-ChildItem. - Write-Log -Message "Delete registry key [$Key]." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $Key -Force -ErrorAction 'Stop' - } - Else { - Throw "Unable to delete child key(s) of [$Key] without [-Recurse] switch." - } - } - } - Else { - Write-Log -Message "Unable to delete registry key [$Key] because it does not exist." -Severity 2 -Source ${CmdletName} - } - } - Else { - If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { - Write-Log -Message "Delete registry value [$Key] [$Name]." -Source ${CmdletName} - - If ($Name -eq '(Default)') { - ## Remove (Default) registry key value with the following workaround because Remove-ItemProperty cannot remove the (Default) registry key value - $null = (Get-Item -LiteralPath $Key -ErrorAction 'Stop').OpenSubKey('','ReadWriteSubTree').DeleteValue('') - } - Else { - $null = Remove-ItemProperty -LiteralPath $Key -Name $Name -Force -ErrorAction 'Stop' - } - } - Else { - Write-Log -Message "Unable to delete registry value [$Key] [$Name] because registry key does not exist." -Severity 2 -Source ${CmdletName} - } - } - } - Catch [System.Management.Automation.PSArgumentException] { - Write-Log -Message "Unable to delete registry value [$Key] [$Name] because it does not exist." -Severity 2 -Source ${CmdletName} - } - Catch { - If (-not ($Name)) { - Write-Log -Message "Failed to delete registry key [$Key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to delete registry key [$Key]: $($_.Exception.Message)" - } - } - Else { - Write-Log -Message "Failed to delete registry value [$Key] [$Name]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to delete registry value [$Key] [$Name]: $($_.Exception.Message)" - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [switch]$Recurse, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$SID, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## If the SID variable is specified, then convert all HKEY_CURRENT_USER key's to HKEY_USERS\$SID + If ($PSBoundParameters.ContainsKey('SID')) { + [string]$Key = Convert-RegistryPath -Key $Key -SID $SID + } + Else { + [string]$Key = Convert-RegistryPath -Key $Key + } + + If (-not ($Name)) { + If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { + If ($Recurse) { + Write-Log -Message "Delete registry key recursively [$Key]." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $Key -Force -Recurse -ErrorAction 'Stop' + } + Else { + If ($null -eq (Get-ChildItem -LiteralPath $Key -ErrorAction 'Stop')){ + ## Check if there are subkeys of $Key, if so, executing Remove-Item will hang. Avoiding this with Get-ChildItem. + Write-Log -Message "Delete registry key [$Key]." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $Key -Force -ErrorAction 'Stop' + } + Else { + Throw "Unable to delete child key(s) of [$Key] without [-Recurse] switch." + } + } + } + Else { + Write-Log -Message "Unable to delete registry key [$Key] because it does not exist." -Severity 2 -Source ${CmdletName} + } + } + Else { + If (Test-Path -LiteralPath $Key -ErrorAction 'Stop') { + Write-Log -Message "Delete registry value [$Key] [$Name]." -Source ${CmdletName} + + If ($Name -eq '(Default)') { + ## Remove (Default) registry key value with the following workaround because Remove-ItemProperty cannot remove the (Default) registry key value + $null = (Get-Item -LiteralPath $Key -ErrorAction 'Stop').OpenSubKey('','ReadWriteSubTree').DeleteValue('') + } + Else { + $null = Remove-ItemProperty -LiteralPath $Key -Name $Name -Force -ErrorAction 'Stop' + } + } + Else { + Write-Log -Message "Unable to delete registry value [$Key] [$Name] because registry key does not exist." -Severity 2 -Source ${CmdletName} + } + } + } + Catch [System.Management.Automation.PSArgumentException] { + Write-Log -Message "Unable to delete registry value [$Key] [$Name] because it does not exist." -Severity 2 -Source ${CmdletName} + } + Catch { + If (-not ($Name)) { + Write-Log -Message "Failed to delete registry key [$Key]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to delete registry key [$Key]: $($_.Exception.Message)" + } + } + Else { + Write-Log -Message "Failed to delete registry value [$Key] [$Name]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to delete registry value [$Key] [$Name]: $($_.Exception.Message)" + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4299,109 +4430,109 @@ Function Remove-RegistryKey { Function Invoke-HKCURegistrySettingsForAllUsers { <# .SYNOPSIS - Set current user registry settings for all current users and any new users in the future. + Set current user registry settings for all current users and any new users in the future. .DESCRIPTION - Set HKCU registry settings for all current and future users by loading their NTUSER.dat registry hive file, and making the modifications. - This function will modify HKCU settings for all users even when executed under the SYSTEM account. - To ensure new users in the future get the registry edits, the Default User registry hive used to provision the registry for new users is modified. - This function can be used as an alternative to using ActiveSetup for registry settings. - The advantage of using this function over ActiveSetup is that a user does not have to log off and log back on before the changes take effect. + Set HKCU registry settings for all current and future users by loading their NTUSER.dat registry hive file, and making the modifications. + This function will modify HKCU settings for all users even when executed under the SYSTEM account. + To ensure new users in the future get the registry edits, the Default User registry hive used to provision the registry for new users is modified. + This function can be used as an alternative to using ActiveSetup for registry settings. + The advantage of using this function over ActiveSetup is that a user does not have to log off and log back on before the changes take effect. .PARAMETER RegistrySettings - Script block which contains HKCU registry settings which should be modified for all users on the system. Must specify the -SID parameter for all HKCU settings. + Script block which contains HKCU registry settings which should be modified for all users on the system. Must specify the -SID parameter for all HKCU settings. .PARAMETER UserProfiles - Specify the user profiles to modify HKCU registry settings for. Default is all user profiles except for system profiles. + Specify the user profiles to modify HKCU registry settings for. Default is all user profiles except for system profiles. .EXAMPLE - [scriptblock]$HKCURegistrySettings = { - Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'qmenable' -Value 0 -Type DWord -SID $UserProfile.SID - Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'updatereliabilitydata' -Value 1 -Type DWord -SID $UserProfile.SID - } - Invoke-HKCURegistrySettingsForAllUsers -RegistrySettings $HKCURegistrySettings + [scriptblock]$HKCURegistrySettings = { + Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'qmenable' -Value 0 -Type DWord -SID $UserProfile.SID + Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'updatereliabilitydata' -Value 1 -Type DWord -SID $UserProfile.SID + } + Invoke-HKCURegistrySettingsForAllUsers -RegistrySettings $HKCURegistrySettings .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [scriptblock]$RegistrySettings, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [psobject[]]$UserProfiles = (Get-UserProfiles) - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ForEach ($UserProfile in $UserProfiles) { - Try { - # Set the path to the user's registry hive when it is loaded - [string]$UserRegistryPath = "Registry::HKEY_USERS\$($UserProfile.SID)" - - # Set the path to the user's registry hive file - [string]$UserRegistryHiveFile = Join-Path -Path $UserProfile.ProfilePath -ChildPath 'NTUSER.DAT' - - # Load the User profile registry hive if it is not already loaded because the User is logged in - [boolean]$ManuallyLoadedRegHive = $false - If (-not (Test-Path -LiteralPath $UserRegistryPath)) { - # Load the User registry hive if the registry hive file exists - If (Test-Path -LiteralPath $UserRegistryHiveFile -PathType 'Leaf') { - Write-Log -Message "Load the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe load "`"HKEY_USERS\$($UserProfile.SID)`"" "`"$UserRegistryHiveFile`"" - - If ($global:LastExitCode -ne 0) { - Throw "Failed to load the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. Failure message [$HiveLoadResult]. Continue..." - } - - [boolean]$ManuallyLoadedRegHive = $true - } - Else { - Throw "Failed to find the registry hive file [$UserRegistryHiveFile] for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. Continue..." - } - } - Else { - Write-Log -Message "The User [$($UserProfile.NTAccount)] registry hive is already loaded in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - } - - ## Execute ScriptBlock which contains code to manipulate HKCU registry. - # Make sure read/write calls to the HKCU registry hive specify the -SID parameter or settings will not be changed for all users. - # Example: Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'qmenable' -Value 0 -Type DWord -SID $UserProfile.SID - Write-Log -Message 'Execute ScriptBlock to modify HKCU registry settings for all users.' -Source ${CmdletName} - & $RegistrySettings - } - Catch { - Write-Log -Message "Failed to modify the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)] `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - Finally { - If ($ManuallyLoadedRegHive) { - Try { - Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe unload "`"HKEY_USERS\$($UserProfile.SID)`"" - - If ($global:LastExitCode -ne 0) { - Write-Log -Message "REG.exe failed to unload the registry hive and exited with exit code [$($global:LastExitCode)]. Performing manual garbage collection to ensure successful unloading of registry hive." -Severity 2 -Source ${CmdletName} - [GC]::Collect() - [GC]::WaitForPendingFinalizers() - Start-Sleep -Seconds 5 - - Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} - [string]$HiveLoadResult = & reg.exe unload "`"HKEY_USERS\$($UserProfile.SID)`"" - If ($global:LastExitCode -ne 0) { Throw "REG.exe failed with exit code [$($global:LastExitCode)] and result [$HiveLoadResult]." } - } - } - Catch { - Write-Log -Message "Failed to unload the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [scriptblock]$RegistrySettings, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [psobject[]]$UserProfiles = (Get-UserProfiles) + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ForEach ($UserProfile in $UserProfiles) { + Try { + # Set the path to the user's registry hive when it is loaded + [string]$UserRegistryPath = "Registry::HKEY_USERS\$($UserProfile.SID)" + + # Set the path to the user's registry hive file + [string]$UserRegistryHiveFile = Join-Path -Path $UserProfile.ProfilePath -ChildPath 'NTUSER.DAT' + + # Load the User profile registry hive if it is not already loaded because the User is logged in + [boolean]$ManuallyLoadedRegHive = $false + If (-not (Test-Path -LiteralPath $UserRegistryPath)) { + # Load the User registry hive if the registry hive file exists + If (Test-Path -LiteralPath $UserRegistryHiveFile -PathType 'Leaf') { + Write-Log -Message "Load the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" load "`"HKEY_USERS\$($UserProfile.SID)`"" "`"$UserRegistryHiveFile`"" + + If ($global:LastExitCode -ne 0) { + Throw "Failed to load the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. Failure message [$HiveLoadResult]. Continue..." + } + + [boolean]$ManuallyLoadedRegHive = $true + } + Else { + Throw "Failed to find the registry hive file [$UserRegistryHiveFile] for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. Continue..." + } + } + Else { + Write-Log -Message "The User [$($UserProfile.NTAccount)] registry hive is already loaded in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} + } + + ## Execute ScriptBlock which contains code to manipulate HKCU registry. + # Make sure read/write calls to the HKCU registry hive specify the -SID parameter or settings will not be changed for all users. + # Example: Set-RegistryKey -Key 'HKCU\Software\Microsoft\Office\14.0\Common' -Name 'qmenable' -Value 0 -Type DWord -SID $UserProfile.SID + Write-Log -Message 'Execute ScriptBlock to modify HKCU registry settings for all users.' -Source ${CmdletName} + & $RegistrySettings + } + Catch { + Write-Log -Message "Failed to modify the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)] `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + Finally { + If ($ManuallyLoadedRegHive) { + Try { + Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" unload "`"HKEY_USERS\$($UserProfile.SID)`"" + + If ($global:LastExitCode -ne 0) { + Write-Log -Message "REG.exe failed to unload the registry hive and exited with exit code [$($global:LastExitCode)]. Performing manual garbage collection to ensure successful unloading of registry hive." -Severity 2 -Source ${CmdletName} + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Start-Sleep -Seconds 5 + + Write-Log -Message "Unload the User [$($UserProfile.NTAccount)] registry hive in path [HKEY_USERS\$($UserProfile.SID)]." -Source ${CmdletName} + [string]$HiveLoadResult = & "$envWinDir\System32\reg.exe" unload "`"HKEY_USERS\$($UserProfile.SID)`"" + If ($global:LastExitCode -ne 0) { Throw "REG.exe failed with exit code [$($global:LastExitCode)] and result [$HiveLoadResult]." } + } + } + Catch { + Write-Log -Message "Failed to unload the registry hive for User [$($UserProfile.NTAccount)] with SID [$($UserProfile.SID)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4410,119 +4541,119 @@ Function Invoke-HKCURegistrySettingsForAllUsers { Function ConvertTo-NTAccountOrSID { <# .SYNOPSIS - Convert between NT Account names and their security identifiers (SIDs). + Convert between NT Account names and their security identifiers (SIDs). .DESCRIPTION - Specify either the NT Account name or the SID and get the other. Can also convert well known sid types. + Specify either the NT Account name or the SID and get the other. Can also convert well known sid types. .PARAMETER AccountName - The Windows NT Account name specified in \ format. - Use fully qualified account names (e.g., \) instead of isolated names (e.g, ) because they are unambiguous and provide better performance. + The Windows NT Account name specified in \ format. + Use fully qualified account names (e.g., \) instead of isolated names (e.g, ) because they are unambiguous and provide better performance. .PARAMETER SID - The Windows NT Account SID. + The Windows NT Account SID. .PARAMETER WellKnownSIDName - Specify the Well Known SID name translate to the actual SID (e.g., LocalServiceSid). - To get all well known SIDs available on system: [enum]::GetNames([Security.Principal.WellKnownSidType]) + Specify the Well Known SID name translate to the actual SID (e.g., LocalServiceSid). + To get all well known SIDs available on system: [enum]::GetNames([Security.Principal.WellKnownSidType]) .PARAMETER WellKnownToNTAccount - Convert the Well Known SID to an NTAccount name + Convert the Well Known SID to an NTAccount name .EXAMPLE - ConvertTo-NTAccountOrSID -AccountName 'CONTOSO\User1' - Converts a Windows NT Account name to the corresponding SID + ConvertTo-NTAccountOrSID -AccountName 'CONTOSO\User1' + Converts a Windows NT Account name to the corresponding SID .EXAMPLE - ConvertTo-NTAccountOrSID -SID 'S-1-5-21-1220945662-2111687655-725345543-14012660' - Converts a Windows NT Account SID to the corresponding NT Account Name + ConvertTo-NTAccountOrSID -SID 'S-1-5-21-1220945662-2111687655-725345543-14012660' + Converts a Windows NT Account SID to the corresponding NT Account Name .EXAMPLE - ConvertTo-NTAccountOrSID -WellKnownSIDName 'NetworkServiceSid' - Converts a Well Known SID name to a SID + ConvertTo-NTAccountOrSID -WellKnownSIDName 'NetworkServiceSid' + Converts a Well Known SID name to a SID .NOTES - This is an internal script function and should typically not be called directly. - The conversion can return an empty result if the user account does not exist anymore or if translation fails. - http://blogs.technet.com/b/askds/archive/2011/07/28/troubleshooting-sid-translation-failures-from-the-obvious-to-the-not-so-obvious.aspx + This is an internal script function and should typically not be called directly. + The conversion can return an empty result if the user account does not exist anymore or if translation fails. + http://blogs.technet.com/b/askds/archive/2011/07/28/troubleshooting-sid-translation-failures-from-the-obvious-to-the-not-so-obvious.aspx .LINK - http://psappdeploytoolkit.com - List of Well Known SIDs: http://msdn.microsoft.com/en-us/library/system.security.principal.wellknownsidtype(v=vs.110).aspx + http://psappdeploytoolkit.com + List of Well Known SIDs: http://msdn.microsoft.com/en-us/library/system.security.principal.wellknownsidtype(v=vs.110).aspx #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,ParameterSetName='NTAccountToSID',ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string]$AccountName, - [Parameter(Mandatory=$true,ParameterSetName='SIDToNTAccount',ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string]$SID, - [Parameter(Mandatory=$true,ParameterSetName='WellKnownName',ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string]$WellKnownSIDName, - [Parameter(Mandatory=$false,ParameterSetName='WellKnownName')] - [ValidateNotNullOrEmpty()] - [switch]$WellKnownToNTAccount - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Switch ($PSCmdlet.ParameterSetName) { - 'SIDToNTAccount' { - [string]$msg = "the SID [$SID] to an NT Account name" - Write-Log -Message "Convert $msg." -Source ${CmdletName} - - $NTAccountSID = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $SID - $NTAccount = $NTAccountSID.Translate([Security.Principal.NTAccount]) - Write-Output -InputObject $NTAccount - } - 'NTAccountToSID' { - [string]$msg = "the NT Account [$AccountName] to a SID" - Write-Log -Message "Convert $msg." -Source ${CmdletName} - - $NTAccount = New-Object -TypeName 'System.Security.Principal.NTAccount' -ArgumentList $AccountName - $NTAccountSID = $NTAccount.Translate([Security.Principal.SecurityIdentifier]) - Write-Output -InputObject $NTAccountSID - } - 'WellKnownName' { - If ($WellKnownToNTAccount) { - [string]$ConversionType = 'NTAccount' - } - Else { - [string]$ConversionType = 'SID' - } - [string]$msg = "the Well Known SID Name [$WellKnownSIDName] to a $ConversionType" - Write-Log -Message "Convert $msg." -Source ${CmdletName} - - # Get the SID for the root domain - Try { - $MachineRootDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'Stop').Domain.ToLower() - $ADDomainObj = New-Object -TypeName 'System.DirectoryServices.DirectoryEntry' -ArgumentList "LDAP://$MachineRootDomain" - $DomainSidInBinary = $ADDomainObj.ObjectSid - $DomainSid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ($DomainSidInBinary[0], 0) - } - Catch { - Write-Log -Message 'Unable to get Domain SID from Active Directory. Setting Domain SID to $null.' -Severity 2 -Source ${CmdletName} - $DomainSid = $null - } - - # Get the SID for the well known SID name - $WellKnownSidType = [Security.Principal.WellKnownSidType]::$WellKnownSIDName - $NTAccountSID = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ($WellKnownSidType, $DomainSid) - - If ($WellKnownToNTAccount) { - $NTAccount = $NTAccountSID.Translate([Security.Principal.NTAccount]) - Write-Output -InputObject $NTAccount - } - Else { - Write-Output -InputObject $NTAccountSID - } - } - } - } - Catch { - Write-Log -Message "Failed to convert $msg. It may not be a valid account anymore or there is some other problem. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,ParameterSetName='NTAccountToSID',ValueFromPipelineByPropertyName=$true)] + [ValidateNotNullOrEmpty()] + [string]$AccountName, + [Parameter(Mandatory=$true,ParameterSetName='SIDToNTAccount',ValueFromPipelineByPropertyName=$true)] + [ValidateNotNullOrEmpty()] + [string]$SID, + [Parameter(Mandatory=$true,ParameterSetName='WellKnownName',ValueFromPipelineByPropertyName=$true)] + [ValidateNotNullOrEmpty()] + [string]$WellKnownSIDName, + [Parameter(Mandatory=$false,ParameterSetName='WellKnownName')] + [ValidateNotNullOrEmpty()] + [switch]$WellKnownToNTAccount + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Switch ($PSCmdlet.ParameterSetName) { + 'SIDToNTAccount' { + [string]$msg = "the SID [$SID] to an NT Account name" + Write-Log -Message "Convert $msg." -Source ${CmdletName} + + $NTAccountSID = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList $SID + $NTAccount = $NTAccountSID.Translate([Security.Principal.NTAccount]) + Write-Output -InputObject $NTAccount + } + 'NTAccountToSID' { + [string]$msg = "the NT Account [$AccountName] to a SID" + Write-Log -Message "Convert $msg." -Source ${CmdletName} + + $NTAccount = New-Object -TypeName 'System.Security.Principal.NTAccount' -ArgumentList $AccountName + $NTAccountSID = $NTAccount.Translate([Security.Principal.SecurityIdentifier]) + Write-Output -InputObject $NTAccountSID + } + 'WellKnownName' { + If ($WellKnownToNTAccount) { + [string]$ConversionType = 'NTAccount' + } + Else { + [string]$ConversionType = 'SID' + } + [string]$msg = "the Well Known SID Name [$WellKnownSIDName] to a $ConversionType" + Write-Log -Message "Convert $msg." -Source ${CmdletName} + + # Get the SID for the root domain + Try { + $MachineRootDomain = (Get-WmiObject -Class 'Win32_ComputerSystem' -ErrorAction 'Stop').Domain.ToLower() + $ADDomainObj = New-Object -TypeName 'System.DirectoryServices.DirectoryEntry' -ArgumentList "LDAP://$MachineRootDomain" + $DomainSidInBinary = $ADDomainObj.ObjectSid + $DomainSid = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ($DomainSidInBinary[0], 0) + } + Catch { + Write-Log -Message 'Unable to get Domain SID from Active Directory. Setting Domain SID to $null.' -Severity 2 -Source ${CmdletName} + $DomainSid = $null + } + + # Get the SID for the well known SID name + $WellKnownSidType = [Security.Principal.WellKnownSidType]::$WellKnownSIDName + $NTAccountSID = New-Object -TypeName 'System.Security.Principal.SecurityIdentifier' -ArgumentList ($WellKnownSidType, $DomainSid) + + If ($WellKnownToNTAccount) { + $NTAccount = $NTAccountSID.Translate([Security.Principal.NTAccount]) + Write-Output -InputObject $NTAccount + } + Else { + Write-Output -InputObject $NTAccountSID + } + } + } + } + Catch { + Write-Log -Message "Failed to convert $msg. It may not be a valid account anymore or there is some other problem. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4531,105 +4662,106 @@ Function ConvertTo-NTAccountOrSID { Function Get-UserProfiles { <# .SYNOPSIS - Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine and also the Default User (which does not log on). + Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine and also the Default User (which does not log on). .DESCRIPTION - Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine and also the Default User (which does not log on). - Please note that the NTAccount property may be empty for some user profiles but the SID and ProfilePath properties will always be populated. + Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine and also the Default User (which does not log on). + Please note that the NTAccount property may be empty for some user profiles but the SID and ProfilePath properties will always be populated. .PARAMETER ExcludeNTAccount - Specify NT account names in Domain\Username format to exclude from the list of user profiles. + Specify NT account names in Domain\Username format to exclude from the list of user profiles. .PARAMETER ExcludeSystemProfiles - Exclude system profiles: SYSTEM, LOCAL SERVICE, NETWORK SERVICE. Default is: $true. + Exclude system profiles: SYSTEM, LOCAL SERVICE, NETWORK SERVICE. Default is: $true. .PARAMETER ExcludeDefaultUser - Exclude the Default User. Default is: $false. + Exclude the Default User. Default is: $false. .EXAMPLE - Get-UserProfiles - Returns the following properties for each user profile on the system: NTAccount, SID, ProfilePath + Get-UserProfiles + Returns the following properties for each user profile on the system: NTAccount, SID, ProfilePath .EXAMPLE - Get-UserProfiles -ExcludeNTAccount 'CONTOSO\Robot','CONTOSO\ntadmin' + Get-UserProfiles -ExcludeNTAccount 'CONTOSO\Robot','CONTOSO\ntadmin' .EXAMPLE - [string[]]$ProfilePaths = Get-UserProfiles | Select-Object -ExpandProperty 'ProfilePath' - Returns the user profile path for each user on the system. This information can then be used to make modifications under the user profile on the filesystem. + [string[]]$ProfilePaths = Get-UserProfiles | Select-Object -ExpandProperty 'ProfilePath' + Returns the user profile path for each user on the system. This information can then be used to make modifications under the user profile on the filesystem. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string[]]$ExcludeNTAccount, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ExcludeSystemProfiles = $true, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$ExcludeDefaultUser = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Get the User Profile Path, User Account SID, and the User Account Name for all users that log onto the machine.' -Source ${CmdletName} - - ## Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine - [string]$UserProfileListRegKey = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' - [psobject[]]$UserProfiles = Get-ChildItem -LiteralPath $UserProfileListRegKey -ErrorAction 'Stop' | - ForEach-Object { - Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'Stop' | Where-Object { ($_.ProfileImagePath) } | - Select-Object @{ Label = 'NTAccount'; Expression = { $(ConvertTo-NTAccountOrSID -SID $_.PSChildName).Value } }, @{ Label = 'SID'; Expression = { $_.PSChildName } }, @{ Label = 'ProfilePath'; Expression = { $_.ProfileImagePath } } - } - If ($ExcludeSystemProfiles) { - [string[]]$SystemProfiles = 'S-1-5-18', 'S-1-5-19', 'S-1-5-20' - [psobject[]]$UserProfiles = $UserProfiles | Where-Object { $SystemProfiles -notcontains $_.SID } - } - If ($ExcludeNTAccount) { - [psobject[]]$UserProfiles = $UserProfiles | Where-Object { $ExcludeNTAccount -notcontains $_.NTAccount } - } - - ## Find the path to the Default User profile - If (-not $ExcludeDefaultUser) { - [string]$UserProfilesDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'ProfilesDirectory' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'ProfilesDirectory' - - # On Windows Vista or higher - If (([version]$envOSVersion).Major -gt 5) { - # Path to Default User Profile directory on Windows Vista or higher: By default, C:\Users\Default - [string]$DefaultUserProfileDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'Default' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Default' - } - # On Windows XP or lower - Else { - # Default User Profile Name: By default, 'Default User' - [string]$DefaultUserProfileName = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'DefaultUserProfile' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DefaultUserProfile' - - # Path to Default User Profile directory: By default, C:\Documents and Settings\Default User - [string]$DefaultUserProfileDirectory = Join-Path -Path $UserProfilesDirectory -ChildPath $DefaultUserProfileName - } - - ## Create a custom object for the Default User profile. - # Since the Default User is not an actual User account, it does not have a username or a SID. - # We will make up a SID and add it to the custom object so that we have a location to load the default registry hive into later on. - [psobject]$DefaultUserProfile = New-Object -TypeName 'PSObject' -Property @{ - NTAccount = 'Default User' - SID = 'S-1-5-21-Default-User' - ProfilePath = $DefaultUserProfileDirectory - } - - ## Add the Default User custom object to the User Profile list. - $UserProfiles += $DefaultUserProfile - } - - Write-Output -InputObject $UserProfiles - } - Catch { - Write-Log -Message "Failed to create a custom object representing all user profiles on the machine. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string[]]$ExcludeNTAccount, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ExcludeSystemProfiles = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$ExcludeDefaultUser = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Get the User Profile Path, User Account SID, and the User Account Name for all users that log onto the machine.' -Source ${CmdletName} + + ## Get the User Profile Path, User Account Sid, and the User Account Name for all users that log onto the machine + [string]$UserProfileListRegKey = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + [psobject[]]$UserProfiles = Get-ChildItem -LiteralPath $UserProfileListRegKey -ErrorAction 'Stop' | + ForEach-Object { + Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'Stop' | Where-Object { ($_.ProfileImagePath) } | + Select-Object @{ Label = 'NTAccount'; Expression = { $(ConvertTo-NTAccountOrSID -SID $_.PSChildName).Value } }, @{ Label = 'SID'; Expression = { $_.PSChildName } }, @{ Label = 'ProfilePath'; Expression = { $_.ProfileImagePath } } + } | + Where-Object { $_.NTAccount } ## This removes "defaultuser0" account, which is Windows's 10 bug + If ($ExcludeSystemProfiles) { + [string[]]$SystemProfiles = 'S-1-5-18', 'S-1-5-19', 'S-1-5-20' + [psobject[]]$UserProfiles = $UserProfiles | Where-Object { $SystemProfiles -notcontains $_.SID } + } + If ($ExcludeNTAccount) { + [psobject[]]$UserProfiles = $UserProfiles | Where-Object { $ExcludeNTAccount -notcontains $_.NTAccount } + } + + ## Find the path to the Default User profile + If (-not $ExcludeDefaultUser) { + [string]$UserProfilesDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'ProfilesDirectory' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'ProfilesDirectory' + + # On Windows Vista or higher + If (([version]$envOSVersion).Major -gt 5) { + # Path to Default User Profile directory on Windows Vista or higher: By default, C:\Users\Default + [string]$DefaultUserProfileDirectory = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'Default' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Default' + } + # On Windows XP or lower + Else { + # Default User Profile Name: By default, 'Default User' + [string]$DefaultUserProfileName = Get-ItemProperty -LiteralPath $UserProfileListRegKey -Name 'DefaultUserProfile' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DefaultUserProfile' + + # Path to Default User Profile directory: By default, C:\Documents and Settings\Default User + [string]$DefaultUserProfileDirectory = Join-Path -Path $UserProfilesDirectory -ChildPath $DefaultUserProfileName + } + + ## Create a custom object for the Default User profile. + # Since the Default User is not an actual User account, it does not have a username or a SID. + # We will make up a SID and add it to the custom object so that we have a location to load the default registry hive into later on. + [psobject]$DefaultUserProfile = New-Object -TypeName 'PSObject' -Property @{ + NTAccount = 'Default User' + SID = 'S-1-5-21-Default-User' + ProfilePath = $DefaultUserProfileDirectory + } + + ## Add the Default User custom object to the User Profile list. + $UserProfiles += $DefaultUserProfile + } + + Write-Output -InputObject $UserProfiles + } + Catch { + Write-Log -Message "Failed to create a custom object representing all user profiles on the machine. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4638,79 +4770,79 @@ Function Get-UserProfiles { Function Get-FileVersion { <# .SYNOPSIS - Gets the version of the specified file + Gets the version of the specified file .DESCRIPTION - Gets the version of the specified file + Gets the version of the specified file .PARAMETER File - Path of the file + Path of the file .PARAMETER ProductVersion - Switch that makes the command return ProductVersion instead of FileVersion + Switch that makes the command return ProductVersion instead of FileVersion .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-FileVersion -File "$envProgramFilesX86\Adobe\Reader 11.0\Reader\AcroRd32.exe" + Get-FileVersion -File "$envProgramFilesX86\Adobe\Reader 11.0\Reader\AcroRd32.exe" .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$File, - [Parameter(Mandatory=$false)] - [switch]$ProductVersion, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Get version info for file [$file]." -Source ${CmdletName} - - If (Test-Path -LiteralPath $File -PathType 'Leaf') { - $fileVersionInfo = (Get-Command -Name $file -ErrorAction 'Stop').FileVersionInfo - If ($ProductVersion) { - $fileVersion = $fileVersionInfo.ProductVersion - } else { - $fileVersion = $fileVersionInfo.FileVersion - } - - If ($fileVersion) { - If ($ProductVersion) { - Write-Log -Message "Product version is [$fileVersion]." -Source ${CmdletName} - } - else - { - Write-Log -Message "File version is [$fileVersion]." -Source ${CmdletName} - } - - Write-Output -InputObject $fileVersion - } - Else { - Write-Log -Message 'No version information found.' -Source ${CmdletName} - } - } - Else { - Throw "File path [$file] does not exist." - } - } - Catch { - Write-Log -Message "Failed to get version info. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to get version info: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$File, + [Parameter(Mandatory=$false)] + [switch]$ProductVersion, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Get version info for file [$file]." -Source ${CmdletName} + + If (Test-Path -LiteralPath $File -PathType 'Leaf') { + $fileVersionInfo = (Get-Command -Name $file -ErrorAction 'Stop').FileVersionInfo + If ($ProductVersion) { + $fileVersion = $fileVersionInfo.ProductVersion + } else { + $fileVersion = $fileVersionInfo.FileVersion + } + + If ($fileVersion) { + If ($ProductVersion) { + Write-Log -Message "Product version is [$fileVersion]." -Source ${CmdletName} + } + else + { + Write-Log -Message "File version is [$fileVersion]." -Source ${CmdletName} + } + + Write-Output -InputObject $fileVersion + } + Else { + Write-Log -Message 'No version information found.' -Source ${CmdletName} + } + } + Else { + Throw "File path [$file] does not exist." + } + } + Catch { + Write-Log -Message "Failed to get version info. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to get version info: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4719,153 +4851,153 @@ Function Get-FileVersion { Function New-Shortcut { <# .SYNOPSIS - Creates a new .lnk or .url type shortcut + Creates a new .lnk or .url type shortcut .DESCRIPTION - Creates a new shortcut .lnk or .url file, with configurable options + Creates a new shortcut .lnk or .url file, with configurable options .PARAMETER Path - Path to save the shortcut + Path to save the shortcut .PARAMETER TargetPath - Target path or URL that the shortcut launches + Target path or URL that the shortcut launches .PARAMETER Arguments - Arguments to be passed to the target path + Arguments to be passed to the target path .PARAMETER IconLocation - Location of the icon used for the shortcut + Location of the icon used for the shortcut .PARAMETER IconIndex - Executables, DLLs, ICO files with multiple icons need the icon index to be specified + Executables, DLLs, ICO files with multiple icons need the icon index to be specified .PARAMETER Description - Description of the shortcut + Description of the shortcut .PARAMETER WorkingDirectory - Working Directory to be used for the target path + Working Directory to be used for the target path .PARAMETER WindowStyle - Windows style of the application. Options: Normal, Maximized, Minimized. Default is: Normal. + Windows style of the application. Options: Normal, Maximized, Minimized. Default is: Normal. .PARAMETER RunAsAdmin - Set shortcut to run program as administrator. This option will prompt user to elevate when executing shortcut. + Set shortcut to run program as administrator. This option will prompt user to elevate when executing shortcut. .PARAMETER Hotkey Create a Hotkey to launch the shortcut, e.g. "CTRL+SHIFT+F" .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - New-Shortcut -Path "$envProgramData\Microsoft\Windows\Start Menu\My Shortcut.lnk" -TargetPath "$envWinDir\system32\notepad.exe" -IconLocation "$envWinDir\system32\notepad.exe" -Description 'Notepad' -WorkingDirectory "$envHomeDrive\$envHomePath" + New-Shortcut -Path "$envProgramData\Microsoft\Windows\Start Menu\My Shortcut.lnk" -TargetPath "$envWinDir\system32\notepad.exe" -IconLocation "$envWinDir\system32\notepad.exe" -Description 'Notepad' -WorkingDirectory "$envHomeDrive\$envHomePath" .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Path, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$TargetPath, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Arguments, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$IconLocation, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$IconIndex, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Description, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$WorkingDirectory, - [Parameter(Mandatory=$false)] - [ValidateSet('Normal','Maximized','Minimized')] - [string]$WindowStyle, - [Parameter(Mandatory=$false)] - [switch]$RunAsAdmin, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Hotkey, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - If (-not $Shell) { [__comobject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'Stop' } - } - Process { - Try { - Try { - [IO.FileInfo]$Path = [IO.FileInfo]$Path - [string]$PathDirectory = $Path.DirectoryName - - If (-not (Test-Path -LiteralPath $PathDirectory -PathType 'Container' -ErrorAction 'Stop')) { - Write-Log -Message "Create shortcut directory [$PathDirectory]." -Source ${CmdletName} - $null = New-Item -Path $PathDirectory -ItemType 'Directory' -Force -ErrorAction 'Stop' - } - } - Catch { - Write-Log -Message "Failed to create shortcut directory [$PathDirectory]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - Throw - } - - Write-Log -Message "Create shortcut [$($path.FullName)]." -Source ${CmdletName} - If (($path.FullName).ToLower().EndsWith('.url')) { - [string[]]$URLFile = '[InternetShortcut]' - $URLFile += "URL=$targetPath" - If ($iconIndex) { $URLFile += "IconIndex=$iconIndex" } - If ($IconLocation) { $URLFile += "IconFile=$iconLocation" } - $URLFile | Out-File -FilePath $path.FullName -Force -Encoding 'default' -ErrorAction 'Stop' - } - ElseIf (($path.FullName).ToLower().EndsWith('.lnk')) { - If (($iconLocation -and $iconIndex) -and (-not ($iconLocation.Contains(',')))) { - $iconLocation = $iconLocation + ",$iconIndex" - } - Switch ($windowStyle) { - 'Normal' { $windowStyleInt = 1 } - 'Maximized' { $windowStyleInt = 3 } - 'Minimized' { $windowStyleInt = 7 } - Default { $windowStyleInt = 1 } - } - $shortcut = $shell.CreateShortcut($path.FullName) - $shortcut.TargetPath = $targetPath - $shortcut.Arguments = $arguments - $shortcut.Description = $description - $shortcut.WorkingDirectory = $workingDirectory - $shortcut.WindowStyle = $windowStyleInt + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$TargetPath, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Arguments, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$IconLocation, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$IconIndex, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Description, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$WorkingDirectory, + [Parameter(Mandatory=$false)] + [ValidateSet('Normal','Maximized','Minimized')] + [string]$WindowStyle, + [Parameter(Mandatory=$false)] + [switch]$RunAsAdmin, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Hotkey, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + If (-not $Shell) { [__comobject]$Shell = New-Object -ComObject 'WScript.Shell' -ErrorAction 'Stop' } + } + Process { + Try { + Try { + [IO.FileInfo]$Path = [IO.FileInfo]$Path + [string]$PathDirectory = $Path.DirectoryName + + If (-not (Test-Path -LiteralPath $PathDirectory -PathType 'Container' -ErrorAction 'Stop')) { + Write-Log -Message "Create shortcut directory [$PathDirectory]." -Source ${CmdletName} + $null = New-Item -Path $PathDirectory -ItemType 'Directory' -Force -ErrorAction 'Stop' + } + } + Catch { + Write-Log -Message "Failed to create shortcut directory [$PathDirectory]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Throw + } + + Write-Log -Message "Create shortcut [$($path.FullName)]." -Source ${CmdletName} + If (($path.FullName).ToLower().EndsWith('.url')) { + [string[]]$URLFile = '[InternetShortcut]' + $URLFile += "URL=$targetPath" + If ($iconIndex) { $URLFile += "IconIndex=$iconIndex" } + If ($IconLocation) { $URLFile += "IconFile=$iconLocation" } + $URLFile | Out-File -FilePath $path.FullName -Force -Encoding 'default' -ErrorAction 'Stop' + } + ElseIf (($path.FullName).ToLower().EndsWith('.lnk')) { + If (($iconLocation -and $iconIndex) -and (-not ($iconLocation.Contains(',')))) { + $iconLocation = $iconLocation + ",$iconIndex" + } + Switch ($windowStyle) { + 'Normal' { $windowStyleInt = 1 } + 'Maximized' { $windowStyleInt = 3 } + 'Minimized' { $windowStyleInt = 7 } + Default { $windowStyleInt = 1 } + } + $shortcut = $shell.CreateShortcut($path.FullName) + $shortcut.TargetPath = $targetPath + $shortcut.Arguments = $arguments + $shortcut.Description = $description + $shortcut.WorkingDirectory = $workingDirectory + $shortcut.WindowStyle = $windowStyleInt If ($hotkey) {$shortcut.Hotkey = $hotkey} - If ($iconLocation) { $shortcut.IconLocation = $iconLocation } - $shortcut.Save() - - ## Set shortcut to run program as administrator - If ($RunAsAdmin) { - Write-Log -Message 'Set shortcut to run program as administrator.' -Source ${CmdletName} - $TempFileName = [IO.Path]::GetRandomFileName() - $TempFile = [IO.FileInfo][IO.Path]::Combine($Path.Directory, $TempFileName) - $Writer = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($TempFile, ([IO.FileMode]::Create)) -ErrorAction 'Stop' - $Reader = $Path.OpenRead() - While ($Reader.Position -lt $Reader.Length) { - $Byte = $Reader.ReadByte() - If ($Reader.Position -eq 22) { $Byte = 34 } - $Writer.WriteByte($Byte) - } - $Reader.Close() - $Writer.Close() - $Path.Delete() - $null = Rename-Item -Path $TempFile -NewName $Path.Name -Force -ErrorAction 'Stop' - } - } - } - Catch { - Write-Log -Message "Failed to create shortcut [$($path.FullName)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to create shortcut [$($path.FullName)]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + If ($iconLocation) { $shortcut.IconLocation = $iconLocation } + $shortcut.Save() + + ## Set shortcut to run program as administrator + If ($RunAsAdmin) { + Write-Log -Message 'Set shortcut to run program as administrator.' -Source ${CmdletName} + $TempFileName = [IO.Path]::GetRandomFileName() + $TempFile = [IO.FileInfo][IO.Path]::Combine($Path.Directory, $TempFileName) + $Writer = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($TempFile, ([IO.FileMode]::Create)) -ErrorAction 'Stop' + $Reader = $Path.OpenRead() + While ($Reader.Position -lt $Reader.Length) { + $Byte = $Reader.ReadByte() + If ($Reader.Position -eq 22) { $Byte = 34 } + $Writer.WriteByte($Byte) + } + $Reader.Close() + $Writer.Close() + $Path.Delete() + $null = Rename-Item -Path $TempFile -NewName $Path.Name -Force -ErrorAction 'Stop' + } + } + } + Catch { + Write-Log -Message "Failed to create shortcut [$($path.FullName)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to create shortcut [$($path.FullName)]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -4874,286 +5006,324 @@ Function New-Shortcut { Function Execute-ProcessAsUser { <# .SYNOPSIS - Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. + Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. .DESCRIPTION - Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. + Execute a process with a logged in user account, by using a scheduled task, to provide interaction with user in the SYSTEM context. .PARAMETER UserName - Logged in Username under which to run the process from. Default is: The active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. + Logged in Username under which to run the process from. Default is: The active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. .PARAMETER Path - Path to the file being executed. + Path to the file being executed. .PARAMETER Parameters - Arguments to be passed to the file being executed. + Arguments to be passed to the file being executed. .PARAMETER SecureParameters - Hides all parameters passed to the executable from the Toolkit log file. + Hides all parameters passed to the executable from the Toolkit log file. .PARAMETER RunLevel - Specifies the level of user rights that Task Scheduler uses to run the task. The acceptable values for this parameter are: - - HighestAvailable: Tasks run by using the highest available privileges (Admin privileges for Administrators). Default Value. - - LeastPrivilege: Tasks run by using the least-privileged user account (LUA) privileges. + Specifies the level of user rights that Task Scheduler uses to run the task. The acceptable values for this parameter are: + - HighestAvailable: Tasks run by using the highest available privileges (Admin privileges for Administrators). Default Value. + - LeastPrivilege: Tasks run by using the least-privileged user account (LUA) privileges. .PARAMETER Wait - Wait for the process, launched by the scheduled task, to complete execution before accepting more input. Default is $false. + Wait for the process, launched by the scheduled task, to complete execution before accepting more input. Default is $false. .PARAMETER PassThru - Returns the exit code from this function or the process launched by the scheduled task. + Returns the exit code from this function or the process launched by the scheduled task. .PARAMETER WorkingDirectory - Set working directory for the process. + Set working directory for the process. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is $true. + Continue if an error is encountered. Default is $true. .EXAMPLE - Execute-ProcessAsUser -UserName 'CONTOSO\User' -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait - Execute process under a user account by specifying a username under which to execute it. + Execute-ProcessAsUser -UserName 'CONTOSO\User' -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait + Execute process under a user account by specifying a username under which to execute it. .EXAMPLE - Execute-ProcessAsUser -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait - Execute process under a user account by using the default active logged in user that was detected when the toolkit was launched. + Execute-ProcessAsUser -Path "$PSHOME\powershell.exe" -Parameters "-Command & { & `"C:\Test\Script.ps1`"; Exit `$LastExitCode }" -Wait + Execute process under a user account by using the default active logged in user that was detected when the toolkit was launched. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$UserName = $RunAsActiveUser.NTAccount, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Path, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Parameters = '', - [Parameter(Mandatory=$false)] - [switch]$SecureParameters = $false, - [Parameter(Mandatory=$false)] - [ValidateSet('HighestAvailable','LeastPrivilege')] - [string]$RunLevel = 'HighestAvailable', - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$Wait = $false, - [Parameter(Mandatory=$false)] - [switch]$PassThru = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$WorkingDirectory, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Initialize exit code variable - [int32]$executeProcessAsUserExitCode = 0 - - ## Confirm that the username field is not empty - If (-not $UserName) { - [int32]$executeProcessAsUserExitCode = 60009 - Write-Log -Message "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." - } - Return - } - - ## Confirm if the toolkit is running with administrator privileges - If (($RunLevel -eq 'HighestAvailable') -and (-not $IsAdmin)) { - [int32]$executeProcessAsUserExitCode = 60003 - Write-Log -Message "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." - } - Return - } - - ## Check whether the specified Working Directory exists - If ($WorkingDirectory -and (-not (Test-Path -LiteralPath $WorkingDirectory -PathType 'Container'))) { - Write-Log -Message "The specified working directory does not exist or is not a directory. The scheduled task might not work as expected." -Severity 2 -Source ${CmdletName} - } - - ## Build the scheduled task XML name - [string]$schTaskName = "$appDeployToolkitName-ExecuteAsUser" - - ## Create the temporary App Deploy Toolkit files folder if it doesn't already exist - If (-not (Test-Path -LiteralPath $dirAppDeployTemp -PathType 'Container')) { - New-Item -Path $dirAppDeployTemp -ItemType 'Directory' -Force -ErrorAction 'Stop' - } - - ## If PowerShell.exe is being launched, then create a VBScript to launch PowerShell so that we can suppress the console window that flashes otherwise - If (($Path -eq 'PowerShell.exe') -or ((Split-Path -Path $Path -Leaf) -eq 'PowerShell.exe')) { - # Permit inclusion of double quotes in parameters - If ($($Parameters.Substring($Parameters.Length - 1)) -eq '"') { - [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$', '') + ' & chr(34)' } - Else { - [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$','') + '"' } - [string[]]$executeProcessAsUserScript = "strCommand = $executeProcessAsUserParametersVBS" - $executeProcessAsUserScript += 'set oWShell = CreateObject("WScript.Shell")' - $executeProcessAsUserScript += 'intReturn = oWShell.Run(strCommand, 0, true)' - $executeProcessAsUserScript += 'WScript.Quit intReturn' - $executeProcessAsUserScript | Out-File -FilePath "$dirAppDeployTemp\$($schTaskName).vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' - $Path = 'wscript.exe' - $Parameters = "`"$dirAppDeployTemp\$($schTaskName).vbs`"" - } - - ## Prepare working directory insert - [string]$WorkingDirectoryInsert = "" - If ($WorkingDirectory) { - $WorkingDirectoryInsert = "`n $WorkingDirectory" - } - ## Specify the scheduled task configuration in XML format - [string]$xmlSchTask = @" + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$UserName = $RunAsActiveUser.NTAccount, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Parameters = '', + [Parameter(Mandatory=$false)] + [switch]$SecureParameters = $false, + [Parameter(Mandatory=$false)] + [ValidateSet('HighestAvailable','LeastPrivilege')] + [string]$RunLevel = 'HighestAvailable', + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$Wait = $false, + [Parameter(Mandatory=$false)] + [switch]$PassThru = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$WorkingDirectory, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + [string]$executeAsUserTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'ExecuteAsUser' + } + Process { + ## Initialize exit code variable + [int32]$executeProcessAsUserExitCode = 0 + + ## Confirm that the username field is not empty + If (-not $UserName) { + [int32]$executeProcessAsUserExitCode = 60009 + Write-Log -Message "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "The function [${CmdletName}] has a -UserName parameter that has an empty default value because no logged in users were detected when the toolkit was launched." + } + Return + } + + ## Confirm if the toolkit is running with administrator privileges + If (($RunLevel -eq 'HighestAvailable') -and (-not $IsAdmin)) { + [int32]$executeProcessAsUserExitCode = 60003 + Write-Log -Message "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "The function [${CmdletName}] requires the toolkit to be running with Administrator privileges if the [-RunLevel] parameter is set to 'HighestAvailable'." + } + Return + } + + ## Check whether the specified Working Directory exists + If ($WorkingDirectory -and (-not (Test-Path -LiteralPath $WorkingDirectory -PathType 'Container'))) { + Write-Log -Message "The specified working directory does not exist or is not a directory. The scheduled task might not work as expected." -Severity 2 -Source ${CmdletName} + } + + ## Build the scheduled task XML name + [string]$schTaskName = "$appDeployToolkitName-ExecuteAsUser" + + ## Remove and recreate the temporary folder + If (Test-Path -LiteralPath $executeAsUserTempPath -PathType 'Container') { + Write-Log -Message "Previous [$executeAsUserTempPath] found. Attempting removal." -Source ${CmdletName} + Remove-Folder -Path $executeAsUserTempPath + } + Write-Log -Message "Creating [$executeAsUserTempPath]." -Source ${CmdletName} + Try { + $null = New-Item -Path $executeAsUserTempPath -ItemType 'Directory' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Unable to create [$executeAsUserTempPath]. Possible attempt to gain elevated rights." -Source ${CmdletName} -Severity 2 + } + + ## If PowerShell.exe is being launched, then create a VBScript to launch PowerShell so that we can suppress the console window that flashes otherwise + If (((Split-Path -Path $Path -Leaf) -like 'PowerShell*') -or ((Split-Path -Path $Path -Leaf) -like 'cmd*')) { + If ($SecureParameters) { + Write-Log -Message "Preparing a vbs script that will start [$Path] (Parameters Hidden) as the logged-on user [$userName] silently..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Preparing a vbs script that will start [$Path $Parameters] as the logged-on user [$userName] silently..." -Source ${CmdletName} + } + # Permit inclusion of double quotes in parameters + $QuotesIndex = $Parameters.Length - 1 + If ($QuotesIndex -lt 0) { + $QuotesIndex = 0 + } + + If ($($Parameters.Substring($QuotesIndex)) -eq '"') { + [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$', '') + ' & chr(34)' } + Else { + [string]$executeProcessAsUserParametersVBS = 'chr(34) & ' + "`"$($Path)`"" + ' & chr(34) & ' + '" ' + ($Parameters -replace "`r`n", ';' -replace "`n", ';' -replace '"', "`" & chr(34) & `"" -replace ' & chr\(34\) & "$','') + '"' } + [string[]]$executeProcessAsUserScript = "strCommand = $executeProcessAsUserParametersVBS" + $executeProcessAsUserScript += 'set oWShell = CreateObject("WScript.Shell")' + $executeProcessAsUserScript += 'intReturn = oWShell.Run(strCommand, 0, true)' + $executeProcessAsUserScript += 'WScript.Quit intReturn' + $executeProcessAsUserScript | Out-File -FilePath "$executeAsUserTempPath\$($schTaskName).vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' + $Path = "$envWinDir\System32\wscript.exe" + $Parameters = "`"$executeAsUserTempPath\$($schTaskName).vbs`"" + + try { + Set-ItemPermission -Path "$executeAsUserTempPath\$schTaskName.vbs" -User $UserName -Permission 'Read' + } + catch { + Write-Log -Message "Failed to set read permissions on path [$executeAsUserTempPath\$schTaskName.vbs]. The function might not be able to work correctly." -Source ${CmdletName} -Severity 2 + } + } + + ## Prepare working directory insert + [string]$WorkingDirectoryInsert = "" + If ($WorkingDirectory) { + $WorkingDirectoryInsert = "`n $WorkingDirectory" + } + ## Specify the scheduled task configuration in XML format + [string]$xmlSchTask = @" - StopExisting - false - false - true - false - false - - false - false - - true - true - false - false - false - PT72H - 7 + StopExisting + false + false + true + false + false + + false + false + + true + true + false + false + false + PT72H + 7 - - $Path - $Parameters$WorkingDirectoryInsert - + + $Path + $Parameters$WorkingDirectoryInsert + - - $UserName - InteractiveToken - $RunLevel - + + $UserName + InteractiveToken + $RunLevel + "@ - ## Export the XML to file - Try { - # Specify the filename to export the XML to - [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\$schTaskName.xml" - [string]$xmlSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' - } - Catch { - [int32]$executeProcessAsUserExitCode = 60007 - Write-Log -Message "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]: $($_.Exception.Message)" - } - Return - } - - ## Create Scheduled Task to run the process with a logged-on user account - If ($Parameters) { - If ($SecureParameters) { - Write-Log -Message "Create scheduled task to run the process [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} - } - Else { - Write-Log -Message "Create scheduled task to run the process [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} - } - } - Else { - Write-Log -Message "Create scheduled task to run the process [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} - } - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru - If ($schTaskResult.ExitCode -ne 0) { - [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode - Write-Log -Message "Failed to create the scheduled task by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to create the scheduled task by importing the scheduled task XML file [$xmlSchTaskFilePath]." - } - Return - } - - ## Trigger the Scheduled Task - If ($Parameters) { - If ($SecureParameters) { - Write-Log -Message "Trigger execution of scheduled task with command [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} - } - Else { - Write-Log -Message "Trigger execution of scheduled task with command [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} - } - } - Else { - Write-Log -Message "Trigger execution of scheduled task with command [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} - } - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/run /i /tn $schTaskName" -WindowStyle 'Hidden' -CreateNoWindow -Passthru - If ($schTaskResult.ExitCode -ne 0) { - [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode - Write-Log -Message "Failed to trigger scheduled task [$schTaskName]." -Severity 3 -Source ${CmdletName} - # Delete Scheduled Task - Write-Log -Message 'Delete the scheduled task which did not trigger.' -Source ${CmdletName} - Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -IgnoreExitCodes "*" - If (-not $ContinueOnError) { - Throw "Failed to trigger scheduled task [$schTaskName]." - } - Return - } - - ## Wait for the process launched by the scheduled task to complete execution - If ($Wait) { - Write-Log -Message "Waiting for the process launched by the scheduled task [$schTaskName] to complete execution (this may take some time)..." -Source ${CmdletName} - Start-Sleep -Seconds 1 - #If on Windows Vista or higer, Windows Task Scheduler 2.0 is supported. 'Schedule.Service' ComObject output is UI language independent - If (([version]$envOSVersion).Major -gt 5) { - Try { - [__comobject]$ScheduleService = New-Object -ComObject 'Schedule.Service' -ErrorAction Stop - $ScheduleService.Connect() - $RootFolder = $ScheduleService.GetFolder('\') - $Task = $RootFolder.GetTask("$schTaskName") - # Task State(Status) 4 = 'Running' - While ($Task.State -eq 4) { - Start-Sleep -Seconds 5 - } - # Get the exit code from the process launched by the scheduled task - [int32]$executeProcessAsUserExitCode = $Task.LastTaskResult - } - Catch { - Write-Log -Message "Failed to retrieve information from Task Scheduler. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - Finally { - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($ScheduleService) } Catch { } - } - } - #Windows Task Scheduler 1.0 - Else { - While ((($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Status' | Select-Object -First 1) -eq 'Running') { - Start-Sleep -Seconds 5 - } - # Get the exit code from the process launched by the scheduled task - [int32]$executeProcessAsUserExitCode = ($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Last Result' | Select-Object -First 1 - } - Write-Log -Message "Exit code from process launched by scheduled task [$executeProcessAsUserExitCode]." -Source ${CmdletName} - } - Else { - Start-Sleep -Seconds 1 - } - - ## Delete scheduled task - Try { - Write-Log -Message "Delete scheduled task [$schTaskName]." -Source ${CmdletName} - Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to delete scheduled task [$schTaskName]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - If ($PassThru) { Write-Output -InputObject $executeProcessAsUserExitCode } - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + ## Export the XML to file + Try { + # Specify the filename to export the XML to + [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\$schTaskName.xml" + [string]$xmlSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' + Set-ItemPermission -Path $xmlSchTaskFilePath -User $UserName -Permission 'Read' + } + Catch { + [int32]$executeProcessAsUserExitCode = 60007 + Write-Log -Message "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]: $($_.Exception.Message)" + } + Return + } + + ## Create Scheduled Task to run the process with a logged-on user account + If ($Parameters) { + If ($SecureParameters) { + Write-Log -Message "Creating scheduled task to run the process [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Creating scheduled task to run the process [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} + } + } + Else { + Write-Log -Message "Creating scheduled task to run the process [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} + } + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru -ExitOnProcessFailure $false + If ($schTaskResult.ExitCode -ne 0) { + [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode + Write-Log -Message "Failed to create the scheduled task by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to create the scheduled task by importing the scheduled task XML file [$xmlSchTaskFilePath]." + } + Return + } + + ## Trigger the Scheduled Task + If ($Parameters) { + If ($SecureParameters) { + Write-Log -Message "Trigger execution of scheduled task with command [$Path] (Parameters Hidden) as the logged-on user [$userName]..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Trigger execution of scheduled task with command [$Path $Parameters] as the logged-on user [$userName]..." -Source ${CmdletName} + } + } + Else { + Write-Log -Message "Trigger execution of scheduled task with command [$Path] as the logged-on user [$userName]..." -Source ${CmdletName} + } + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/run /i /tn $schTaskName" -WindowStyle 'Hidden' -CreateNoWindow -Passthru -ExitOnProcessFailure $false + If ($schTaskResult.ExitCode -ne 0) { + [int32]$executeProcessAsUserExitCode = $schTaskResult.ExitCode + Write-Log -Message "Failed to trigger scheduled task [$schTaskName]." -Severity 3 -Source ${CmdletName} + # Delete Scheduled Task + Write-Log -Message 'Delete the scheduled task which did not trigger.' -Source ${CmdletName} + Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -ExitOnProcessFailure $false + If (-not $ContinueOnError) { + Throw "Failed to trigger scheduled task [$schTaskName]." + } + Return + } + + ## Wait for the process launched by the scheduled task to complete execution + If ($Wait) { + Write-Log -Message "Waiting for the process launched by the scheduled task [$schTaskName] to complete execution (this may take some time)..." -Source ${CmdletName} + Start-Sleep -Seconds 1 + #If on Windows Vista or higer, Windows Task Scheduler 2.0 is supported. 'Schedule.Service' ComObject output is UI language independent + If (([version]$envOSVersion).Major -gt 5) { + Try { + [__comobject]$ScheduleService = New-Object -ComObject 'Schedule.Service' -ErrorAction Stop + $ScheduleService.Connect() + $RootFolder = $ScheduleService.GetFolder('\') + $Task = $RootFolder.GetTask("$schTaskName") + # Task State(Status) 4 = 'Running' + While ($Task.State -eq 4) { + Start-Sleep -Seconds 5 + } + # Get the exit code from the process launched by the scheduled task + [int32]$executeProcessAsUserExitCode = $Task.LastTaskResult + } + Catch { + Write-Log -Message "Failed to retrieve information from Task Scheduler. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + Finally { + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($ScheduleService) } Catch { } + } + } + #Windows Task Scheduler 1.0 + Else { + While ((($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Status' | Select-Object -First 1) -eq 'Running') { + Start-Sleep -Seconds 5 + } + # Get the exit code from the process launched by the scheduled task + [int32]$executeProcessAsUserExitCode = ($exeSchTasksResult = & $exeSchTasks /query /TN $schTaskName /V /FO CSV) | ConvertFrom-CSV | Select-Object -ExpandProperty 'Last Result' | Select-Object -First 1 + } + Write-Log -Message "Exit code from process launched by scheduled task [$executeProcessAsUserExitCode]." -Source ${CmdletName} + } + Else { + Start-Sleep -Seconds 1 + } + + ## Delete scheduled task + Try { + Write-Log -Message "Delete scheduled task [$schTaskName]." -Source ${CmdletName} + Execute-Process -Path $exeSchTasks -Parameters "/delete /tn $schTaskName /f" -WindowStyle 'Hidden' -CreateNoWindow -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to delete scheduled task [$schTaskName]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Remove the XML scheduled task file + If (Test-Path -LiteralPath $xmlSchTaskFilePath -PathType 'Leaf') { + Remove-File -Path $xmlSchTaskFilePath + } + + ## Remove the temporary folder + If (Test-Path -LiteralPath $executeAsUserTempPath -PathType 'Container') { + Remove-Folder -Path $executeAsUserTempPath + } + } + End { + If ($PassThru) { Write-Output -InputObject $executeProcessAsUserExitCode } + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5162,44 +5332,44 @@ Function Execute-ProcessAsUser { Function Update-Desktop { <# .SYNOPSIS - Refresh the Windows Explorer Shell, which causes the desktop icons and the environment variables to be reloaded. + Refresh the Windows Explorer Shell, which causes the desktop icons and the environment variables to be reloaded. .DESCRIPTION - Refresh the Windows Explorer Shell, which causes the desktop icons and the environment variables to be reloaded. + Refresh the Windows Explorer Shell, which causes the desktop icons and the environment variables to be reloaded. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Update-Desktop + Update-Desktop .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Refresh the Desktop and the Windows Explorer environment process block.' -Source ${CmdletName} - [PSADT.Explorer]::RefreshDesktopAndEnvironmentVariables() - } - Catch { - Write-Log -Message "Failed to refresh the Desktop and the Windows Explorer environment process block. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to refresh the Desktop and the Windows Explorer environment process block: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Refresh the Desktop and the Windows Explorer environment process block.' -Source ${CmdletName} + [PSADT.Explorer]::RefreshDesktopAndEnvironmentVariables() + } + Catch { + Write-Log -Message "Failed to refresh the Desktop and the Windows Explorer environment process block. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to refresh the Desktop and the Windows Explorer environment process block: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } Set-Alias -Name 'Refresh-Desktop' -Value 'Update-Desktop' -Scope 'Script' -Force -ErrorAction 'SilentlyContinue' #endregion @@ -5209,157 +5379,160 @@ Set-Alias -Name 'Refresh-Desktop' -Value 'Update-Desktop' -Scope 'Script' -Force Function Update-SessionEnvironmentVariables { <# .SYNOPSIS - Updates the environment variables for the current PowerShell session with any environment variable changes that may have occurred during script execution. + Updates the environment variables for the current PowerShell session with any environment variable changes that may have occurred during script execution. .DESCRIPTION - Environment variable changes that take place during script execution are not visible to the current PowerShell session. - Use this function to refresh the current PowerShell session with all environment variable settings. + Environment variable changes that take place during script execution are not visible to the current PowerShell session. + Use this function to refresh the current PowerShell session with all environment variable settings. .PARAMETER LoadLoggedOnUserEnvironmentVariables - If script is running in SYSTEM context, this option allows loading environment variables from the active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. + If script is running in SYSTEM context, this option allows loading environment variables from the active console user. If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Update-SessionEnvironmentVariables + Update-SessionEnvironmentVariables .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$LoadLoggedOnUserEnvironmentVariables = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - [scriptblock]$GetEnvironmentVar = { - Param ( - $Key, - $Scope - ) - [Environment]::GetEnvironmentVariable($Key, $Scope) - } - } - Process { - Try { - Write-Log -Message 'Refresh the environment variables for this PowerShell session.' -Source ${CmdletName} - - If ($LoadLoggedOnUserEnvironmentVariables -and $RunAsActiveUser) { - [string]$CurrentUserEnvironmentSID = $RunAsActiveUser.SID - } - Else { - [string]$CurrentUserEnvironmentSID = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value - } - [string]$MachineEnvironmentVars = 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' - [string]$UserEnvironmentVars = "Registry::HKEY_USERS\$CurrentUserEnvironmentSID\Environment" - - ## Update all session environment variables. Ordering is important here: $UserEnvironmentVars comes second so that we can override $MachineEnvironmentVars. - $MachineEnvironmentVars, $UserEnvironmentVars | Get-Item | Where-Object { $_ } | ForEach-Object { $envRegPath = $_.PSPath; $_ | Select-Object -ExpandProperty 'Property' | ForEach-Object { Set-Item -LiteralPath "env:$($_)" -Value (Get-ItemProperty -LiteralPath $envRegPath -Name $_).$_ } } - - ## Set PATH environment variable separately because it is a combination of the user and machine environment variables - [string[]]$PathFolders = 'Machine', 'User' | ForEach-Object { (& $GetEnvironmentVar -Key 'PATH' -Scope $_) } | Where-Object { $_ } | ForEach-Object { $_.Trim(';') } | ForEach-Object { $_.Split(';') } | ForEach-Object { $_.Trim() } | ForEach-Object { $_.Trim('"') } | Select-Object -Unique - $env:PATH = $PathFolders -join ';' - } - Catch { - Write-Log -Message "Failed to refresh the environment variables for this PowerShell session. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to refresh the environment variables for this PowerShell session: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$LoadLoggedOnUserEnvironmentVariables = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + [scriptblock]$GetEnvironmentVar = { + Param ( + $Key, + $Scope + ) + [Environment]::GetEnvironmentVariable($Key, $Scope) + } + } + Process { + Try { + Write-Log -Message 'Refresh the environment variables for this PowerShell session.' -Source ${CmdletName} + + If ($LoadLoggedOnUserEnvironmentVariables -and $RunAsActiveUser) { + [string]$CurrentUserEnvironmentSID = $RunAsActiveUser.SID + } + Else { + [string]$CurrentUserEnvironmentSID = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + } + [string]$MachineEnvironmentVars = 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' + [string]$UserEnvironmentVars = "Registry::HKEY_USERS\$CurrentUserEnvironmentSID\Environment" + + ## Update all session environment variables. Ordering is important here: $UserEnvironmentVars comes second so that we can override $MachineEnvironmentVars. + $MachineEnvironmentVars, $UserEnvironmentVars | Get-Item | Where-Object { $_ } | ForEach-Object { $envRegPath = $_.PSPath; $_ | Select-Object -ExpandProperty 'Property' | ForEach-Object { Set-Item -LiteralPath "env:$($_)" -Value (Get-ItemProperty -LiteralPath $envRegPath -Name $_).$_ } } + + ## Set PATH environment variable separately because it is a combination of the user and machine environment variables + [string[]]$PathFolders = 'Machine', 'User' | ForEach-Object { (& $GetEnvironmentVar -Key 'PATH' -Scope $_) } | Where-Object { $_ } | ForEach-Object { $_.Trim(';') } | ForEach-Object { $_.Split(';') } | ForEach-Object { $_.Trim() } | ForEach-Object { $_.Trim('"') } | Select-Object -Unique + $env:PATH = $PathFolders -join ';' + } + Catch { + Write-Log -Message "Failed to refresh the environment variables for this PowerShell session. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to refresh the environment variables for this PowerShell session: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } Set-Alias -Name 'Refresh-SessionEnvironmentVariables' -Value 'Update-SessionEnvironmentVariables' -Scope 'Script' -Force -ErrorAction 'SilentlyContinue' #endregion -#region Function Get-ScheduledTask -Function Get-ScheduledTask { +#region Function Get-SchedulerTask +Function Get-SchedulerTask { <# .SYNOPSIS - Retrieve all details for scheduled tasks on the local computer. + Retrieve all details for scheduled tasks on the local computer. .DESCRIPTION - Retrieve all details for scheduled tasks on the local computer using schtasks.exe. All property names have spaces and colons removed. + Retrieve all details for scheduled tasks on the local computer using schtasks.exe. All property names have spaces and colons removed. .PARAMETER TaskName - Specify the name of the scheduled task to retrieve details for. Uses regex match to find scheduled task. + Specify the name of the scheduled task to retrieve details for. Uses regex match to find scheduled task. .PARAMETER ContinueOnError - Continue if an error is encountered. Default: $true. + Continue if an error is encountered. Default: $true. .EXAMPLE - Get-ScheduledTask - To display a list of all scheduled task properties. + Get-SchedulerTask + To display a list of all scheduled task properties. .EXAMPLE - Get-ScheduledTask | Out-GridView - To display a grid view of all scheduled task properties. + Get-SchedulerTask | Out-GridView + To display a grid view of all scheduled task properties. .EXAMPLE - Get-ScheduledTask | Select-Object -Property TaskName - To display a list of all scheduled task names. + Get-SchedulerTask | Select-Object -Property TaskName + To display a list of all scheduled task names. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$TaskName, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - If (-not $exeSchTasks) { [string]$exeSchTasks = "$env:WINDIR\system32\schtasks.exe" } - [psobject[]]$ScheduledTasks = @() - } - Process { - Try { - Write-Log -Message 'Retrieve Scheduled Tasks...' -Source ${CmdletName} - [string[]]$exeSchtasksResults = & $exeSchTasks /Query /V /FO CSV - If ($global:LastExitCode -ne 0) { Throw "Failed to retrieve scheduled tasks using [$exeSchTasks]." } - [psobject[]]$SchtasksResults = $exeSchtasksResults | ConvertFrom-CSV -Header 'HostName', 'TaskName', 'Next Run Time', 'Status', 'Logon Mode', 'Last Run Time', 'Last Result', 'Author', 'Task To Run', 'Start In', 'Comment', 'Scheduled Task State', 'Idle Time', 'Power Management', 'Run As User', 'Delete Task If Not Rescheduled', 'Stop Task If Runs X Hours and X Mins', 'Schedule', 'Schedule Type', 'Start Time', 'Start Date', 'End Date', 'Days', 'Months', 'Repeat: Every', 'Repeat: Until: Time', 'Repeat: Until: Duration', 'Repeat: Stop If Still Running' -ErrorAction 'Stop' - - If ($SchtasksResults) { - ForEach ($SchtasksResult in $SchtasksResults) { - If ($SchtasksResult.TaskName -match $TaskName) { - $SchtasksResult | Get-Member -MemberType 'Properties' | - ForEach-Object -Begin { - [hashtable]$Task = @{} - } -Process { - ## Remove spaces and colons in property names. Do not set property value if line being processed is a column header (this will only work on English language machines). - ($Task.($($_.Name).Replace(' ','').Replace(':',''))) = If ($_.Name -ne $SchtasksResult.($_.Name)) { $SchtasksResult.($_.Name) } - } -End { - ## Only add task to the custom object if all property values are not empty - If (($Task.Values | Select-Object -Unique | Measure-Object).Count) { - $ScheduledTasks += New-Object -TypeName 'PSObject' -Property $Task - } - } - } - } - } - } - Catch { - Write-Log -Message "Failed to retrieve scheduled tasks. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to retrieve scheduled tasks: $($_.Exception.Message)" - } - } - } - End { - Write-Output -InputObject $ScheduledTasks - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$TaskName, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + [psobject[]]$ScheduledTasks = @() + } + Process { + Try { + Write-Log -Message 'Retrieve Scheduled Tasks...' -Source ${CmdletName} + [string[]]$exeSchtasksResults = & $exeSchTasks /Query /V /FO CSV + If ($global:LastExitCode -ne 0) { Throw "Failed to retrieve scheduled tasks using [$exeSchTasks]." } + [psobject[]]$SchtasksResults = $exeSchtasksResults | ConvertFrom-CSV -Header 'HostName', 'TaskName', 'Next Run Time', 'Status', 'Logon Mode', 'Last Run Time', 'Last Result', 'Author', 'Task To Run', 'Start In', 'Comment', 'Scheduled Task State', 'Idle Time', 'Power Management', 'Run As User', 'Delete Task If Not Rescheduled', 'Stop Task If Runs X Hours and X Mins', 'Schedule', 'Schedule Type', 'Start Time', 'Start Date', 'End Date', 'Days', 'Months', 'Repeat: Every', 'Repeat: Until: Time', 'Repeat: Until: Duration', 'Repeat: Stop If Still Running' -ErrorAction 'Stop' + + If ($SchtasksResults) { + ForEach ($SchtasksResult in $SchtasksResults) { + If ($SchtasksResult.TaskName -match $TaskName) { + $SchtasksResult | Get-Member -MemberType 'Properties' | + ForEach-Object -Begin { + [hashtable]$Task = @{} + } -Process { + ## Remove spaces and colons in property names. Do not set property value if line being processed is a column header (this will only work on English language machines). + ($Task.($($_.Name).Replace(' ','').Replace(':',''))) = If ($_.Name -ne $SchtasksResult.($_.Name)) { $SchtasksResult.($_.Name) } + } -End { + ## Only add task to the custom object if all property values are not empty + If (($Task.Values | Select-Object -Unique | Measure-Object).Count) { + $ScheduledTasks += New-Object -TypeName 'PSObject' -Property $Task + } + } + } + } + } + } + Catch { + Write-Log -Message "Failed to retrieve scheduled tasks. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to retrieve scheduled tasks: $($_.Exception.Message)" + } + } + } + End { + Write-Output -InputObject $ScheduledTasks + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } +} +# If Get-ScheduledTask doesn't exist, add alias Get-ScheduledTask +If (-not (Get-Command -Name "Get-ScheduledTask" -ErrorAction SilentlyContinue)) { + New-Alias -Name "Get-ScheduledTask" -Value "Get-SchedulerTask" } #endregion @@ -5368,153 +5541,176 @@ Function Get-ScheduledTask { Function Block-AppExecution { <# .SYNOPSIS - Block the execution of an application(s) + Block the execution of an application(s) .DESCRIPTION - This function is called when you pass the -BlockExecution parameter to the Stop-RunningApplications function. It does the following: - 1. Makes a copy of this script in a temporary directory on the local machine. - 2. Checks for an existing scheduled task from previous failed installation attempt where apps were blocked and if found, calls the Unblock-AppExecution function to restore the original IFEO registry keys. - This is to prevent the function from overriding the backup of the original IFEO options. - 3. Creates a scheduled task to restore the IFEO registry key values in case the script is terminated uncleanly by calling the local temporary copy of this script with the parameter -CleanupBlockedApps. - 4. Modifies the "Image File Execution Options" registry key for the specified process(s) to call this script with the parameter -ShowBlockedAppDialog. - 5. When the script is called with those parameters, it will display a custom message to the user to indicate that execution of the application has been blocked while the installation is in progress. - The text of this message can be customized in the XML configuration file. + This function is called when you pass the -BlockExecution parameter to the Stop-RunningApplications function. It does the following: + 1. Makes a copy of this script in a temporary directory on the local machine. + 2. Checks for an existing scheduled task from previous failed installation attempt where apps were blocked and if found, calls the Unblock-AppExecution function to restore the original IFEO registry keys. + This is to prevent the function from overriding the backup of the original IFEO options. + 3. Creates a scheduled task to restore the IFEO registry key values in case the script is terminated uncleanly by calling the local temporary copy of this script with the parameter -CleanupBlockedApps. + 4. Modifies the "Image File Execution Options" registry key for the specified process(s) to call this script with the parameter -ShowBlockedAppDialog. + 5. When the script is called with those parameters, it will display a custom message to the user to indicate that execution of the application has been blocked while the installation is in progress. + The text of this message can be customized in the XML configuration file. .PARAMETER ProcessName - Name of the process or processes separated by commas + Name of the process or processes separated by commas .EXAMPLE - Block-AppExecution -ProcessName ('winword','excel') + Block-AppExecution -ProcessName ('winword','excel') .NOTES - This is an internal script function and should typically not be called directly. - It is used when the -BlockExecution parameter is specified with the Show-InstallationWelcome function to block applications. + This is an internal script function and should typically not be called directly. + It is used when the -BlockExecution parameter is specified with the Show-InstallationWelcome function to block applications. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ## Specify process names separated by commas - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string[]]$ProcessName - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Remove illegal characters from the scheduled task arguments string - [char[]]$invalidScheduledTaskChars = '$', '!', '''', '"', '(', ')', ';', '\', '`', '*', '?', '{', '}', '[', ']', '<', '>', '|', '&', '%', '#', '~', '@', ' ' - [string]$SchInstallName = $installName - ForEach ($invalidChar in $invalidScheduledTaskChars) { [string]$SchInstallName = $SchInstallName -replace [regex]::Escape($invalidChar),'' } - [string]$schTaskUnblockAppsCommand += "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$dirAppDeployTemp\$scriptFileName`" -CleanupBlockedApps -ReferredInstallName `"$SchInstallName`" -ReferredInstallTitle `"$installTitle`" -ReferredLogName `"$logName`" -AsyncToolkitLaunch" - ## Specify the scheduled task configuration in XML format - [string]$xmlUnblockAppsSchTask = @" + [CmdletBinding()] + Param ( + ## Specify process names separated by commas + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string[]]$ProcessName + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Remove illegal characters from the scheduled task arguments string + [char[]]$invalidScheduledTaskChars = '$', '!', '''', '"', '(', ')', ';', '\', '`', '*', '?', '{', '}', '[', ']', '<', '>', '|', '&', '%', '#', '~', '@', ' ' + [string]$SchInstallName = $installName + ForEach ($invalidChar in $invalidScheduledTaskChars) { [string]$SchInstallName = $SchInstallName -replace [regex]::Escape($invalidChar),'' } + [string]$blockExecutionTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'BlockExecution' + [string]$schTaskUnblockAppsCommand += "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$blockExecutionTempPath\$scriptFileName`" -CleanupBlockedApps -ReferredInstallName `"$SchInstallName`" -ReferredInstallTitle `"$installTitle`" -ReferredLogName `"$logName`" -AsyncToolkitLaunch" + ## Specify the scheduled task configuration in XML format + [string]$xmlUnblockAppsSchTask = @" - - - - true - - - - - S-1-5-18 - - - - IgnoreNew - false - false - true - false - false - - false - false - - true - true - false - false - false - PT1H - 7 - - - - powershell.exe - $schTaskUnblockAppsCommand - - + + + + true + + + + + S-1-5-18 + + + + IgnoreNew + false + false + true + false + false + + false + false + + true + true + false + false + false + PT1H + 7 + + + + $PSHome\powershell.exe + $schTaskUnblockAppsCommand + + "@ - } - Process { - ## Bypass if in NonInteractive mode - If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Function [${CmdletName}] [Mode: $deployMode]." -Source ${CmdletName} - Return - } - - [string]$schTaskBlockedAppsName = $installName + '_BlockedApps' - - ## Delete this file if it exists as it can cause failures (it is a bug from an older version of the toolkit) - If (Test-Path -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -PathType 'Leaf' -ErrorAction 'SilentlyContinue') { - $null = Remove-Item -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -Force -ErrorAction 'SilentlyContinue' - } - ## Create Temporary directory (if required) and copy Toolkit so it can be called by scheduled task later if required - If (-not (Test-Path -LiteralPath $dirAppDeployTemp -PathType 'Container' -ErrorAction 'SilentlyContinue')) { - $null = New-Item -Path $dirAppDeployTemp -ItemType 'Directory' -ErrorAction 'SilentlyContinue' - } - - Copy-Item -Path "$scriptRoot\*.*" -Destination $dirAppDeployTemp -Exclude 'thumbs.db' -Force -Recurse -ErrorAction 'SilentlyContinue' - - ## Build the debugger block value script - [string]$debuggerBlockMessageCmd = "`"powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `" & chr(34) & `"$dirAppDeployTemp\$scriptFileName`" & chr(34) & `" -ShowBlockedAppDialog -AsyncToolkitLaunch -ReferredInstallTitle `" & chr(34) & `"$installTitle`" & chr(34)" - [string[]]$debuggerBlockScript = "strCommand = $debuggerBlockMessageCmd" - $debuggerBlockScript += 'set oWShell = CreateObject("WScript.Shell")' - $debuggerBlockScript += 'oWShell.Run strCommand, 0, false' - $debuggerBlockScript | Out-File -FilePath "$dirAppDeployTemp\AppDeployToolkit_BlockAppExecutionMessage.vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' - [string]$debuggerBlockValue = "wscript.exe `"$dirAppDeployTemp\AppDeployToolkit_BlockAppExecutionMessage.vbs`"" - - ## Create a scheduled task to run on startup to call this script and clean up blocked applications in case the installation is interrupted, e.g. user shuts down during installation" - Write-Log -Message 'Create scheduled task to cleanup blocked applications in case installation is interrupted.' -Source ${CmdletName} - If (Get-ScheduledTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { - Write-Log -Message "Scheduled task [$schTaskBlockedAppsName] already exists." -Source ${CmdletName} - } - Else { - ## Export the scheduled task XML to file - Try { - # Specify the filename to export the XML to - [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\SchTaskUnBlockApps.xml" - [string]$xmlUnblockAppsSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - Return - } - - ## Import the Scheduled Task XML file to create the Scheduled Task - [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskBlockedAppsName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru - If ($schTaskResult.ExitCode -ne 0) { - Write-Log -Message "Failed to create the scheduled task [$schTaskBlockedAppsName] by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} - Return - } - } - - [string[]]$blockProcessName = $processName - ## Append .exe to match registry keys - [string[]]$blockProcessName = $blockProcessName | ForEach-Object { $_ + '.exe' } -ErrorAction 'SilentlyContinue' - - ## Enumerate each process and set the debugger value to block application execution - ForEach ($blockProcess in $blockProcessName) { - Write-Log -Message "Set the Image File Execution Option registry key to block execution of [$blockProcess]." -Source ${CmdletName} - Set-RegistryKey -Key (Join-Path -Path $regKeyAppExecution -ChildPath $blockProcess) -Name 'Debugger' -Value $debuggerBlockValue -ContinueOnError $true - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + } + Process { + ## Bypass if no Admin rights + If ($configToolkitRequireAdmin -eq $false) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Require Admin: $configToolkitRequireAdmin]." -Source ${CmdletName} + Return + } + ## Bypass if in NonInteractive mode + If ($deployModeNonInteractive) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Mode: $deployMode]." -Source ${CmdletName} + Return + } + + [string]$schTaskBlockedAppsName = $installName + '_BlockedApps' + + ## Delete this file if it exists as it can cause failures (it is a bug from an older version of the toolkit) + If (Test-Path -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -PathType 'Leaf' -ErrorAction 'SilentlyContinue') { + $null = Remove-Item -LiteralPath "$configToolkitTempPath\PSAppDeployToolkit" -Force -ErrorAction 'SilentlyContinue' + } + + If (Test-Path -LiteralPath $blockExecutionTempPath -PathType 'Container') { + Remove-Folder -Path $blockExecutionTempPath + } + + Try { + $null = New-Item -Path $blockExecutionTempPath -ItemType 'Directory' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Unable to create [$blockExecutionTempPath]. Possible attempt to gain elevated rights." -Source ${CmdletName} + } + + Copy-Item -Path "$scriptRoot\*.*" -Destination $blockExecutionTempPath -Exclude 'thumbs.db' -Force -Recurse -ErrorAction 'SilentlyContinue' + + ## Build the debugger block value script + [string]$debuggerBlockMessageCmd = "`"$PSHome\powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `" & chr(34) & `"$blockExecutionTempPath\$scriptFileName`" & chr(34) & `" -ShowBlockedAppDialog -AsyncToolkitLaunch -ReferredInstallTitle `" & chr(34) & `"$installTitle`" & chr(34)" + [string[]]$debuggerBlockScript = "strCommand = $debuggerBlockMessageCmd" + $debuggerBlockScript += 'set oWShell = CreateObject("WScript.Shell")' + $debuggerBlockScript += 'oWShell.Run strCommand, 0, false' + $debuggerBlockScript | Out-File -FilePath "$blockExecutionTempPath\AppDeployToolkit_BlockAppExecutionMessage.vbs" -Force -Encoding 'default' -ErrorAction 'SilentlyContinue' + [string]$debuggerBlockValue = "$envWinDir\System32\wscript.exe `"$blockExecutionTempPath\AppDeployToolkit_BlockAppExecutionMessage.vbs`"" + + ## Set contents to be readable for all users (BUILTIN\USERS) + try { + $Users = ConvertTo-NTAccountOrSID -SID "S-1-5-32-545" + Set-ItemPermission -Path $blockExecutionTempPath -User $Users -Permission 'Read' -Inheritance "ObjectInherit","ContainerInherit" + } + catch { + Write-Log -Message "Failed to set read permissions on path [$blockExecutionTempPath]. The function might not be able to work correctly." -Source ${CmdletName} -Severity 2 + } + + ## Create a scheduled task to run on startup to call this script and clean up blocked applications in case the installation is interrupted, e.g. user shuts down during installation" + Write-Log -Message 'Create scheduled task to cleanup blocked applications in case installation is interrupted.' -Source ${CmdletName} + If (Get-SchedulerTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { + Write-Log -Message "Scheduled task [$schTaskBlockedAppsName] already exists." -Source ${CmdletName} + } + Else { + ## Export the scheduled task XML to file + Try { + ## Specify the filename to export the XML to + ## XML does not need to be user readable to stays in protected TEMP folder + [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\SchTaskUnBlockApps.xml" + [string]$xmlUnblockAppsSchTask | Out-File -FilePath $xmlSchTaskFilePath -Force -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to export the scheduled task XML file [$xmlSchTaskFilePath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Return + } + + ## Import the Scheduled Task XML file to create the Scheduled Task + [psobject]$schTaskResult = Execute-Process -Path $exeSchTasks -Parameters "/create /f /tn $schTaskBlockedAppsName /xml `"$xmlSchTaskFilePath`"" -WindowStyle 'Hidden' -CreateNoWindow -PassThru -ExitOnProcessFailure $false + If ($schTaskResult.ExitCode -ne 0) { + Write-Log -Message "Failed to create the scheduled task [$schTaskBlockedAppsName] by importing the scheduled task XML file [$xmlSchTaskFilePath]." -Severity 3 -Source ${CmdletName} + Return + } + } + + [string[]]$blockProcessName = $processName + ## Append .exe to match registry keys + [string[]]$blockProcessName = $blockProcessName | ForEach-Object { $_ + '.exe' } -ErrorAction 'SilentlyContinue' + + ## Enumerate each process and set the debugger value to block application execution + ForEach ($blockProcess in $blockProcessName) { + Write-Log -Message "Set the Image File Execution Option registry key to block execution of [$blockProcess]." -Source ${CmdletName} + Set-RegistryKey -Key (Join-Path -Path $regKeyAppExecution -ChildPath $blockProcess) -Name 'Debugger' -Value $debuggerBlockValue -ContinueOnError $true + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5523,62 +5719,79 @@ Function Block-AppExecution { Function Unblock-AppExecution { <# .SYNOPSIS - Unblocks the execution of applications performed by the Block-AppExecution function + Unblocks the execution of applications performed by the Block-AppExecution function .DESCRIPTION - This function is called by the Exit-Script function or when the script itself is called with the parameters -CleanupBlockedApps + This function is called by the Exit-Script function or when the script itself is called with the parameters -CleanupBlockedApps .EXAMPLE - Unblock-AppExecution + Unblock-AppExecution .NOTES - This is an internal script function and should typically not be called directly. - It is used when the -BlockExecution parameter is specified with the Show-InstallationWelcome function to undo the actions performed by Block-AppExecution. + This is an internal script function and should typically not be called directly. + It is used when the -BlockExecution parameter is specified with the Show-InstallationWelcome function to undo the actions performed by Block-AppExecution. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Bypass if in NonInteractive mode - If ($deployModeNonInteractive) { - Write-Log -Message "Bypassing Function [${CmdletName}] [Mode: $deployMode]." -Source ${CmdletName} - Return - } - - ## Remove Debugger values to unblock processes - [psobject[]]$unblockProcesses = $null - [psobject[]]$unblockProcesses += (Get-ChildItem -LiteralPath $regKeyAppExecution -Recurse -ErrorAction 'SilentlyContinue' | ForEach-Object { Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'SilentlyContinue'}) - ForEach ($unblockProcess in ($unblockProcesses | Where-Object { $_.Debugger -like '*AppDeployToolkit_BlockAppExecutionMessage*' })) { - Write-Log -Message "Remove the Image File Execution Options registry key to unblock execution of [$($unblockProcess.PSChildName)]." -Source ${CmdletName} - $unblockProcess | Remove-ItemProperty -Name 'Debugger' -ErrorAction 'SilentlyContinue' - } - - ## If block execution variable is $true, set it to $false - If ($BlockExecution) { - # Make this variable globally available so we can check whether we need to call Unblock-AppExecution - Set-Variable -Name 'BlockExecution' -Value $false -Scope 'Script' - } - - ## Remove the scheduled task if it exists - [string]$schTaskBlockedAppsName = $installName + '_BlockedApps' - Try { - If (Get-ScheduledTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { - Write-Log -Message "Delete Scheduled Task [$schTaskBlockedAppsName]." -Source ${CmdletName} - Execute-Process -Path $exeSchTasks -Parameters "/Delete /TN $schTaskBlockedAppsName /F" - } - } - Catch { - Write-Log -Message "Error retrieving/deleting Scheduled Task.`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Bypass if no Admin rights + If ($configToolkitRequireAdmin -eq $false) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Require Admin: $configToolkitRequireAdmin]." -Source ${CmdletName} + Return + } + ## Bypass if in NonInteractive mode + If ($deployModeNonInteractive) { + Write-Log -Message "Bypassing Function [${CmdletName}], because [Mode: $deployMode]." -Source ${CmdletName} + Return + } + + ## Remove Debugger values to unblock processes + [psobject[]]$unblockProcesses = $null + [psobject[]]$unblockProcesses += (Get-ChildItem -LiteralPath $regKeyAppExecution -Recurse -ErrorAction 'SilentlyContinue' | ForEach-Object { Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction 'SilentlyContinue'}) + ForEach ($unblockProcess in ($unblockProcesses | Where-Object { $_.Debugger -like '*AppDeployToolkit_BlockAppExecutionMessage*' })) { + Write-Log -Message "Remove the Image File Execution Options registry key to unblock execution of [$($unblockProcess.PSChildName)]." -Source ${CmdletName} + $unblockProcess | Remove-ItemProperty -Name 'Debugger' -ErrorAction 'SilentlyContinue' + } + + ## If block execution variable is $true, set it to $false + If ($BlockExecution) { + # Make this variable globally available so we can check whether we need to call Unblock-AppExecution + Set-Variable -Name 'BlockExecution' -Value $false -Scope 'Script' + } + + ## Remove the scheduled task if it exists + [string]$schTaskBlockedAppsName = $installName + '_BlockedApps' + Try { + If (Get-SchedulerTask -ContinueOnError $true | Select-Object -Property 'TaskName' | Where-Object { $_.TaskName -eq "\$schTaskBlockedAppsName" }) { + Write-Log -Message "Delete Scheduled Task [$schTaskBlockedAppsName]." -Source ${CmdletName} + Execute-Process -Path $exeSchTasks -Parameters "/Delete /TN $schTaskBlockedAppsName /F" + } + } + Catch { + Write-Log -Message "Error retrieving/deleting Scheduled Task.`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Remove BlockAppExecution Schedule Task XML file + [string]$xmlSchTaskFilePath = "$dirAppDeployTemp\SchTaskUnBlockApps.xml" + If (Test-Path -LiteralPath $xmlSchTaskFilePath) { + Remove-Item -Path $xmlSchTaskFilePath + } + + ## Remove BlockAppExection Temporary directory + [string]$blockExecutionTempPath = Join-Path -Path $dirAppDeployTemp -ChildPath 'BlockExecution' + If (Test-Path -LiteralPath $blockExecutionTempPath -PathType 'Container') { + Remove-Folder -Path $blockExecutionTempPath + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5587,32 +5800,32 @@ Function Unblock-AppExecution { Function Get-DeferHistory { <# .SYNOPSIS - Get the history of deferrals from the registry for the current application, if it exists. + Get the history of deferrals from the registry for the current application, if it exists. .DESCRIPTION - Get the history of deferrals from the registry for the current application, if it exists. + Get the history of deferrals from the registry for the current application, if it exists. .EXAMPLE - Get-DeferHistory + Get-DeferHistory .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Write-Log -Message 'Get deferral history...' -Source ${CmdletName} - Get-RegistryKey -Key $regKeyDeferHistory -ContinueOnError $true - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Write-Log -Message 'Get deferral history...' -Source ${CmdletName} + Get-RegistryKey -Key $regKeyDeferHistory -ContinueOnError $true + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5621,42 +5834,42 @@ Function Get-DeferHistory { Function Set-DeferHistory { <# .SYNOPSIS - Set the history of deferrals in the registry for the current application. + Set the history of deferrals in the registry for the current application. .DESCRIPTION - Set the history of deferrals in the registry for the current application. + Set the history of deferrals in the registry for the current application. .EXAMPLE - Set-DeferHistory + Set-DeferHistory .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [string]$deferTimesRemaining, - [Parameter(Mandatory=$false)] - [string]$deferDeadline - ) - - 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 ($deferTimesRemaining -and ($deferTimesRemaining -ge 0)) { - Write-Log -Message "Set deferral history: [DeferTimesRemaining = $deferTimesRemaining]." -Source ${CmdletName} - Set-RegistryKey -Key $regKeyDeferHistory -Name 'DeferTimesRemaining' -Value $deferTimesRemaining -ContinueOnError $true - } - If ($deferDeadline) { - Write-Log -Message "Set deferral history: [DeferDeadline = $deferDeadline]." -Source ${CmdletName} - Set-RegistryKey -Key $regKeyDeferHistory -Name 'DeferDeadline' -Value $deferDeadline -ContinueOnError $true - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$deferTimesRemaining, + [Parameter(Mandatory=$false)] + [string]$deferDeadline + ) + + 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 ($deferTimesRemaining -and ($deferTimesRemaining -ge 0)) { + Write-Log -Message "Set deferral history: [DeferTimesRemaining = $deferTimesRemaining]." -Source ${CmdletName} + Set-RegistryKey -Key $regKeyDeferHistory -Name 'DeferTimesRemaining' -Value $deferTimesRemaining -ContinueOnError $true + } + If ($deferDeadline) { + Write-Log -Message "Set deferral history: [DeferDeadline = $deferDeadline]." -Source ${CmdletName} + Set-RegistryKey -Key $regKeyDeferHistory -Name 'DeferDeadline' -Value $deferDeadline -ContinueOnError $true + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5665,60 +5878,60 @@ Function Set-DeferHistory { Function Get-UniversalDate { <# .SYNOPSIS - Returns the date/time for the local culture in a universal sortable date time pattern. + Returns the date/time for the local culture in a universal sortable date time pattern. .DESCRIPTION - Converts the current datetime or a datetime string for the current culture into a universal sortable date time pattern, e.g. 2013-08-22 11:51:52Z + Converts the current datetime or a datetime string for the current culture into a universal sortable date time pattern, e.g. 2013-08-22 11:51:52Z .PARAMETER DateTime - Specify the DateTime in the current culture. + Specify the DateTime in the current culture. .PARAMETER ContinueOnError - Continue if an error is encountered. Default: $false. + Continue if an error is encountered. Default: $false. .EXAMPLE - Get-UniversalDate - Returns the current date in a universal sortable date time pattern. + Get-UniversalDate + Returns the current date in a universal sortable date time pattern. .EXAMPLE - Get-UniversalDate -DateTime '25/08/2013' - Returns the date for the current culture in a universal sortable date time pattern. + Get-UniversalDate -DateTime '25/08/2013' + Returns the date for the current culture in a universal sortable date time pattern. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - # Get the current date - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$DateTime = ((Get-Date -Format ($culture).DateTimeFormat.UniversalDateTimePattern).ToString()), - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## If a universal sortable date time pattern was provided, remove the Z, otherwise it could get converted to a different time zone. - If ($DateTime -match 'Z$') { $DateTime = $DateTime -replace 'Z$', '' } - [datetime]$DateTime = [datetime]::Parse($DateTime, $culture) - - ## Convert the date to a universal sortable date time pattern based on the current culture - Write-Log -Message "Convert the date [$DateTime] to a universal sortable date time pattern based on the current culture [$($culture.Name)]." -Source ${CmdletName} - [string]$universalDateTime = (Get-Date -Date $DateTime -Format ($culture).DateTimeFormat.UniversalSortableDateTimePattern -ErrorAction 'Stop').ToString() - Write-Output -InputObject $universalDateTime - } - Catch { - Write-Log -Message "The specified date/time [$DateTime] is not in a format recognized by the current culture [$($culture.Name)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "The specified date/time [$DateTime] is not in a format recognized by the current culture: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + # Get the current date + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$DateTime = ((Get-Date -Format ($culture).DateTimeFormat.UniversalDateTimePattern).ToString()), + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## If a universal sortable date time pattern was provided, remove the Z, otherwise it could get converted to a different time zone. + If ($DateTime -match 'Z$') { $DateTime = $DateTime -replace 'Z$', '' } + [datetime]$DateTime = [datetime]::Parse($DateTime, $culture) + + ## Convert the date to a universal sortable date time pattern based on the current culture + Write-Log -Message "Convert the date [$DateTime] to a universal sortable date time pattern based on the current culture [$($culture.Name)]." -Source ${CmdletName} + [string]$universalDateTime = (Get-Date -Date $DateTime -Format ($culture).DateTimeFormat.UniversalSortableDateTimePattern -ErrorAction 'Stop').ToString() + Write-Output -InputObject $universalDateTime + } + Catch { + Write-Log -Message "The specified date/time [$DateTime] is not in a format recognized by the current culture [$($culture.Name)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "The specified date/time [$DateTime] is not in a format recognized by the current culture: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5727,80 +5940,81 @@ Function Get-UniversalDate { Function Get-RunningProcesses { <# .SYNOPSIS - Gets the processes that are running from a custom list of process objects and also adds a property called ProcessDescription. + Gets the processes that are running from a custom list of process objects and also adds a property called ProcessDescription. .DESCRIPTION - Gets the processes that are running from a custom list of process objects and also adds a property called ProcessDescription. + Gets the processes that are running from a custom list of process objects and also adds a property called ProcessDescription. .PARAMETER ProcessObjects - Custom object containing the process objects to search for. + Custom object containing the process objects to search for. If not supplied, the function just returns $null .EXAMPLE - Get-RunningProcesses + Get-RunningProcesses -ProcessObjects $ProcessObjects .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false,Position=0)] - [psobject[]]$ProcessObjects, - [Parameter(Mandatory=$false,Position=1)] - [switch]$DisableLogging - ) - - 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 ($processObjects) { - [string]$runningAppsCheck = ($processObjects | ForEach-Object { $_.ProcessName }) -join ',' - If (-not($DisableLogging)) { - Write-Log -Message "Check for running application(s) [$runningAppsCheck]..." -Source ${CmdletName} - } - ## Create an array of process names to search for - [string[]]$processNames = $processObjects | ForEach-Object { $_.ProcessName } - - ## Get all running processes and escape special characters. Match against the process names to search for to find running processes. - [Diagnostics.Process[]]$runningProcesses = Get-Process | Where-Object { $processNames -contains $_.ProcessName } | Sort-Object Name -Unique - - If ($runningProcesses) { - [string]$runningProcessList = ($runningProcesses | ForEach-Object { $_.ProcessName } | Select-Object -Unique) -join ',' - If (-not($DisableLogging)) { - Write-Log -Message "The following processes are running: [$runningProcessList]." -Source ${CmdletName} - Write-Log -Message 'Resolve process descriptions...' -Source ${CmdletName} - } - ## Resolve the running process names to descriptions - ForEach ($runningProcess in $runningProcesses) { - ForEach ($processObject in $processObjects) { - If ($runningProcess.ProcessName -eq $processObject.ProcessName) { - If ($processObject.ProcessDescription) { - # The description of the process provided as a Parameter to the function, e.g. -ProcessName "winword=Microsoft Office Word". - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $processObject.ProcessDescription -Force -PassThru -ErrorAction 'SilentlyContinue' - } - ElseIf ($runningProcess.Description) { - # If the process already has a description field specified, then use it - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $runningProcess.Description -Force -PassThru -ErrorAction 'SilentlyContinue' - } - Else { - # Fall back on the process name if no description is provided by the process or as a parameter to the function - $runningProcess | Add-Member -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $runningProcess.ProcessName -Force -PassThru -ErrorAction 'SilentlyContinue' - } - } - } - } - } - Else { - If (-not($DisableLogging)) { - Write-Log -Message 'Application(s) are not running.' -Source ${CmdletName} - } - } - Write-Output -InputObject $runningProcesses - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false,Position=0)] + [psobject[]]$ProcessObjects, + [Parameter(Mandatory=$false,Position=1)] + [switch]$DisableLogging + ) + + 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 ($processObjects -and $processObjects[0].ProcessName) { + [string]$runningAppsCheck = $processObjects.ProcessName -join ',' + If (-not($DisableLogging)) { + Write-Log -Message "Check for running applications: [$runningAppsCheck]" -Source ${CmdletName} + } + ## Prepare a filter for Where-Object + [scriptblock]$whereObjectFilter = { + ForEach ($processObject in $processObjects) { + If ($_.ProcessName -ieq $processObject.ProcessName) { + If ($processObject.ProcessDescription) { + # The description of the process provided as a Parameter to the function, e.g. -ProcessName "winword=Microsoft Office Word". + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $processObject.ProcessDescription -Force -PassThru -ErrorAction 'SilentlyContinue' + } + ElseIf ($_.Description) { + # If the process already has a description field specified, then use it + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $_.Description -Force -PassThru -ErrorAction 'SilentlyContinue' + } + Else { + # Fall back on the process name if no description is provided by the process or as a parameter to the function + Add-Member -InputObject $_ -MemberType 'NoteProperty' -Name 'ProcessDescription' -Value $_.ProcessName -Force -PassThru -ErrorAction 'SilentlyContinue' + } + Write-Output $true + return; + } + } + + Write-Output $false + return; + } + ## Get all running processes and escape special characters. Match against the process names to search for to find running processes. + [Diagnostics.Process[]]$runningProcesses = Get-Process | Where-Object -FilterScript $whereObjectFilter | Sort-Object ProcessName + + If (-not($DisableLogging)) { + If ($runningProcesses) { + [string]$runningProcessList = ($runningProcesses.ProcessName | Select-Object -Unique) -join ',' + Write-Log -Message "The following processes are running: [$runningProcessList]." -Source ${CmdletName} + } + Else { + Write-Log -Message 'Specified applications are not running.' -Source ${CmdletName} + } + } + Write-Output -InputObject $runningProcesses + } Else { + Write-Output -InputObject $null + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -5809,482 +6023,482 @@ Function Get-RunningProcesses { Function Show-InstallationWelcome { <# .SYNOPSIS - Show a welcome dialog prompting the user with information about the installation and actions to be performed before the installation can begin. + Show a welcome dialog prompting the user with information about the installation and actions to be performed before the installation can begin. .DESCRIPTION - The following prompts can be included in the welcome dialog: - a) Close the specified running applications, or optionally close the applications without showing a prompt (using the -Silent switch). - b) Defer the installation a certain number of times, for a certain number of days or until a deadline is reached. - c) Countdown until applications are automatically closed. - d) Prevent users from launching the specified applications while the installation is in progress. - Notes: - The process descriptions are retrieved from WMI, with a fall back on the process name if no description is available. Alternatively, you can specify the description yourself with a '=' symbol - see examples. - The dialog box will timeout after the timeout specified in the XML configuration file (default 1 hour and 55 minutes) to prevent SCCM installations from timing out and returning a failure code to SCCM. When the dialog times out, the script will exit and return a 1618 code (SCCM fast retry code). + The following prompts can be included in the welcome dialog: + a) Close the specified running applications, or optionally close the applications without showing a prompt (using the -Silent switch). + b) Defer the installation a certain number of times, for a certain number of days or until a deadline is reached. + c) Countdown until applications are automatically closed. + d) Prevent users from launching the specified applications while the installation is in progress. + Notes: + The process descriptions are retrieved from WMI, with a fall back on the process name if no description is available. Alternatively, you can specify the description yourself with a '=' symbol - see examples. + The dialog box will timeout after the timeout specified in the XML configuration file (default 1 hour and 55 minutes) to prevent SCCM installations from timing out and returning a failure code to SCCM. When the dialog times out, the script will exit and return a 1618 code (SCCM fast retry code). .PARAMETER CloseApps - Name of the process to stop (do not include the .exe). Specify multiple processes separated by a comma. Specify custom descriptions like this: "winword=Microsoft Office Word,excel=Microsoft Office Excel" + Name of the process to stop (do not include the .exe). Specify multiple processes separated by a comma. Specify custom descriptions like this: "winword=Microsoft Office Word,excel=Microsoft Office Excel" .PARAMETER Silent - Stop processes without prompting the user. + Stop processes without prompting the user. .PARAMETER CloseAppsCountdown - Option to provide a countdown in seconds until the specified applications are automatically closed. This only takes effect if deferral is not allowed or has expired. + Option to provide a countdown in seconds until the specified applications are automatically closed. This only takes effect if deferral is not allowed or has expired. .PARAMETER ForceCloseAppsCountdown - Option to provide a countdown in seconds until the specified applications are automatically closed regardless of whether deferral is allowed. + Option to provide a countdown in seconds until the specified applications are automatically closed regardless of whether deferral is allowed. .PARAMETER PromptToSave - Specify whether to prompt to save working documents when the user chooses to close applications by selecting the "Close Programs" button. Option does not work in SYSTEM context unless toolkit launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. + Specify whether to prompt to save working documents when the user chooses to close applications by selecting the "Close Programs" button. Option does not work in SYSTEM context unless toolkit launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. .PARAMETER PersistPrompt - Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. The user will have no option but to respond to the prompt. This only takes effect if deferral is not allowed or has expired. + Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. The user will have no option but to respond to the prompt. This only takes effect if deferral is not allowed or has expired. .PARAMETER BlockExecution - Option to prevent the user from launching the process/application during the installation. + Option to prevent the user from launching the process/application during the installation. .PARAMETER AllowDefer - Enables an optional defer button to allow the user to defer the installation. + Enables an optional defer button to allow the user to defer the installation. .PARAMETER AllowDeferCloseApps - Enables an optional defer button to allow the user to defer the installation only if there are running applications that need to be closed. + Enables an optional defer button to allow the user to defer the installation only if there are running applications that need to be closed. .PARAMETER DeferTimes - Specify the number of times the installation can be deferred. + Specify the number of times the installation can be deferred. .PARAMETER DeferDays - Specify the number of days since first run that the installation can be deferred. This is converted to a deadline. + Specify the number of days since first run that the installation can be deferred. This is converted to a deadline. .PARAMETER DeferDeadline - Specify the deadline date until which the installation can be deferred. - Specify the date in the local culture if the script is intended for that same culture. - If the script is intended to run on EN-US machines, specify the date in the format: "08/25/2013" or "08-25-2013" or "08-25-2013 18:00:00" - If the script is intended for multiple cultures, specify the date in the universal sortable date/time format: "2013-08-22 11:51:52Z" - The deadline date will be displayed to the user in the format of their culture. + Specify the deadline date until which the installation can be deferred. + Specify the date in the local culture if the script is intended for that same culture. + If the script is intended to run on EN-US machines, specify the date in the format: "08/25/2013" or "08-25-2013" or "08-25-2013 18:00:00" + If the script is intended for multiple cultures, specify the date in the universal sortable date/time format: "2013-08-22 11:51:52Z" + The deadline date will be displayed to the user in the format of their culture. .PARAMETER CheckDiskSpace - Specify whether to check if there is enough disk space for the installation to proceed. - If this parameter is specified without the RequiredDiskSpace parameter, the required disk space is calculated automatically based on the size of the script source and associated files. + Specify whether to check if there is enough disk space for the installation to proceed. + If this parameter is specified without the RequiredDiskSpace parameter, the required disk space is calculated automatically based on the size of the script source and associated files. .PARAMETER RequiredDiskSpace - Specify required disk space in MB, used in combination with CheckDiskSpace. + Specify required disk space in MB, used in combination with CheckDiskSpace. .PARAMETER MinimizeWindows - Specifies whether to minimize other windows when displaying prompt. Default: $true. + Specifies whether to minimize other windows when displaying prompt. Default: $true. .PARAMETER TopMost - Specifies whether the windows is the topmost window. Default: $true. + Specifies whether the windows is the topmost window. Default: $true. .PARAMETER ForceCountdown - Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled. + Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled. .PARAMETER CustomText - Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. + Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. .EXAMPLE - Show-InstallationWelcome -CloseApps 'iexplore,winword,excel' - Prompt the user to close Internet Explorer, Word and Excel. + Show-InstallationWelcome -CloseApps 'iexplore,winword,excel' + Prompt the user to close Internet Explorer, Word and Excel. .EXAMPLE - Show-InstallationWelcome -CloseApps 'winword,excel' -Silent - Close Word and Excel without prompting the user. + Show-InstallationWelcome -CloseApps 'winword,excel' -Silent + Close Word and Excel without prompting the user. .EXAMPLE - Show-InstallationWelcome -CloseApps 'winword,excel' -BlockExecution - Close Word and Excel and prevent the user from launching the applications while the installation is in progress. + Show-InstallationWelcome -CloseApps 'winword,excel' -BlockExecution + Close Word and Excel and prevent the user from launching the applications while the installation is in progress. .EXAMPLE - Show-InstallationWelcome -CloseApps 'winword=Microsoft Office Word,excel=Microsoft Office Excel' -CloseAppsCountdown 600 - Prompt the user to close Word and Excel, with customized descriptions for the applications and automatically close the applications after 10 minutes. + Show-InstallationWelcome -CloseApps 'winword=Microsoft Office Word,excel=Microsoft Office Excel' -CloseAppsCountdown 600 + Prompt the user to close Word and Excel, with customized descriptions for the applications and automatically close the applications after 10 minutes. .EXAMPLE - Show-InstallationWelcome -CloseApps 'winword,msaccess,excel' -PersistPrompt - Prompt the user to close Word, MSAccess and Excel. - By using the PersistPrompt switch, the dialog will return to the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml, so the user cannot ignore it by dragging it aside. + Show-InstallationWelcome -CloseApps 'winword,msaccess,excel' -PersistPrompt + Prompt the user to close Word, MSAccess and Excel. + By using the PersistPrompt switch, the dialog will return to the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml, so the user cannot ignore it by dragging it aside. .EXAMPLE - Show-InstallationWelcome -AllowDefer -DeferDeadline '25/08/2013' - Allow the user to defer the installation until the deadline is reached. + Show-InstallationWelcome -AllowDefer -DeferDeadline '25/08/2013' + Allow the user to defer the installation until the deadline is reached. .EXAMPLE - Show-InstallationWelcome -CloseApps 'winword,excel' -BlockExecution -AllowDefer -DeferTimes 10 -DeferDeadline '25/08/2013' -CloseAppsCountdown 600 - Close Word and Excel and prevent the user from launching the applications while the installation is in progress. - Allow the user to defer the installation a maximum of 10 times or until the deadline is reached, whichever happens first. - When deferral expires, prompt the user to close the applications and automatically close them after 10 minutes. + Show-InstallationWelcome -CloseApps 'winword,excel' -BlockExecution -AllowDefer -DeferTimes 10 -DeferDeadline '25/08/2013' -CloseAppsCountdown 600 + Close Word and Excel and prevent the user from launching the applications while the installation is in progress. + Allow the user to defer the installation a maximum of 10 times or until the deadline is reached, whichever happens first. + When deferral expires, prompt the user to close the applications and automatically close them after 10 minutes. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding(DefaultParametersetName='None')] - - Param ( - ## Specify process names separated by commas. Optionally specify a process description with an equals symbol, e.g. "winword=Microsoft Office Word" - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$CloseApps, - ## Specify whether to prompt user or force close the applications - [Parameter(Mandatory=$false)] - [switch]$Silent = $false, - ## Specify a countdown to display before automatically closing applications where deferral is not allowed or has expired - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$CloseAppsCountdown = 0, - ## Specify a countdown to display before automatically closing applications whether or not deferral is allowed - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$ForceCloseAppsCountdown = 0, - ## Specify whether to prompt to save working documents when the user chooses to close applications by selecting the "Close Programs" button - [Parameter(Mandatory=$false)] - [switch]$PromptToSave = $false, - ## Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. - [Parameter(Mandatory=$false)] - [switch]$PersistPrompt = $false, - ## Specify whether to block execution of the processes during installation - [Parameter(Mandatory=$false)] - [switch]$BlockExecution = $false, - ## Specify whether to enable the optional defer button on the dialog box - [Parameter(Mandatory=$false)] - [switch]$AllowDefer = $false, - ## Specify whether to enable the optional defer button on the dialog box only if an app needs to be closed - [Parameter(Mandatory=$false)] - [switch]$AllowDeferCloseApps = $false, - ## Specify the number of times the deferral is allowed - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$DeferTimes = 0, - ## Specify the number of days since first run that the deferral is allowed - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$DeferDays = 0, - ## Specify the deadline (in format dd/mm/yyyy) for which deferral will expire as an option - [Parameter(Mandatory=$false)] - [string]$DeferDeadline = '', - ## Specify whether to check if there is enough disk space for the installation to proceed. If this parameter is specified without the RequiredDiskSpace parameter, the required disk space is calculated automatically based on the size of the script source and associated files. - [Parameter(ParameterSetName = "CheckDiskSpaceParameterSet",Mandatory=$true)] - [ValidateScript({$_.IsPresent -eq ($true -or $false)})] - [switch]$CheckDiskSpace, - ## Specify required disk space in MB, used in combination with $CheckDiskSpace. - [Parameter(ParameterSetName = "CheckDiskSpaceParameterSet",Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$RequiredDiskSpace = 0, - ## Specify whether to minimize other windows when displaying prompt - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$MinimizeWindows = $true, - ## Specifies whether the window is the topmost window - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$TopMost = $true, - ## Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$ForceCountdown = 0, - ## Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. - [Parameter(Mandatory=$false)] - [switch]$CustomText = $false - ) - - 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 running in NonInteractive mode, force the processes to close silently - If ($deployModeNonInteractive) { $Silent = $true } - - ## If using Zero-Config MSI Deployment, append any executables found in the MSI to the CloseApps list - If ($useDefaultMsi) { $CloseApps = "$CloseApps,$defaultMsiExecutablesList" } - - ## Check disk space requirements if specified - If ($CheckDiskSpace) { - Write-Log -Message 'Evaluate disk space requirements.' -Source ${CmdletName} - [double]$freeDiskSpace = Get-FreeDiskSpace - If ($RequiredDiskSpace -eq 0) { - Try { - # Determine the size of the Files folder - $fso = New-Object -ComObject 'Scripting.FileSystemObject' -ErrorAction 'Stop' - $RequiredDiskSpace = [math]::Round((($fso.GetFolder($scriptParentPath).Size) / 1MB)) - } - Catch { - Write-Log -Message "Failed to calculate disk space requirement from source files. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - If ($freeDiskSpace -lt $RequiredDiskSpace) { - Write-Log -Message "Failed to meet minimum disk space requirement. Space Required [$RequiredDiskSpace MB], Space Available [$freeDiskSpace MB]." -Severity 3 -Source ${CmdletName} - If (-not $Silent) { - Show-InstallationPrompt -Message ($configDiskSpaceMessage -f $installTitle, $RequiredDiskSpace, ($freeDiskSpace)) -ButtonRightText 'OK' -Icon 'Error' - } - Exit-Script -ExitCode $configInstallationUIExitCode - } - Else { - Write-Log -Message 'Successfully passed minimum disk space requirement check.' -Source ${CmdletName} - } - } - - If ($CloseApps) { - ## Create a Process object with custom descriptions where they are provided (split on an '=' sign) - [psobject[]]$processObjects = @() - # Split multiple processes on a comma, then split on equal sign, then create custom object with process name and description - ForEach ($process in ($CloseApps -split ',' | Where-Object { $_ })) { - If ($process.Contains('=')) { - [string[]]$ProcessSplit = $process -split '=' - $processObjects += New-Object -TypeName 'PSObject' -Property @{ - ProcessName = $ProcessSplit[0] - ProcessDescription = $ProcessSplit[1] - } - } - Else { - [string]$ProcessInfo = $process - $processObjects += New-Object -TypeName 'PSObject' -Property @{ - ProcessName = $process - ProcessDescription = '' - } - } - } - } - - ## Check Deferral history and calculate remaining deferrals - If (($allowDefer) -or ($AllowDeferCloseApps)) { - # Set $allowDefer to true if $AllowDeferCloseApps is true - $allowDefer = $true - - # Get the deferral history from the registry - $deferHistory = Get-DeferHistory - $deferHistoryTimes = $deferHistory | Select-Object -ExpandProperty 'DeferTimesRemaining' -ErrorAction 'SilentlyContinue' - $deferHistoryDeadline = $deferHistory | Select-Object -ExpandProperty 'DeferDeadline' -ErrorAction 'SilentlyContinue' - - # Reset Switches - $checkDeferDays = $false - $checkDeferDeadline = $false - If ($DeferDays -ne 0) { $checkDeferDays = $true } - If ($DeferDeadline) { $checkDeferDeadline = $true } - If ($DeferTimes -ne 0) { - If ($deferHistoryTimes -ge 0) { - Write-Log -Message "Defer history shows [$($deferHistory.DeferTimesRemaining)] deferrals remaining." -Source ${CmdletName} - $DeferTimes = $deferHistory.DeferTimesRemaining - 1 - } - Else { - $DeferTimes = $DeferTimes - 1 - } - Write-Log -Message "User has [$deferTimes] deferrals remaining." -Source ${CmdletName} - If ($DeferTimes -lt 0) { - Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} - $AllowDefer = $false - } - } - Else { - If (Test-Path -LiteralPath 'variable:deferTimes') { Remove-Variable -Name 'deferTimes' } - $DeferTimes = $null - } - If ($checkDeferDays -and $allowDefer) { - If ($deferHistoryDeadline) { - Write-Log -Message "Defer history shows a deadline date of [$deferHistoryDeadline]." -Source ${CmdletName} - [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime $deferHistoryDeadline - } - Else { - [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime (Get-Date -Date ((Get-Date).AddDays($deferDays)) -Format ($culture).DateTimeFormat.UniversalDateTimePattern).ToString() - } - Write-Log -Message "User has until [$deferDeadlineUniversal] before deferral expires." -Source ${CmdletName} - If ((Get-UniversalDate) -gt $deferDeadlineUniversal) { - Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} - $AllowDefer = $false - } - } - If ($checkDeferDeadline -and $allowDefer) { - # Validate Date - Try { - [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime $deferDeadline -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Date is not in the correct format for the current culture. Type the date in the current locale format, such as 20/08/2014 (Europe) or 08/20/2014 (United States). If the script is intended for multiple cultures, specify the date in the universal sortable date/time format, e.g. '2013-08-22 11:51:52Z'. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - Throw "Date is not in the correct format for the current culture. Type the date in the current locale format, such as 20/08/2014 (Europe) or 08/20/2014 (United States). If the script is intended for multiple cultures, specify the date in the universal sortable date/time format, e.g. '2013-08-22 11:51:52Z': $($_.Exception.Message)" - } - Write-Log -Message "User has until [$deferDeadlineUniversal] remaining." -Source ${CmdletName} - If ((Get-UniversalDate) -gt $deferDeadlineUniversal) { - Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} - $AllowDefer = $false - } - } - } - If (($deferTimes -lt 0) -and (-not ($deferDeadlineUniversal))) { $AllowDefer = $false } - - ## Prompt the user to close running applications and optionally defer if enabled - If (-not ($deployModeSilent) -and (-not ($silent))) { - If ($forceCloseAppsCountdown -gt 0) { - # Keep the same variable for countdown to simplify the code: - $closeAppsCountdown = $forceCloseAppsCountdown - # Change this variable to a boolean now to switch the countdown on even with deferral - [boolean]$forceCloseAppsCountdown = $true - } - ElseIf ($forceCountdown -gt 0){ - # Keep the same variable for countdown to simplify the code: - $closeAppsCountdown = $forceCountdown - # Change this variable to a boolean now to switch the countdown on - [boolean]$forceCountdown = $true - } - Set-Variable -Name 'closeAppsCountdownGlobal' -Value $closeAppsCountdown -Scope 'Script' - - While ((Get-RunningProcesses -ProcessObjects $processObjects -OutVariable 'runningProcesses') -or (($promptResult -ne 'Defer') -and ($promptResult -ne 'Close'))) { - [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' - # Check if we need to prompt the user to defer, to defer and close apps, or not to prompt them at all - If ($allowDefer) { - # If there is deferral and closing apps is allowed but there are no apps to be closed, break the while loop - If ($AllowDeferCloseApps -and (-not $runningProcessDescriptions)) { - Break - } - # Otherwise, as long as the user has not selected to close the apps or the processes are still running and the user has not selected to continue, prompt user to close running processes with deferral - ElseIf (($promptResult -ne 'Close') -or (($runningProcessDescriptions) -and ($promptResult -ne 'Continue'))) { - [string]$promptResult = Show-WelcomePrompt -ProcessDescriptions $runningProcessDescriptions -CloseAppsCountdown $closeAppsCountdownGlobal -ForceCloseAppsCountdown $forceCloseAppsCountdown -ForceCountdown $forceCountdown -PersistPrompt $PersistPrompt -AllowDefer -DeferTimes $deferTimes -DeferDeadline $deferDeadlineUniversal -MinimizeWindows $MinimizeWindows -CustomText:$CustomText -TopMost $TopMost - } - } - # If there is no deferral and processes are running, prompt the user to close running processes with no deferral option - ElseIf (($runningProcessDescriptions) -or ($forceCountdown)) { - [string]$promptResult = Show-WelcomePrompt -ProcessDescriptions $runningProcessDescriptions -CloseAppsCountdown $closeAppsCountdownGlobal -ForceCloseAppsCountdown $forceCloseAppsCountdown -ForceCountdown $forceCountdown -PersistPrompt $PersistPrompt -MinimizeWindows $minimizeWindows -CustomText:$CustomText -TopMost $TopMost - } - # If there is no deferral and no processes running, break the while loop - Else { - Break - } - - # If the user has clicked OK, wait a few seconds for the process to terminate before evaluating the running processes again - If ($promptResult -eq 'Continue') { - Write-Log -Message 'User selected to continue...' -Source ${CmdletName} - Start-Sleep -Seconds 2 - - # Break the while loop if there are no processes to close and the user has clicked OK to continue - If (-not $runningProcesses) { Break } - } - # Force the applications to close - ElseIf ($promptResult -eq 'Close') { - Write-Log -Message 'User selected to force the application(s) to close...' -Source ${CmdletName} - If (($PromptToSave) -and ($SessionZero -and (-not $IsProcessUserInteractive))) { - Write-Log -Message 'Specified [-PromptToSave] option will not be available because current process is running in session zero and is not interactive.' -Severity 2 -Source ${CmdletName} - } - - ForEach ($runningProcess in $runningProcesses) { - [psobject[]]$AllOpenWindowsForRunningProcess = Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.ParentProcess -eq $runningProcess.Name } - # If the PromptToSave parameter was specified and the process has a window open, then prompt the user to save work if there is work to be saved when closing window - If (($PromptToSave) -and (-not ($SessionZero -and (-not $IsProcessUserInteractive))) -and ($AllOpenWindowsForRunningProcess) -and ($runningProcess.MainWindowHandle -ne [IntPtr]::Zero)) { - [timespan]$PromptToSaveTimeout = New-TimeSpan -Seconds $configInstallationPromptToSave - [Diagnostics.StopWatch]$PromptToSaveStopWatch = [Diagnostics.StopWatch]::StartNew() - $PromptToSaveStopWatch.Reset() - ForEach ($OpenWindow in $AllOpenWindowsForRunningProcess) { - Try { - Write-Log -Message "Stop process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] and prompt to save if there is work to be saved (timeout in [$configInstallationPromptToSave] seconds)..." -Source ${CmdletName} - [boolean]$IsBringWindowToFrontSuccess = [PSADT.UiAutomation]::BringWindowToFront($OpenWindow.WindowHandle) - [boolean]$IsCloseWindowCallSuccess = $runningProcess.CloseMainWindow() - If (-not $IsCloseWindowCallSuccess) { - Write-Log -Message "Failed to call the CloseMainWindow() method on process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] because the main window may be disabled due to a modal dialog being shown." -Severity 3 -Source ${CmdletName} - } - Else { - $PromptToSaveStopWatch.Start() - Do { - [boolean]$IsWindowOpen = [boolean](Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.WindowHandle -eq $OpenWindow.WindowHandle }) - If (-not $IsWindowOpen) { Break } - Start-Sleep -Seconds 3 - } While (($IsWindowOpen) -and ($PromptToSaveStopWatch.Elapsed -lt $PromptToSaveTimeout)) - $PromptToSaveStopWatch.Reset() - If ($IsWindowOpen) { - Write-Log -Message "Exceeded the [$configInstallationPromptToSave] seconds timeout value for the user to save work associated with process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)]." -Severity 2 -Source ${CmdletName} - } - Else { - Write-Log -Message "Window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)] was successfully closed." -Source ${CmdletName} - } - } - } - Catch { - Write-Log -Message "Failed to close window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - Continue - } - Finally { - $runningProcess.Refresh() - } - } - } - Else { - Write-Log -Message "Stop process $($runningProcess.Name)..." -Source ${CmdletName} - Stop-Process -Id $runningProcess.Id -Force -ErrorAction 'SilentlyContinue' - } - } - Start-Sleep -Seconds 2 - } - # Stop the script (if not actioned before the timeout value) - ElseIf ($promptResult -eq 'Timeout') { - Write-Log -Message 'Installation not actioned before the timeout value.' -Source ${CmdletName} - $BlockExecution = $false - - If (($deferTimes -ge 0) -or ($deferDeadlineUniversal)) { - Set-DeferHistory -DeferTimesRemaining $DeferTimes -DeferDeadline $deferDeadlineUniversal - } - ## Dispose the welcome prompt timer here because if we dispose it within the Show-WelcomePrompt function we risk resetting the timer and missing the specified timeout period - If ($script:welcomeTimer) { - Try { - $script:welcomeTimer.Dispose() - $script:welcomeTimer = $null - } - Catch { } - } - - # Restore minimized windows - $null = $shellApp.UndoMinimizeAll() - - Exit-Script -ExitCode $configInstallationUIExitCode - } - # Stop the script (user chose to defer) - ElseIf ($promptResult -eq 'Defer') { - Write-Log -Message 'Installation deferred by the user.' -Source ${CmdletName} - $BlockExecution = $false - - Set-DeferHistory -DeferTimesRemaining $DeferTimes -DeferDeadline $deferDeadlineUniversal - - # Restore minimized windows - $null = $shellApp.UndoMinimizeAll() - - Exit-Script -ExitCode $configInstallationDeferExitCode - } - } - } - - ## Force the processes to close silently, without prompting the user - If (($Silent -or $deployModeSilent) -and $CloseApps) { - [array]$runningProcesses = $null - [array]$runningProcesses = Get-RunningProcesses $processObjects - If ($runningProcesses) { - [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' - Write-Log -Message "Force close application(s) [$($runningProcessDescriptions)] without prompting user." -Source ${CmdletName} - $runningProcesses | Stop-Process -Force -ErrorAction 'SilentlyContinue' - Start-Sleep -Seconds 2 - } - } - - ## Force nsd.exe to stop if Notes is one of the required applications to close - If (($processObjects | Select-Object -ExpandProperty 'ProcessName') -contains 'notes') { - ## Get the path where Notes is installed - [string]$notesPath = Get-Item -LiteralPath $regKeyLotusNotes -ErrorAction 'SilentlyContinue' | Get-ItemProperty | Select-Object -ExpandProperty 'Path' - - ## Ensure we aren't running as a Local System Account and Notes install directory was found - If ((-not $IsLocalSystemAccount) -and ($notesPath)) { - # Get a list of all the executables in the Notes folder - [string[]]$notesPathExes = Get-ChildItem -LiteralPath $notesPath -Filter '*.exe' -Recurse | Select-Object -ExpandProperty 'BaseName' | Sort-Object - ## Check for running Notes executables and run NSD if any are found - $notesPathExes | ForEach-Object { - If ((Get-Process | Select-Object -ExpandProperty 'Name') -contains $_) { - [string]$notesNSDExecutable = Join-Path -Path $notesPath -ChildPath 'NSD.exe' - Try { - If (Test-Path -LiteralPath $notesNSDExecutable -PathType 'Leaf' -ErrorAction 'Stop') { - Write-Log -Message "Execute [$notesNSDExecutable] with the -kill argument..." -Source ${CmdletName} - [Diagnostics.Process]$notesNSDProcess = Start-Process -FilePath $notesNSDExecutable -ArgumentList '-kill' -WindowStyle 'Hidden' -PassThru -ErrorAction 'SilentlyContinue' - - If (-not ($notesNSDProcess.WaitForExit(10000))) { - Write-Log -Message "[$notesNSDExecutable] did not end in a timely manner. Force terminate process." -Source ${CmdletName} - Stop-Process -Name 'NSD' -Force -ErrorAction 'SilentlyContinue' - } - } - } - Catch { - Write-Log -Message "Failed to launch [$notesNSDExecutable]. `n$(Resolve-Error)" -Source ${CmdletName} - } - - Write-Log -Message "[$notesNSDExecutable] returned exit code [$($notesNSDProcess.ExitCode)]." -Source ${CmdletName} - - # Force NSD process to stop in case the previous command was not successful - Stop-Process -Name 'NSD' -Force -ErrorAction 'SilentlyContinue' - } - } - } - - # Strip all Notes processes from the process list except notes.exe, because the other notes processes (e.g. notes2.exe) may be invoked by the Notes installation, so we don't want to block their execution. - If ($notesPathExes) { - [array]$processesIgnoringNotesExceptions = Compare-Object -ReferenceObject ($processObjects | Select-Object -ExpandProperty 'ProcessName' | Sort-Object) -DifferenceObject $notesPathExes -IncludeEqual | Where-Object { ($_.SideIndicator -eq '<=') -or ($_.InputObject -eq 'notes') } | Select-Object -ExpandProperty 'InputObject' - [array]$processObjects = $processObjects | Where-Object { $processesIgnoringNotesExceptions -contains $_.ProcessName } - } - } - - ## If block execution switch is true, call the function to block execution of these processes - If ($BlockExecution) { - # Make this variable globally available so we can check whether we need to call Unblock-AppExecution - Set-Variable -Name 'BlockExecution' -Value $BlockExecution -Scope 'Script' - Write-Log -Message '[-BlockExecution] parameter specified.' -Source ${CmdletName} - Block-AppExecution -ProcessName ($processObjects | Select-Object -ExpandProperty 'ProcessName') - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding(DefaultParametersetName='None')] + + Param ( + ## Specify process names separated by commas. Optionally specify a process description with an equals symbol, e.g. "winword=Microsoft Office Word" + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$CloseApps, + ## Specify whether to prompt user or force close the applications + [Parameter(Mandatory=$false)] + [switch]$Silent = $false, + ## Specify a countdown to display before automatically closing applications where deferral is not allowed or has expired + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$CloseAppsCountdown = 0, + ## Specify a countdown to display before automatically closing applications whether or not deferral is allowed + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$ForceCloseAppsCountdown = 0, + ## Specify whether to prompt to save working documents when the user chooses to close applications by selecting the "Close Programs" button + [Parameter(Mandatory=$false)] + [switch]$PromptToSave = $false, + ## Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. + [Parameter(Mandatory=$false)] + [switch]$PersistPrompt = $false, + ## Specify whether to block execution of the processes during installation + [Parameter(Mandatory=$false)] + [switch]$BlockExecution = $false, + ## Specify whether to enable the optional defer button on the dialog box + [Parameter(Mandatory=$false)] + [switch]$AllowDefer = $false, + ## Specify whether to enable the optional defer button on the dialog box only if an app needs to be closed + [Parameter(Mandatory=$false)] + [switch]$AllowDeferCloseApps = $false, + ## Specify the number of times the deferral is allowed + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$DeferTimes = 0, + ## Specify the number of days since first run that the deferral is allowed + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$DeferDays = 0, + ## Specify the deadline (in format dd/mm/yyyy) for which deferral will expire as an option + [Parameter(Mandatory=$false)] + [string]$DeferDeadline = '', + ## Specify whether to check if there is enough disk space for the installation to proceed. If this parameter is specified without the RequiredDiskSpace parameter, the required disk space is calculated automatically based on the size of the script source and associated files. + [Parameter(ParameterSetName = "CheckDiskSpaceParameterSet",Mandatory=$true)] + [ValidateScript({$_.IsPresent -eq ($true -or $false)})] + [switch]$CheckDiskSpace, + ## Specify required disk space in MB, used in combination with $CheckDiskSpace. + [Parameter(ParameterSetName = "CheckDiskSpaceParameterSet",Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$RequiredDiskSpace = 0, + ## Specify whether to minimize other windows when displaying prompt + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$MinimizeWindows = $true, + ## Specifies whether the window is the topmost window + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$TopMost = $true, + ## Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$ForceCountdown = 0, + ## Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. + [Parameter(Mandatory=$false)] + [switch]$CustomText = $false + ) + + 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 running in NonInteractive mode, force the processes to close silently + If ($deployModeNonInteractive) { $Silent = $true } + + ## If using Zero-Config MSI Deployment, append any executables found in the MSI to the CloseApps list + If ($useDefaultMsi) { $CloseApps = "$CloseApps,$defaultMsiExecutablesList" } + + ## Check disk space requirements if specified + If ($CheckDiskSpace) { + Write-Log -Message 'Evaluate disk space requirements.' -Source ${CmdletName} + [double]$freeDiskSpace = Get-FreeDiskSpace + If ($RequiredDiskSpace -eq 0) { + Try { + # Determine the size of the Files folder + $fso = New-Object -ComObject 'Scripting.FileSystemObject' -ErrorAction 'Stop' + $RequiredDiskSpace = [math]::Round((($fso.GetFolder($scriptParentPath).Size) / 1MB)) + } + Catch { + Write-Log -Message "Failed to calculate disk space requirement from source files. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + If ($freeDiskSpace -lt $RequiredDiskSpace) { + Write-Log -Message "Failed to meet minimum disk space requirement. Space Required [$RequiredDiskSpace MB], Space Available [$freeDiskSpace MB]." -Severity 3 -Source ${CmdletName} + If (-not $Silent) { + Show-InstallationPrompt -Message ($configDiskSpaceMessage -f $installTitle, $RequiredDiskSpace, ($freeDiskSpace)) -ButtonRightText 'OK' -Icon 'Error' + } + Exit-Script -ExitCode $configInstallationUIExitCode + } + Else { + Write-Log -Message 'Successfully passed minimum disk space requirement check.' -Source ${CmdletName} + } + } + + If ($CloseApps) { + ## Create a Process object with custom descriptions where they are provided (split on an '=' sign) + [psobject[]]$processObjects = @() + # Split multiple processes on a comma, then split on equal sign, then create custom object with process name and description + ForEach ($process in ($CloseApps -split ',' | Where-Object { $_ })) { + If ($process.Contains('=')) { + [string[]]$ProcessSplit = $process -split '=' + $processObjects += New-Object -TypeName 'PSObject' -Property @{ + ProcessName = $ProcessSplit[0] + ProcessDescription = $ProcessSplit[1] + } + } + Else { + [string]$ProcessInfo = $process + $processObjects += New-Object -TypeName 'PSObject' -Property @{ + ProcessName = $process + ProcessDescription = '' + } + } + } + } + + ## Check Deferral history and calculate remaining deferrals + If (($allowDefer) -or ($AllowDeferCloseApps)) { + # Set $allowDefer to true if $AllowDeferCloseApps is true + $allowDefer = $true + + # Get the deferral history from the registry + $deferHistory = Get-DeferHistory + $deferHistoryTimes = $deferHistory | Select-Object -ExpandProperty 'DeferTimesRemaining' -ErrorAction 'SilentlyContinue' + $deferHistoryDeadline = $deferHistory | Select-Object -ExpandProperty 'DeferDeadline' -ErrorAction 'SilentlyContinue' + + # Reset Switches + $checkDeferDays = $false + $checkDeferDeadline = $false + If ($DeferDays -ne 0) { $checkDeferDays = $true } + If ($DeferDeadline) { $checkDeferDeadline = $true } + If ($DeferTimes -ne 0) { + If ($deferHistoryTimes -ge 0) { + Write-Log -Message "Defer history shows [$($deferHistory.DeferTimesRemaining)] deferrals remaining." -Source ${CmdletName} + $DeferTimes = $deferHistory.DeferTimesRemaining - 1 + } + Else { + $DeferTimes = $DeferTimes - 1 + } + Write-Log -Message "User has [$deferTimes] deferrals remaining." -Source ${CmdletName} + If ($DeferTimes -lt 0) { + Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} + $AllowDefer = $false + } + } + Else { + If (Test-Path -LiteralPath 'variable:deferTimes') { Remove-Variable -Name 'deferTimes' } + $DeferTimes = $null + } + If ($checkDeferDays -and $allowDefer) { + If ($deferHistoryDeadline) { + Write-Log -Message "Defer history shows a deadline date of [$deferHistoryDeadline]." -Source ${CmdletName} + [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime $deferHistoryDeadline + } + Else { + [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime (Get-Date -Date ((Get-Date).AddDays($deferDays)) -Format ($culture).DateTimeFormat.UniversalDateTimePattern).ToString() + } + Write-Log -Message "User has until [$deferDeadlineUniversal] before deferral expires." -Source ${CmdletName} + If ((Get-UniversalDate) -gt $deferDeadlineUniversal) { + Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} + $AllowDefer = $false + } + } + If ($checkDeferDeadline -and $allowDefer) { + # Validate Date + Try { + [string]$deferDeadlineUniversal = Get-UniversalDate -DateTime $deferDeadline -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Date is not in the correct format for the current culture. Type the date in the current locale format, such as 20/08/2014 (Europe) or 08/20/2014 (United States). If the script is intended for multiple cultures, specify the date in the universal sortable date/time format, e.g. '2013-08-22 11:51:52Z'. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Throw "Date is not in the correct format for the current culture. Type the date in the current locale format, such as 20/08/2014 (Europe) or 08/20/2014 (United States). If the script is intended for multiple cultures, specify the date in the universal sortable date/time format, e.g. '2013-08-22 11:51:52Z': $($_.Exception.Message)" + } + Write-Log -Message "User has until [$deferDeadlineUniversal] remaining." -Source ${CmdletName} + If ((Get-UniversalDate) -gt $deferDeadlineUniversal) { + Write-Log -Message 'Deferral has expired.' -Source ${CmdletName} + $AllowDefer = $false + } + } + } + If (($deferTimes -lt 0) -and (-not ($deferDeadlineUniversal))) { $AllowDefer = $false } + + ## Prompt the user to close running applications and optionally defer if enabled + If (-not ($deployModeSilent) -and (-not ($silent))) { + If ($forceCloseAppsCountdown -gt 0) { + # Keep the same variable for countdown to simplify the code: + $closeAppsCountdown = $forceCloseAppsCountdown + # Change this variable to a boolean now to switch the countdown on even with deferral + [boolean]$forceCloseAppsCountdown = $true + } + ElseIf ($forceCountdown -gt 0){ + # Keep the same variable for countdown to simplify the code: + $closeAppsCountdown = $forceCountdown + # Change this variable to a boolean now to switch the countdown on + [boolean]$forceCountdown = $true + } + Set-Variable -Name 'closeAppsCountdownGlobal' -Value $closeAppsCountdown -Scope 'Script' + + While ((Get-RunningProcesses -ProcessObjects $processObjects -OutVariable 'runningProcesses') -or (($promptResult -ne 'Defer') -and ($promptResult -ne 'Close'))) { + [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' + # Check if we need to prompt the user to defer, to defer and close apps, or not to prompt them at all + If ($allowDefer) { + # If there is deferral and closing apps is allowed but there are no apps to be closed, break the while loop + If ($AllowDeferCloseApps -and (-not $runningProcessDescriptions)) { + Break + } + # Otherwise, as long as the user has not selected to close the apps or the processes are still running and the user has not selected to continue, prompt user to close running processes with deferral + ElseIf (($promptResult -ne 'Close') -or (($runningProcessDescriptions) -and ($promptResult -ne 'Continue'))) { + [string]$promptResult = Show-WelcomePrompt -ProcessDescriptions $runningProcessDescriptions -CloseAppsCountdown $closeAppsCountdownGlobal -ForceCloseAppsCountdown $forceCloseAppsCountdown -ForceCountdown $forceCountdown -PersistPrompt $PersistPrompt -AllowDefer -DeferTimes $deferTimes -DeferDeadline $deferDeadlineUniversal -MinimizeWindows $MinimizeWindows -CustomText:$CustomText -TopMost $TopMost + } + } + # If there is no deferral and processes are running, prompt the user to close running processes with no deferral option + ElseIf (($runningProcessDescriptions) -or ($forceCountdown)) { + [string]$promptResult = Show-WelcomePrompt -ProcessDescriptions $runningProcessDescriptions -CloseAppsCountdown $closeAppsCountdownGlobal -ForceCloseAppsCountdown $forceCloseAppsCountdown -ForceCountdown $forceCountdown -PersistPrompt $PersistPrompt -MinimizeWindows $minimizeWindows -CustomText:$CustomText -TopMost $TopMost + } + # If there is no deferral and no processes running, break the while loop + Else { + Break + } + + # If the user has clicked OK, wait a few seconds for the process to terminate before evaluating the running processes again + If ($promptResult -eq 'Continue') { + Write-Log -Message 'User selected to continue...' -Source ${CmdletName} + Start-Sleep -Seconds 2 + + # Break the while loop if there are no processes to close and the user has clicked OK to continue + If (-not $runningProcesses) { Break } + } + # Force the applications to close + ElseIf ($promptResult -eq 'Close') { + Write-Log -Message 'User selected to force the application(s) to close...' -Source ${CmdletName} + If (($PromptToSave) -and ($SessionZero -and (-not $IsProcessUserInteractive))) { + Write-Log -Message 'Specified [-PromptToSave] option will not be available because current process is running in session zero and is not interactive.' -Severity 2 -Source ${CmdletName} + } + + ForEach ($runningProcess in $runningProcesses) { + [psobject[]]$AllOpenWindowsForRunningProcess = Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.ParentProcess -eq $runningProcess.Name } + # If the PromptToSave parameter was specified and the process has a window open, then prompt the user to save work if there is work to be saved when closing window + If (($PromptToSave) -and (-not ($SessionZero -and (-not $IsProcessUserInteractive))) -and ($AllOpenWindowsForRunningProcess) -and ($runningProcess.MainWindowHandle -ne [IntPtr]::Zero)) { + [timespan]$PromptToSaveTimeout = New-TimeSpan -Seconds $configInstallationPromptToSave + [Diagnostics.StopWatch]$PromptToSaveStopWatch = [Diagnostics.StopWatch]::StartNew() + $PromptToSaveStopWatch.Reset() + ForEach ($OpenWindow in $AllOpenWindowsForRunningProcess) { + Try { + Write-Log -Message "Stop process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] and prompt to save if there is work to be saved (timeout in [$configInstallationPromptToSave] seconds)..." -Source ${CmdletName} + [boolean]$IsBringWindowToFrontSuccess = [PSADT.UiAutomation]::BringWindowToFront($OpenWindow.WindowHandle) + [boolean]$IsCloseWindowCallSuccess = $runningProcess.CloseMainWindow() + If (-not $IsCloseWindowCallSuccess) { + Write-Log -Message "Failed to call the CloseMainWindow() method on process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)] because the main window may be disabled due to a modal dialog being shown." -Severity 3 -Source ${CmdletName} + } + Else { + $PromptToSaveStopWatch.Start() + Do { + [boolean]$IsWindowOpen = [boolean](Get-WindowTitle -GetAllWindowTitles -DisableFunctionLogging | Where-Object { $_.WindowHandle -eq $OpenWindow.WindowHandle }) + If (-not $IsWindowOpen) { Break } + Start-Sleep -Seconds 3 + } While (($IsWindowOpen) -and ($PromptToSaveStopWatch.Elapsed -lt $PromptToSaveTimeout)) + $PromptToSaveStopWatch.Reset() + If ($IsWindowOpen) { + Write-Log -Message "Exceeded the [$configInstallationPromptToSave] seconds timeout value for the user to save work associated with process [$($runningProcess.Name)] with window title [$($OpenWindow.WindowTitle)]." -Severity 2 -Source ${CmdletName} + } + Else { + Write-Log -Message "Window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)] was successfully closed." -Source ${CmdletName} + } + } + } + Catch { + Write-Log -Message "Failed to close window [$($OpenWindow.WindowTitle)] for process [$($runningProcess.Name)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + Continue + } + Finally { + $runningProcess.Refresh() + } + } + } + Else { + Write-Log -Message "Stop process $($runningProcess.Name)..." -Source ${CmdletName} + Stop-Process -Id $runningProcess.Id -Force -ErrorAction 'SilentlyContinue' + } + } + Start-Sleep -Seconds 2 + } + # Stop the script (if not actioned before the timeout value) + ElseIf ($promptResult -eq 'Timeout') { + Write-Log -Message 'Installation not actioned before the timeout value.' -Source ${CmdletName} + $BlockExecution = $false + + If (($deferTimes -ge 0) -or ($deferDeadlineUniversal)) { + Set-DeferHistory -DeferTimesRemaining $DeferTimes -DeferDeadline $deferDeadlineUniversal + } + ## Dispose the welcome prompt timer here because if we dispose it within the Show-WelcomePrompt function we risk resetting the timer and missing the specified timeout period + If ($script:welcomeTimer) { + Try { + $script:welcomeTimer.Dispose() + $script:welcomeTimer = $null + } + Catch { } + } + + # Restore minimized windows + $null = $shellApp.UndoMinimizeAll() + + Exit-Script -ExitCode $configInstallationUIExitCode + } + # Stop the script (user chose to defer) + ElseIf ($promptResult -eq 'Defer') { + Write-Log -Message 'Installation deferred by the user.' -Source ${CmdletName} + $BlockExecution = $false + + Set-DeferHistory -DeferTimesRemaining $DeferTimes -DeferDeadline $deferDeadlineUniversal + + # Restore minimized windows + $null = $shellApp.UndoMinimizeAll() + + Exit-Script -ExitCode $configInstallationDeferExitCode + } + } + } + + ## Force the processes to close silently, without prompting the user + If (($Silent -or $deployModeSilent) -and $CloseApps) { + [array]$runningProcesses = $null + [array]$runningProcesses = Get-RunningProcesses $processObjects + If ($runningProcesses) { + [string]$runningProcessDescriptions = ($runningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' + Write-Log -Message "Force close application(s) [$($runningProcessDescriptions)] without prompting user." -Source ${CmdletName} + $runningProcesses | Stop-Process -Force -ErrorAction 'SilentlyContinue' + Start-Sleep -Seconds 2 + } + } + + ## Force nsd.exe to stop if Notes is one of the required applications to close + If (($processObjects | Select-Object -ExpandProperty 'ProcessName') -contains 'notes') { + ## Get the path where Notes is installed + [string]$notesPath = Get-Item -LiteralPath $regKeyLotusNotes -ErrorAction 'SilentlyContinue' | Get-ItemProperty | Select-Object -ExpandProperty 'Path' + + ## Ensure we aren't running as a Local System Account and Notes install directory was found + If ((-not $IsLocalSystemAccount) -and ($notesPath)) { + # Get a list of all the executables in the Notes folder + [string[]]$notesPathExes = Get-ChildItem -LiteralPath $notesPath -Filter '*.exe' -Recurse | Select-Object -ExpandProperty 'BaseName' | Sort-Object + ## Check for running Notes executables and run NSD if any are found + $notesPathExes | ForEach-Object { + If ((Get-Process | Select-Object -ExpandProperty 'Name') -contains $_) { + [string]$notesNSDExecutable = Join-Path -Path $notesPath -ChildPath 'NSD.exe' + Try { + If (Test-Path -LiteralPath $notesNSDExecutable -PathType 'Leaf' -ErrorAction 'Stop') { + Write-Log -Message "Execute [$notesNSDExecutable] with the -kill argument..." -Source ${CmdletName} + [Diagnostics.Process]$notesNSDProcess = Start-Process -FilePath $notesNSDExecutable -ArgumentList '-kill' -WindowStyle 'Hidden' -PassThru -ErrorAction 'SilentlyContinue' + + If (-not ($notesNSDProcess.WaitForExit(10000))) { + Write-Log -Message "[$notesNSDExecutable] did not end in a timely manner. Force terminate process." -Source ${CmdletName} + Stop-Process -Name 'NSD' -Force -ErrorAction 'SilentlyContinue' + } + } + } + Catch { + Write-Log -Message "Failed to launch [$notesNSDExecutable]. `n$(Resolve-Error)" -Source ${CmdletName} + } + + Write-Log -Message "[$notesNSDExecutable] returned exit code [$($notesNSDProcess.ExitCode)]." -Source ${CmdletName} + + # Force NSD process to stop in case the previous command was not successful + Stop-Process -Name 'NSD' -Force -ErrorAction 'SilentlyContinue' + } + } + } + + # Strip all Notes processes from the process list except notes.exe, because the other notes processes (e.g. notes2.exe) may be invoked by the Notes installation, so we don't want to block their execution. + If ($notesPathExes) { + [array]$processesIgnoringNotesExceptions = Compare-Object -ReferenceObject ($processObjects | Select-Object -ExpandProperty 'ProcessName' | Sort-Object) -DifferenceObject $notesPathExes -IncludeEqual | Where-Object { ($_.SideIndicator -eq '<=') -or ($_.InputObject -eq 'notes') } | Select-Object -ExpandProperty 'InputObject' + [array]$processObjects = $processObjects | Where-Object { $processesIgnoringNotesExceptions -contains $_.ProcessName } + } + } + + ## If block execution switch is true, call the function to block execution of these processes + If ($BlockExecution) { + # Make this variable globally available so we can check whether we need to call Unblock-AppExecution + Set-Variable -Name 'BlockExecution' -Value $BlockExecution -Scope 'Script' + Write-Log -Message '[-BlockExecution] parameter specified.' -Source ${CmdletName} + Block-AppExecution -ProcessName ($processObjects | Select-Object -ExpandProperty 'ProcessName') + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -6293,589 +6507,539 @@ Function Show-InstallationWelcome { Function Show-WelcomePrompt { <# .SYNOPSIS - Called by Show-InstallationWelcome to prompt the user to optionally do the following: - 1) Close the specified running applications. - 2) Provide an option to defer the installation. - 3) Show a countdown before applications are automatically closed. + Called by Show-InstallationWelcome to prompt the user to optionally do the following: + 1) Close the specified running applications. + 2) Provide an option to defer the installation. + 3) Show a countdown before applications are automatically closed. .DESCRIPTION - The user is presented with a Windows Forms dialog box to close the applications themselves and continue or to have the script close the applications for them. - If the -AllowDefer option is set to true, an optional "Defer" button will be shown to the user. If they select this option, the script will exit and return a 1618 code (SCCM fast retry code). - The dialog box will timeout after the timeout specified in the XML configuration file (default 1 hour and 55 minutes) to prevent SCCM installations from timing out and returning a failure code to SCCM. When the dialog times out, the script will exit and return a 1618 code (SCCM fast retry code). + The user is presented with a Windows Forms dialog box to close the applications themselves and continue or to have the script close the applications for them. + If the -AllowDefer option is set to true, an optional "Defer" button will be shown to the user. If they select this option, the script will exit and return a 1618 code (SCCM fast retry code). + The dialog box will timeout after the timeout specified in the XML configuration file (default 1 hour and 55 minutes) to prevent SCCM installations from timing out and returning a failure code to SCCM. When the dialog times out, the script will exit and return a 1618 code (SCCM fast retry code). .PARAMETER ProcessDescriptions - The descriptive names of the applications that are running and need to be closed. + The descriptive names of the applications that are running and need to be closed. .PARAMETER CloseAppsCountdown - Specify the countdown time in seconds before running applications are automatically closed when deferral is not allowed or expired. + Specify the countdown time in seconds before running applications are automatically closed when deferral is not allowed or expired. .PARAMETER ForceCloseAppsCountdown - Specify whether to show the countdown regardless of whether deferral is allowed. + Specify whether to show the countdown regardless of whether deferral is allowed. .PARAMETER PersistPrompt - Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. + Specify whether to make the prompt persist in the center of the screen every couple of seconds, specified in the AppDeployToolkitConfig.xml. .PARAMETER AllowDefer - Specify whether to provide an option to defer the installation. + Specify whether to provide an option to defer the installation. .PARAMETER DeferTimes - Specify the number of times the user is allowed to defer. + Specify the number of times the user is allowed to defer. .PARAMETER DeferDeadline - Specify the deadline date before the user is allowed to defer. + Specify the deadline date before the user is allowed to defer. .PARAMETER MinimizeWindows - Specifies whether to minimize other windows when displaying prompt. Default: $true. + Specifies whether to minimize other windows when displaying prompt. Default: $true. .PARAMETER TopMost - Specifies whether the windows is the topmost window. Default: $true. + Specifies whether the windows is the topmost window. Default: $true. .PARAMETER ForceCountdown - Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled. + Specify a countdown to display before automatically proceeding with the installation when a deferral is enabled. .PARAMETER CustomText - Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. + Specify whether to display a custom message specified in the XML file. Custom message must be populated for each language section in the XML. .EXAMPLE - Show-WelcomePrompt -ProcessDescriptions 'Lotus Notes, Microsoft Word' -CloseAppsCountdown 600 -AllowDefer -DeferTimes 10 + Show-WelcomePrompt -ProcessDescriptions 'Lotus Notes, Microsoft Word' -CloseAppsCountdown 600 -AllowDefer -DeferTimes 10 .NOTES - This is an internal script function and should typically not be called directly. It is used by the Show-InstallationWelcome prompt to display a custom prompt. + This is an internal script function and should typically not be called directly. It is used by the Show-InstallationWelcome prompt to display a custom prompt. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [string]$ProcessDescriptions, - [Parameter(Mandatory=$false)] - [int32]$CloseAppsCountdown, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ForceCloseAppsCountdown, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$PersistPrompt = $false, - [Parameter(Mandatory=$false)] - [switch]$AllowDefer = $false, - [Parameter(Mandatory=$false)] - [string]$DeferTimes, - [Parameter(Mandatory=$false)] - [string]$DeferDeadline, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$MinimizeWindows = $true, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$TopMost = $true, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$ForceCountdown = 0, - [Parameter(Mandatory=$false)] - [switch]$CustomText = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Reset switches - [boolean]$showCloseApps = $false - [boolean]$showDefer = $false - [boolean]$persistWindow = $false - - ## Reset times - [datetime]$startTime = Get-Date - [datetime]$countdownTime = $startTime - - ## Check if the countdown was specified - If ($CloseAppsCountdown) { - If ($CloseAppsCountdown -gt $configInstallationUITimeout) { - Throw 'The close applications countdown time cannot be longer than the timeout specified in the XML configuration for installation UI dialogs to timeout.' - } - } - - ## Initial form layout: Close Applications / Allow Deferral - If ($processDescriptions) { - Write-Log -Message "Prompt user to close application(s) [$processDescriptions]..." -Source ${CmdletName} - $showCloseApps = $true - } - If (($allowDefer) -and (($deferTimes -ge 0) -or ($deferDeadline))) { - Write-Log -Message 'User has the option to defer.' -Source ${CmdletName} - $showDefer = $true - If ($deferDeadline) { - # Remove the Z from universal sortable date time format, otherwise it could be converted to a different time zone - $deferDeadline = $deferDeadline -replace 'Z','' - # Convert the deadline date to a string - [string]$deferDeadline = (Get-Date -Date $deferDeadline).ToString() - } - } - - ## If deferral is being shown and 'close apps countdown' or 'persist prompt' was specified, enable those features. - If (-not $showDefer) { - If ($closeAppsCountdown -gt 0) { - Write-Log -Message "Close applications countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} - $showCountdown = $true - } - } - If ($showDefer) { - If ($persistPrompt) { $persistWindow = $true } - } - ## If 'force close apps countdown' was specified, enable that feature. - If ($forceCloseAppsCountdown -eq $true) { - Write-Log -Message "Close applications countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} - $showCountdown = $true - } - ## If 'force countdown' was specified, enable that feature. - If ($forceCountdown -eq $true) { - Write-Log -Message "Countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} - $showCountdown = $true - } - - [string[]]$processDescriptions = $processDescriptions -split ',' - [Windows.Forms.Application]::EnableVisualStyles() - - $formWelcome = New-Object -TypeName 'System.Windows.Forms.Form' - $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' - $labelAppName = New-Object -TypeName 'System.Windows.Forms.Label' - $labelCountdown = New-Object -TypeName 'System.Windows.Forms.Label' - $labelDefer = New-Object -TypeName 'System.Windows.Forms.Label' - $listBoxCloseApps = New-Object -TypeName 'System.Windows.Forms.ListBox' - $buttonContinue = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonDefer = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonCloseApps = New-Object -TypeName 'System.Windows.Forms.Button' - $buttonAbort = New-Object -TypeName 'System.Windows.Forms.Button' - $formWelcomeWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' - $flowLayoutPanel = New-Object -TypeName 'System.Windows.Forms.FlowLayoutPanel' - $panelButtons = New-Object -TypeName 'System.Windows.Forms.Panel' - $toolTip = New-Object -TypeName 'System.Windows.Forms.ToolTip' - - ## Remove all event handlers from the controls - [scriptblock]$Form_Cleanup_FormClosed = { - Try { - $labelAppName.remove_Click($handler_labelAppName_Click) - $labelDefer.remove_Click($handler_labelDefer_Click) - $buttonCloseApps.remove_Click($buttonCloseApps_OnClick) - $buttonContinue.remove_Click($buttonContinue_OnClick) - $buttonDefer.remove_Click($buttonDefer_OnClick) - $buttonAbort.remove_Click($buttonAbort_OnClick) - $script:welcomeTimer.remove_Tick($timer_Tick) - $timerPersist.remove_Tick($timerPersist_Tick) - $timerRunningProcesses.remove_Tick($timerRunningProcesses_Tick) - $formWelcome.remove_Load($Form_StateCorrection_Load) - $formWelcome.remove_FormClosed($Form_Cleanup_FormClosed) - } - Catch { } - } - - [scriptblock]$Form_StateCorrection_Load = { - ## Correct the initial state of the form to prevent the .NET maximized form issue - $formWelcome.WindowState = 'Normal' - $formWelcome.AutoSize = $true - $formWelcome.TopMost = $TopMost - $formWelcome.BringToFront() - # Get the start position of the form so we can return the form to this position if PersistPrompt is enabled - Set-Variable -Name 'formWelcomeStartPosition' -Value $formWelcome.Location -Scope 'Script' - - ## Initialize the countdown timer - [datetime]$currentTime = Get-Date - [datetime]$countdownTime = $startTime.AddSeconds($CloseAppsCountdown) - $script:welcomeTimer.Start() - - ## Set up the form - [timespan]$remainingTime = $countdownTime.Subtract($currentTime) - [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($forceCountdown -eq $true) { - switch ($deploymentType){ - 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } - 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeUninstall.ToLower())) + "`n$labelCountdownSeconds" } - 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeRepair.ToLower())) + "`n$labelCountdownSeconds" } - Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } - } - } - Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } - } - - ## Add the timer if it doesn't already exist - this avoids the timer being reset if the continue button is clicked - If (-not ($script:welcomeTimer)) { - $script:welcomeTimer = New-Object -TypeName 'System.Windows.Forms.Timer' - } - - If ($showCountdown) { - [scriptblock]$timer_Tick = { - ## Get the time information - [datetime]$currentTime = Get-Date - [datetime]$countdownTime = $startTime.AddSeconds($CloseAppsCountdown) - [timespan]$remainingTime = $countdownTime.Subtract($currentTime) - Set-Variable -Name 'closeAppsCountdownGlobal' -Value $remainingTime.TotalSeconds -Scope 'Script' - - ## If the countdown is complete, close the application(s) or continue - If ($countdownTime -lt $currentTime) { - If ($forceCountdown -eq $true) { - Write-Log -Message 'Countdown timer has elapsed. Force continue.' -Source ${CmdletName} - $buttonContinue.PerformClick() - } - Else { - Write-Log -Message 'Close application(s) countdown timer has elapsed. Force closing application(s).' -Source ${CmdletName} - If ($buttonCloseApps.CanFocus) { $buttonCloseApps.PerformClick() } - Else { $buttonContinue.PerformClick() } - } - } - Else { - # Update the form - [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($forceCountdown -eq $true) { - switch ($deploymentType){ - 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } - 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeUninstall) + "`n$labelCountdownSeconds" } - 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeRepair) + "`n$labelCountdownSeconds" } - Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } - } - } - Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } - [Windows.Forms.Application]::DoEvents() - } - } - } - Else { - $script:welcomeTimer.Interval = ($configInstallationUITimeout * 1000) - [scriptblock]$timer_Tick = { $buttonAbort.PerformClick() } - } - - $script:welcomeTimer.add_Tick($timer_Tick) - - ## Persistence Timer - If ($persistWindow) { - $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' - $timerPersist.Interval = ($configInstallationPersistInterval * 1000) - [scriptblock]$timerPersist_Tick = { Update-InstallationWelcome } - $timerPersist.add_Tick($timerPersist_Tick) - $timerPersist.Start() - } - - ## Process Re-Enumeration Timer - If ($configInstallationWelcomePromptDynamicRunningProcessEvaluation) { - $timerRunningProcesses = New-Object -TypeName 'System.Windows.Forms.Timer' - $timerRunningProcesses.Interval = ($configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval * 1000) - [scriptblock]$timerRunningProcesses_Tick = { try { Get-RunningProcessesDynamically } catch {} } - $timerRunningProcesses.add_Tick($timerRunningProcesses_Tick) - $timerRunningProcesses.Start() - } - - ## Form - $formWelcome.Controls.Add($pictureBanner) - $formWelcome.Controls.Add($buttonAbort) - - ##---------------------------------------------- - ## Create padding object - $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' - $paddingNone.Top = 0 - $paddingNone.Bottom = 0 - $paddingNone.Left = 0 - $paddingNone.Right = 0 - - ## Generic Button properties - $buttonWidth = 110 - $buttonHeight = 23 - $buttonPadding = 50 - $buttonSize = New-Object -TypeName 'System.Drawing.Size' - $buttonSize.Width = $buttonWidth - $buttonSize.Height = $buttonHeight - $buttonPadding = New-Object -TypeName 'System.Windows.Forms.Padding' - $buttonPadding.Top = 0 - $buttonPadding.Bottom = 5 - $buttonPadding.Left = 50 - $buttonPadding.Right = 0 - - ## Picture Banner - $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 - $pictureBanner.ImageLocation = $appDeployLogoBanner - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = 0 - $pictureBanner.Location = $System_Drawing_Point - $pictureBanner.Name = 'pictureBanner' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = $appDeployLogoBannerHeight - $System_Drawing_Size.Width = 450 - $pictureBanner.Size = $System_Drawing_Size - $pictureBanner.SizeMode = 'CenterImage' - $pictureBanner.Margin = $paddingNone - $pictureBanner.TabIndex = 0 - $pictureBanner.TabStop = $false - - ## Label App Name - $labelAppName.DataBindings.DefaultDataSourceUpdateMode = 0 - $labelAppName.Name = 'labelAppName' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - If (-not $showCloseApps) { - $System_Drawing_Size.Height = 40 - } - Else { - $System_Drawing_Size.Height = 65 - } - $System_Drawing_Size.Width = 450 - $labelAppName.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelAppName.MaximumSize = $System_Drawing_Size - $labelAppName.Margin = '0,15,0,15' - $labelAppName.Padding = '20,0,20,0' - $labelAppName.TabIndex = 1 - - ## Initial form layout: Close Applications / Allow Deferral - If ($showCloseApps) { - $labelAppNameText = $configClosePromptMessage - } - ElseIf (($showDefer) -or ($forceCountdown)) { - $labelAppNameText = "$configDeferPromptWelcomeMessage `n$installTitle" - } - If ($CustomText) { - $labelAppNameText = "$labelAppNameText `n`n$configWelcomePromptCustomMessage" - } - $labelAppName.Text = $labelAppNameText - $labelAppName.TextAlign = 'TopCenter' - $labelAppName.Anchor = 'Top' - $labelAppName.AutoSize = $true - $labelAppName.add_Click($handler_labelAppName_Click) - - ## Listbox Close Applications - $listBoxCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 - $listBoxCloseApps.FormattingEnabled = $true - $listBoxCloseApps.HorizontalScrollbar = $true - $listBoxCloseApps.Name = 'listBoxCloseApps' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 100 - $System_Drawing_Size.Width = 300 - $listBoxCloseApps.Size = $System_Drawing_Size - $listBoxCloseApps.Margin = '75,0,0,0' - $listBoxCloseApps.TabIndex = 3 - $ProcessDescriptions | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } - - ## Label Defer - $labelDefer.DataBindings.DefaultDataSourceUpdateMode = 0 - $labelDefer.Name = 'labelDefer' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 90 - $System_Drawing_Size.Width = 450 - $labelDefer.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelDefer.MaximumSize = $System_Drawing_Size - $labelDefer.Margin = $paddingNone - $labelDefer.Padding = '40,0,20,0' - $labelDefer.TabIndex = 4 - $deferralText = "$configDeferPromptExpiryMessage`n" - - If ($deferTimes -ge 0) { - $deferralText = "$deferralText `n$configDeferPromptRemainingDeferrals $([int32]$deferTimes + 1)" - } - If ($deferDeadline) { - $deferralText = "$deferralText `n$configDeferPromptDeadline $deferDeadline" - } - If (($deferTimes -lt 0) -and (-not $DeferDeadline)) { - $deferralText = "$deferralText `n$configDeferPromptNoDeadline" - } - $deferralText = "$deferralText `n`n$configDeferPromptWarningMessage" - $labelDefer.Text = $deferralText - $labelDefer.TextAlign = 'MiddleCenter' - $labelDefer.AutoSize = $true - $labelDefer.add_Click($handler_labelDefer_Click) - - ## Label Countdown - $labelCountdown.DataBindings.DefaultDataSourceUpdateMode = 0 - $labelCountdown.Name = 'labelCountdown' - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 40 - $System_Drawing_Size.Width = 450 - $labelCountdown.Size = $System_Drawing_Size - $System_Drawing_Size.Height = 0 - $labelCountdown.MaximumSize = $System_Drawing_Size - $labelCountdown.Margin = $paddingNone - $labelCountdown.Padding = '40,0,20,0' - $labelCountdown.TabIndex = 4 - $labelCountdown.Font = 'Microsoft Sans Serif, 9pt, style=Bold' - $labelCountdown.Text = '00:00:00' - $labelCountdown.TextAlign = 'MiddleCenter' - $labelCountdown.AutoSize = $true - $labelCountdown.add_Click($handler_labelDefer_Click) - - ## Panel Flow Layout - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - $System_Drawing_Point.Y = $appDeployLogoBannerHeight - $flowLayoutPanel.Location = $System_Drawing_Point - $flowLayoutPanel.AutoSize = $true - $flowLayoutPanel.Anchor = 'Top' - $flowLayoutPanel.FlowDirection = 'TopDown' - $flowLayoutPanel.WrapContents = $true - $flowLayoutPanel.Controls.Add($labelAppName) - If ($showCloseApps) { $flowLayoutPanel.Controls.Add($listBoxCloseApps) } - If ($showDefer) { - $flowLayoutPanel.Controls.Add($labelDefer) - } - If ($showCountdown) { - $flowLayoutPanel.Controls.Add($labelCountdown) - } - - ## Button Close For Me - $buttonCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonCloseApps.Location = '15,0' - $buttonCloseApps.Name = 'buttonCloseApps' - $buttonCloseApps.Size = $buttonSize - $buttonCloseApps.TabIndex = 5 - $buttonCloseApps.Text = $configClosePromptButtonClose - $buttonCloseApps.DialogResult = 'Yes' - $buttonCloseApps.AutoSize = $true - $buttonCloseApps.UseVisualStyleBackColor = $true - $buttonCloseApps.add_Click($buttonCloseApps_OnClick) - - ## Button Defer - $buttonDefer.DataBindings.DefaultDataSourceUpdateMode = 0 - If (-not $showCloseApps) { - $buttonDefer.Location = '15,0' - } - Else { - $buttonDefer.Location = '170,0' - } - $buttonDefer.Name = 'buttonDefer' - $buttonDefer.Size = $buttonSize - $buttonDefer.TabIndex = 6 - $buttonDefer.Text = $configClosePromptButtonDefer - $buttonDefer.DialogResult = 'No' - $buttonDefer.AutoSize = $true - $buttonDefer.UseVisualStyleBackColor = $true - $buttonDefer.add_Click($buttonDefer_OnClick) - - ## Button Continue - $buttonContinue.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonContinue.Location = '325,0' - $buttonContinue.Name = 'buttonContinue' - $buttonContinue.Size = $buttonSize - $buttonContinue.TabIndex = 7 - $buttonContinue.Text = $configClosePromptButtonContinue - $buttonContinue.DialogResult = 'OK' - $buttonContinue.AutoSize = $true - $buttonContinue.UseVisualStyleBackColor = $true - $buttonContinue.add_Click($buttonContinue_OnClick) - If ($showCloseApps) { - # Add tooltip to Continue button - $toolTip.BackColor = [Drawing.Color]::LightGoldenrodYellow - $toolTip.IsBalloon = $false - $toolTip.InitialDelay = 100 - $toolTip.ReshowDelay = 100 - $toolTip.SetToolTip($buttonContinue, $configClosePromptButtonContinueTooltip) - } - - ## Button Abort (Hidden) - $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 - $buttonAbort.Name = 'buttonAbort' - $buttonAbort.Size = '1,1' - $buttonAbort.TabStop = $false - $buttonAbort.DialogResult = 'Abort' - $buttonAbort.TabIndex = 5 - $buttonAbort.UseVisualStyleBackColor = $true - $buttonAbort.add_Click($buttonAbort_OnClick) - - ## Form Welcome - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 0 - $System_Drawing_Size.Width = 0 - $formWelcome.Size = $System_Drawing_Size - $formWelcome.Padding = $paddingNone - $formWelcome.Margin = $paddingNone - $formWelcome.DataBindings.DefaultDataSourceUpdateMode = 0 - $formWelcome.Name = 'WelcomeForm' - $formWelcome.Text = $installTitle - $formWelcome.StartPosition = 'CenterScreen' - $formWelcome.FormBorderStyle = 'FixedDialog' - $formWelcome.MaximizeBox = $false - $formWelcome.MinimizeBox = $false - $formWelcome.TopMost = $TopMost - $formWelcome.TopLevel = $true - $formWelcome.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - $formWelcome.AutoSize = $true - $formWelcome.Controls.Add($pictureBanner) - $formWelcome.Controls.Add($flowLayoutPanel) - - ## Panel Button - $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' - $System_Drawing_Point.X = 0 - # Calculate the position of the panel relative to the size of the form - $System_Drawing_Point.Y = (($formWelcome.Size | Select-Object -ExpandProperty 'Height') - 10) - $panelButtons.Location = $System_Drawing_Point - $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' - $System_Drawing_Size.Height = 40 - $System_Drawing_Size.Width = 450 - $panelButtons.Size = $System_Drawing_Size - $panelButtons.AutoSize = $true - $panelButtons.Anchor = 'Top' - $padding = New-Object -TypeName 'System.Windows.Forms.Padding' - $padding.Top = 0 - $padding.Bottom = 0 - $padding.Left = 0 - $padding.Right = 0 - $panelButtons.Margin = $padding - If ($showCloseApps) { $panelButtons.Controls.Add($buttonCloseApps) } - If ($showDefer) { $panelButtons.Controls.Add($buttonDefer) } - $panelButtons.Controls.Add($buttonContinue) - - ## Add the Buttons Panel to the form - $formWelcome.Controls.Add($panelButtons) - - ## Save the initial state of the form - $formWelcomeWindowState = $formWelcome.WindowState - # Init the OnLoad event to correct the initial state of the form - $formWelcome.add_Load($Form_StateCorrection_Load) - # Clean up the control events - $formWelcome.add_FormClosed($Form_Cleanup_FormClosed) - - Function Update-InstallationWelcome { - $formWelcome.BringToFront() - $formWelcome.Location = "$($formWelcomeStartPosition.X),$($formWelcomeStartPosition.Y)" - $formWelcome.Refresh() - } - - # Function invoked by a timer to periodically check running processes dynamically whilst showing the welcome prompt - Function Get-RunningProcessesDynamically { - $dynamicRunningProcesses = $null - Get-RunningProcesses -ProcessObjects $processObjects -DisableLogging -OutVariable 'dynamicRunningProcesses' - [string]$dynamicRunningProcessDescriptions = ($dynamicRunningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' - If ($dynamicRunningProcessDescriptions -ne $script:runningProcessDescriptions) { - # Update the runningProcessDescriptions variable for the next time this function runs - Set-Variable -Name 'runningProcessDescriptions' -Value $dynamicRunningProcessDescriptions -Force -Scope 'Script' - If ($dynamicrunningProcesses) { - Write-Log -Message "The running processes have changed. Updating the apps to close: [$script:runningProcessDescriptions]..." -Source ${CmdletName} - } - # Update the list box with the processes to close - $listboxCloseApps.Items.Clear() - $script:runningProcessDescriptions -split "," | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } - } - # If CloseApps processes were running when the prompt was shown, and they are subsequently detected to be closed while the form is showing, then close the form. The deferral and CloseApps conditions will be re-evaluated. - If ($ProcessDescriptions) { - If (-not ($dynamicRunningProcesses)) { - Write-Log -Message 'Previously detected running processes are no longer running.' -Source ${CmdletName} - $formWelcome.Dispose() - } - } - # If CloseApps processes were not running when the prompt was shown, and they are subsequently detected to be running while the form is showing, then close the form for relaunch. The deferral and CloseApps conditions will be re-evaluated. - ElseIf (-not $ProcessDescriptions) { - If ($dynamicRunningProcesses) { - Write-Log -Message 'New running processes detected. Updating the form to prompt to close the running applications.' -Source ${CmdletName} - $formWelcome.Dispose() - } - } - } - - ## Minimize all other windows - If ($minimizeWindows) { $null = $shellApp.MinimizeAll() } - - ## Show the form - $result = $formWelcome.ShowDialog() - $formWelcome.Dispose() - - Switch ($result) { - OK { $result = 'Continue' } - No { $result = 'Defer' } - Yes { $result = 'Close' } - Abort { $result = 'Timeout' } - } - - If ($configInstallationWelcomePromptDynamicRunningProcessEvaluation){ - $timerRunningProcesses.Stop() - } - - Write-Output -InputObject $result - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [string]$ProcessDescriptions, + [Parameter(Mandatory=$false)] + [int32]$CloseAppsCountdown, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ForceCloseAppsCountdown, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$PersistPrompt = $false, + [Parameter(Mandatory=$false)] + [switch]$AllowDefer = $false, + [Parameter(Mandatory=$false)] + [string]$DeferTimes, + [Parameter(Mandatory=$false)] + [string]$DeferDeadline, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$MinimizeWindows = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$TopMost = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$ForceCountdown = 0, + [Parameter(Mandatory=$false)] + [switch]$CustomText = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Reset switches + [boolean]$showCloseApps = $false + [boolean]$showDefer = $false + [boolean]$persistWindow = $false + + ## Reset times + [datetime]$startTime = Get-Date + [datetime]$countdownTime = $startTime + + ## Check if the countdown was specified + If ($CloseAppsCountdown) { + If ($CloseAppsCountdown -gt $configInstallationUITimeout) { + Throw 'The close applications countdown time cannot be longer than the timeout specified in the XML configuration for installation UI dialogs to timeout.' + } + } + + ## Initial form layout: Close Applications / Allow Deferral + If ($processDescriptions) { + Write-Log -Message "Prompt user to close application(s) [$processDescriptions]..." -Source ${CmdletName} + $showCloseApps = $true + } + If (($allowDefer) -and (($deferTimes -ge 0) -or ($deferDeadline))) { + Write-Log -Message 'User has the option to defer.' -Source ${CmdletName} + $showDefer = $true + If ($deferDeadline) { + # Remove the Z from universal sortable date time format, otherwise it could be converted to a different time zone + $deferDeadline = $deferDeadline -replace 'Z','' + # Convert the deadline date to a string + [string]$deferDeadline = (Get-Date -Date $deferDeadline).ToString() + } + } + + ## If deferral is being shown and 'close apps countdown' or 'persist prompt' was specified, enable those features. + If (-not $showDefer) { + If ($closeAppsCountdown -gt 0) { + Write-Log -Message "Close applications countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} + $showCountdown = $true + } + } + If ($showDefer) { + If ($persistPrompt) { $persistWindow = $true } + } + ## If 'force close apps countdown' was specified, enable that feature. + If ($forceCloseAppsCountdown -eq $true) { + Write-Log -Message "Close applications countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} + $showCountdown = $true + } + ## If 'force countdown' was specified, enable that feature. + If ($forceCountdown -eq $true) { + Write-Log -Message "Countdown has [$closeAppsCountdown] seconds remaining." -Source ${CmdletName} + $showCountdown = $true + } + + [string[]]$processDescriptions = $processDescriptions -split ',' + [Windows.Forms.Application]::EnableVisualStyles() + + $formWelcome = New-Object -TypeName 'System.Windows.Forms.Form' + $pictureBanner = New-Object -TypeName 'System.Windows.Forms.PictureBox' + $labelAppName = New-Object -TypeName 'System.Windows.Forms.Label' + $labelCountdown = New-Object -TypeName 'System.Windows.Forms.Label' + $labelDefer = New-Object -TypeName 'System.Windows.Forms.Label' + $listBoxCloseApps = New-Object -TypeName 'System.Windows.Forms.ListBox' + $buttonContinue = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonDefer = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonCloseApps = New-Object -TypeName 'System.Windows.Forms.Button' + $buttonAbort = New-Object -TypeName 'System.Windows.Forms.Button' + $formWelcomeWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' + $flowLayoutPanel = New-Object -TypeName 'System.Windows.Forms.FlowLayoutPanel' + $panelButtons = New-Object -TypeName 'System.Windows.Forms.Panel' + $toolTip = New-Object -TypeName 'System.Windows.Forms.ToolTip' + + ## Remove all event handlers from the controls + [scriptblock]$Form_Cleanup_FormClosed = { + Try { + $labelAppName.remove_Click($handler_labelAppName_Click) + $labelDefer.remove_Click($handler_labelDefer_Click) + $buttonCloseApps.remove_Click($buttonCloseApps_OnClick) + $buttonContinue.remove_Click($buttonContinue_OnClick) + $buttonDefer.remove_Click($buttonDefer_OnClick) + $buttonAbort.remove_Click($buttonAbort_OnClick) + $script:welcomeTimer.remove_Tick($timer_Tick) + $timerPersist.remove_Tick($timerPersist_Tick) + $timerRunningProcesses.remove_Tick($timerRunningProcesses_Tick) + $formWelcome.remove_Load($Form_StateCorrection_Load) + $formWelcome.remove_FormClosed($Form_Cleanup_FormClosed) + } + Catch { } + } + + [scriptblock]$Form_StateCorrection_Load = { + ## Correct the initial state of the form to prevent the .NET maximized form issue + $formWelcome.WindowState = 'Normal' + $formWelcome.AutoSize = $true + $formWelcome.TopMost = $TopMost + $formWelcome.BringToFront() + # Get the start position of the form so we can return the form to this position if PersistPrompt is enabled + Set-Variable -Name 'formWelcomeStartPosition' -Value $formWelcome.Location -Scope 'Script' + + ## Initialize the countdown timer + [datetime]$currentTime = Get-Date + [datetime]$countdownTime = $startTime.AddSeconds($CloseAppsCountdown) + $script:welcomeTimer.Start() + + ## Set up the form + [timespan]$remainingTime = $countdownTime.Subtract($currentTime) + [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) + If ($forceCountdown -eq $true) { + switch ($deploymentType){ + 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } + 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeUninstall.ToLower())) + "`n$labelCountdownSeconds" } + 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeRepair.ToLower())) + "`n$labelCountdownSeconds" } + Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $($configDeploymentTypeInstall.ToLower())) + "`n$labelCountdownSeconds" } + } + } + Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } + } + + ## Add the timer if it doesn't already exist - this avoids the timer being reset if the continue button is clicked + If (-not ($script:welcomeTimer)) { + $script:welcomeTimer = New-Object -TypeName 'System.Windows.Forms.Timer' + } + + If ($showCountdown) { + [scriptblock]$timer_Tick = { + ## Get the time information + [datetime]$currentTime = Get-Date + [datetime]$countdownTime = $startTime.AddSeconds($CloseAppsCountdown) + [timespan]$remainingTime = $countdownTime.Subtract($currentTime) + Set-Variable -Name 'closeAppsCountdownGlobal' -Value $remainingTime.TotalSeconds -Scope 'Script' + + ## If the countdown is complete, close the application(s) or continue + If ($countdownTime -le $currentTime) { + If ($forceCountdown -eq $true) { + Write-Log -Message 'Countdown timer has elapsed. Force continue.' -Source ${CmdletName} + $buttonContinue.PerformClick() + } + Else { + Write-Log -Message 'Close application(s) countdown timer has elapsed. Force closing application(s).' -Source ${CmdletName} + If ($buttonCloseApps.CanFocus) { $buttonCloseApps.PerformClick() } + Else { $buttonContinue.PerformClick() } + } + } + Else { + # Update the form + [string]$labelCountdownSeconds = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) + If ($forceCountdown -eq $true) { + switch ($deploymentType){ + 'Install' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } + 'Uninstall' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeUninstall) + "`n$labelCountdownSeconds" } + 'Repair' { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeRepair) + "`n$labelCountdownSeconds" } + Default { $labelCountdown.Text = ($configWelcomePromptCountdownMessage -f $configDeploymentTypeInstall) + "`n$labelCountdownSeconds" } + } + } + Else { $labelCountdown.Text = "$configClosePromptCountdownMessage`n$labelCountdownSeconds" } + [Windows.Forms.Application]::DoEvents() + } + } + } + Else { + $script:welcomeTimer.Interval = ($configInstallationUITimeout * 1000) + [scriptblock]$timer_Tick = { $buttonAbort.PerformClick() } + } + + $script:welcomeTimer.add_Tick($timer_Tick) + + ## Persistence Timer + If ($persistWindow) { + $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' + $timerPersist.Interval = ($configInstallationPersistInterval * 1000) + [scriptblock]$timerPersist_Tick = { Update-InstallationWelcome } + $timerPersist.add_Tick($timerPersist_Tick) + $timerPersist.Start() + } + + ## Process Re-Enumeration Timer + If ($configInstallationWelcomePromptDynamicRunningProcessEvaluation) { + $timerRunningProcesses = New-Object -TypeName 'System.Windows.Forms.Timer' + $timerRunningProcesses.Interval = ($configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval * 1000) + [scriptblock]$timerRunningProcesses_Tick = { try { Get-RunningProcessesDynamically } catch {} } + $timerRunningProcesses.add_Tick($timerRunningProcesses_Tick) + $timerRunningProcesses.Start() + } + + ## Form + $formWelcome.Controls.Add($pictureBanner) + $formWelcome.Controls.Add($buttonAbort) + + ##---------------------------------------------- + ## Create zero px padding object + $paddingNone = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,0 + ## Create basic control size + $defaultControlSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,0 + + ## Generic Button properties + $buttonSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 110,24 + + ## Picture Banner + $pictureBanner.DataBindings.DefaultDataSourceUpdateMode = 0 + $pictureBanner.ImageLocation = $appDeployLogoBanner + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,0 + $pictureBanner.Location = $System_Drawing_Point + $pictureBanner.Name = 'pictureBanner' + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,$appDeployLogoBannerHeight + $pictureBanner.Size = $System_Drawing_Size + $pictureBanner.SizeMode = 'CenterImage' + $pictureBanner.Margin = $paddingNone + $pictureBanner.TabIndex = 0 + $pictureBanner.TabStop = $false + + ## Label App Name + $labelAppName.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelAppName.Name = 'labelAppName' + $labelAppName.Size = $defaultControlSize + $labelAppName.MinimumSize = $defaultControlSize + $labelAppName.MaximumSize = $defaultControlSize + $labelAppName.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,5,0,5 + $labelAppName.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelAppName.TabIndex = 1 + + ## Initial form layout: Close Applications / Allow Deferral + $labelAppNameText = "$configDeferPromptWelcomeMessage`n`n$installTitle" + If ($showCloseApps) { + $labelAppNameText = "$labelAppNameText`n`n$configClosePromptMessage" + } + If ($CustomText -and $configWelcomePromptCustomMessage) { + $labelAppNameText = "$labelAppNameText`n`n$configWelcomePromptCustomMessage" + } + $labelAppName.Text = $labelAppNameText + $labelAppName.TextAlign = 'MiddleCenter' + $labelAppName.Anchor = 'Top' + $labelAppName.AutoSize = $true + $labelAppName.add_Click($handler_labelAppName_Click) + + ## Listbox Close Applications + $listBoxCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 + $listBoxCloseApps.FormattingEnabled = $true + $listBoxCloseApps.HorizontalScrollbar = $true + $listBoxCloseApps.Name = 'listBoxCloseApps' + $System_Drawing_Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 400,100 + $listBoxCloseApps.Size = $System_Drawing_Size + $listBoxCloseApps.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 25,0,0,0 + $listBoxCloseApps.TabIndex = 3 + $ProcessDescriptions | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } + + ## Label Defer + $labelDefer.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelDefer.Name = 'labelDefer' + $labelDefer.Size = $defaultControlSize + $labelDefer.MinimumSize = $defaultControlSize + $labelDefer.MaximumSize = $defaultControlSize + $labelDefer.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,5 + $labelDefer.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelDefer.TabIndex = 4 + $deferralText = "$configDeferPromptExpiryMessage`n" + + If ($deferTimes -ge 0) { + $deferralText = "$deferralText `n$configDeferPromptRemainingDeferrals $([int32]$deferTimes + 1)" + } + If ($deferDeadline) { + $deferralText = "$deferralText `n$configDeferPromptDeadline $deferDeadline" + } + If (($deferTimes -lt 0) -and (-not $DeferDeadline)) { + $deferralText = "$deferralText `n$configDeferPromptNoDeadline" + } + $deferralText = "$deferralText `n`n$configDeferPromptWarningMessage" + $labelDefer.Text = $deferralText + $labelDefer.TextAlign = 'MiddleCenter' + $labelDefer.AutoSize = $true + $labelDefer.add_Click($handler_labelDefer_Click) + + ## Label Countdown + $labelCountdown.DataBindings.DefaultDataSourceUpdateMode = 0 + $labelCountdown.Name = 'labelCountdown' + $labelCountdown.Size = $defaultControlSize + $labelCountdown.MinimumSize = $defaultControlSize + $labelCountdown.MaximumSize = $defaultControlSize + $labelCountdown.Margin = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 0,0,0,5 + $labelCountdown.Padding = New-Object -TypeName 'System.Windows.Forms.Padding' -ArgumentList 10,0,10,0 + $labelCountdown.TabIndex = 4 + $labelCountdown.Font = New-Object -TypeName "System.Drawing.Font" -ArgumentList $labelCountdown.Font,1 + $labelCountdown.Text = '00:00:00' + $labelCountdown.TextAlign = 'MiddleCenter' + $labelCountdown.AutoSize = $true + $labelCountdown.add_Click($handler_labelDefer_Click) + + ## Panel Flow Layout + $System_Drawing_Point = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 0,$appDeployLogoBannerHeight + $flowLayoutPanel.Location = $System_Drawing_Point + $flowLayoutPanel.Margin = $paddingNone + $flowLayoutPanel.AutoSize = $true + $flowLayoutPanel.Anchor = 'Top' + $flowLayoutPanel.FlowDirection = 'TopDown' + $flowLayoutPanel.WrapContents = $true + $flowLayoutPanel.Controls.Add($labelAppName) + If ($showCloseApps) { $flowLayoutPanel.Controls.Add($listBoxCloseApps) } + If ($showDefer) { $flowLayoutPanel.Controls.Add($labelDefer) } + If ($showCountdown) { $flowLayoutPanel.Controls.Add($labelCountdown) } + + ## Button Close For Me + $buttonCloseApps.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonCloseApps.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 15,5 + $buttonCloseApps.Name = 'buttonCloseApps' + $buttonCloseApps.Size = $buttonSize + $buttonCloseApps.TabIndex = 5 + $buttonCloseApps.Text = $configClosePromptButtonClose + $buttonCloseApps.DialogResult = 'Yes' + $buttonCloseApps.AutoSize = $true + $buttonCloseApps.UseVisualStyleBackColor = $true + $buttonCloseApps.add_Click($buttonCloseApps_OnClick) + + ## Button Defer + $buttonDefer.DataBindings.DefaultDataSourceUpdateMode = 0 + If (-not $showCloseApps) { + $buttonDefer.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 15,5 + } + Else { + $buttonDefer.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 170,5 + } + $buttonDefer.Name = 'buttonDefer' + $buttonDefer.Size = $buttonSize + $buttonDefer.TabIndex = 6 + $buttonDefer.Text = $configClosePromptButtonDefer + $buttonDefer.DialogResult = 'No' + $buttonDefer.AutoSize = $true + $buttonDefer.UseVisualStyleBackColor = $true + $buttonDefer.add_Click($buttonDefer_OnClick) + + ## Button Continue + $buttonContinue.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonContinue.Location = New-Object -TypeName 'System.Drawing.Point' -ArgumentList 325,5 + $buttonContinue.Name = 'buttonContinue' + $buttonContinue.Size = $buttonSize + $buttonContinue.TabIndex = 7 + $buttonContinue.Text = $configClosePromptButtonContinue + $buttonContinue.DialogResult = 'OK' + $buttonContinue.AutoSize = $true + $buttonContinue.UseVisualStyleBackColor = $true + $buttonContinue.add_Click($buttonContinue_OnClick) + If ($showCloseApps) { + # Add tooltip to Continue button + $toolTip.BackColor = [Drawing.Color]::LightGoldenrodYellow + $toolTip.IsBalloon = $false + $toolTip.InitialDelay = 100 + $toolTip.ReshowDelay = 100 + $toolTip.SetToolTip($buttonContinue, $configClosePromptButtonContinueTooltip) + } + + ## Button Abort (Hidden) + $buttonAbort.DataBindings.DefaultDataSourceUpdateMode = 0 + $buttonAbort.Name = 'buttonAbort' + $buttonAbort.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 1,1 + $buttonAbort.TabStop = $false + $buttonAbort.DialogResult = 'Abort' + $buttonAbort.TabIndex = 5 + $buttonAbort.Visible = $false + $buttonAbort.UseVisualStyleBackColor = $true + $buttonAbort.add_Click($buttonAbort_OnClick) + + ## Form Welcome + $formWelcome.Size = $defaultControlSize + $formWelcome.MinimumSize = $defaultControlSize + $formWelcome.Padding = $paddingNone + $formWelcome.Margin = $paddingNone + $formWelcome.DataBindings.DefaultDataSourceUpdateMode = 0 + $formWelcome.Name = 'WelcomeForm' + $formWelcome.Text = $installTitle + $formWelcome.StartPosition = 'CenterScreen' + $formWelcome.FormBorderStyle = 'FixedDialog' + $formWelcome.MaximizeBox = $false + $formWelcome.MinimizeBox = $false + $formWelcome.TopMost = $TopMost + $formWelcome.TopLevel = $true + $formWelcome.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + $formWelcome.AutoSize = $true + $formWelcome.Controls.Add($pictureBanner) + + ## Panel Button + $panelButtons.MinimumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.Size = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.MaximumSize = New-Object -TypeName 'System.Drawing.Size' -ArgumentList 450,34 + $panelButtons.AutoSize = $true + $panelButtons.Padding = $paddingNone + $panelButtons.Margin = $paddingNone + If ($showCloseApps) { $panelButtons.Controls.Add($buttonCloseApps) } + If ($showDefer) { $panelButtons.Controls.Add($buttonDefer) } + $panelButtons.Controls.Add($buttonContinue) + + ## Add the Buttons Panel to the flowPanel + $flowLayoutPanel.Controls.Add($panelButtons) + ## Add FlowPanel to the form + $formWelcome.Controls.Add($flowLayoutPanel) + + ## Save the initial state of the form + $formWelcomeWindowState = $formWelcome.WindowState + # Init the OnLoad event to correct the initial state of the form + $formWelcome.add_Load($Form_StateCorrection_Load) + # Clean up the control events + $formWelcome.add_FormClosed($Form_Cleanup_FormClosed) + + Function Update-InstallationWelcome { + $formWelcome.BringToFront() + $formWelcome.Location = "$($formWelcomeStartPosition.X),$($formWelcomeStartPosition.Y)" + $formWelcome.Refresh() + } + + # Function invoked by a timer to periodically check running processes dynamically whilst showing the welcome prompt + Function Get-RunningProcessesDynamically { + $dynamicRunningProcesses = $null + Get-RunningProcesses -ProcessObjects $processObjects -DisableLogging -OutVariable 'dynamicRunningProcesses' + [string]$dynamicRunningProcessDescriptions = ($dynamicRunningProcesses | Where-Object { $_.ProcessDescription } | Select-Object -ExpandProperty 'ProcessDescription' | Select-Object -Unique | Sort-Object) -join ',' + If ($dynamicRunningProcessDescriptions -ne $script:runningProcessDescriptions) { + # Update the runningProcessDescriptions variable for the next time this function runs + Set-Variable -Name 'runningProcessDescriptions' -Value $dynamicRunningProcessDescriptions -Force -Scope 'Script' + If ($dynamicrunningProcesses) { + Write-Log -Message "The running processes have changed. Updating the apps to close: [$script:runningProcessDescriptions]..." -Source ${CmdletName} + } + # Update the list box with the processes to close + $listboxCloseApps.Items.Clear() + $script:runningProcessDescriptions -split "," | ForEach-Object { $null = $listboxCloseApps.Items.Add($_) } + } + # If CloseApps processes were running when the prompt was shown, and they are subsequently detected to be closed while the form is showing, then close the form. The deferral and CloseApps conditions will be re-evaluated. + If ($ProcessDescriptions) { + If (-not ($dynamicRunningProcesses)) { + Write-Log -Message 'Previously detected running processes are no longer running.' -Source ${CmdletName} + $formWelcome.Dispose() + } + } + # If CloseApps processes were not running when the prompt was shown, and they are subsequently detected to be running while the form is showing, then close the form for relaunch. The deferral and CloseApps conditions will be re-evaluated. + ElseIf (-not $ProcessDescriptions) { + If ($dynamicRunningProcesses) { + Write-Log -Message 'New running processes detected. Updating the form to prompt to close the running applications.' -Source ${CmdletName} + $formWelcome.Dispose() + } + } + } + + ## Minimize all other windows + If ($minimizeWindows) { $null = $shellApp.MinimizeAll() } + + ## Show the form + $result = $formWelcome.ShowDialog() + $formWelcome.Dispose() + + Switch ($result) { + OK { $result = 'Continue' } + No { $result = 'Defer' } + Yes { $result = 'Close' } + Abort { $result = 'Timeout' } + } + + If ($configInstallationWelcomePromptDynamicRunningProcessEvaluation){ + $timerRunningProcesses.Stop() + } + + Write-Output -InputObject $result + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -6884,319 +7048,342 @@ Function Show-WelcomePrompt { Function Show-InstallationRestartPrompt { <# .SYNOPSIS - Displays a restart prompt with a countdown to a forced restart. + Displays a restart prompt with a countdown to a forced restart. .DESCRIPTION - Displays a restart prompt with a countdown to a forced restart. + Displays a restart prompt with a countdown to a forced restart. .PARAMETER CountdownSeconds - Specifies the number of seconds to countdown before the system restart. + Specifies the number of seconds to countdown before the system restart. Default: 60 .PARAMETER CountdownNoHideSeconds - Specifies the number of seconds to display the restart prompt without allowing the window to be hidden. + Specifies the number of seconds to display the restart prompt without allowing the window to be hidden. Default: 30 +.PARAMETER NoSilentRestart + Specifies whether the restart should be triggered when Deploy mode is silent or very silent. Default: $true .PARAMETER NoCountdown - Specifies not to show a countdown, just the Restart Now and Restart Later buttons. - The UI will restore/reposition itself persistently based on the interval value specified in the config file. + Specifies not to show a countdown, just the Restart Now and Restart Later buttons. + The UI will restore/reposition itself persistently based on the interval value specified in the config file. +.PARAMETER SilentCountdownSeconds + Specifies number of seconds to countdown for the restart when the toolkit is running in silent mode and NoSilentRestart is $false. Default: 5 +.EXAMPLE + Show-InstallationRestartPrompt -Countdownseconds 600 -CountdownNoHideSeconds 60 .EXAMPLE - Show-InstallationRestartPrompt -Countdownseconds 600 -CountdownNoHideSeconds 60 + Show-InstallationRestartPrompt -NoCountdown .EXAMPLE - Show-InstallationRestartPrompt -NoCountdown + Show-InstallationRestartPrompt -Countdownseconds 300 -NoSilentRestart $false -SilentCountdownSeconds 10 .NOTES + Be mindful of the countdown you specify for the reboot as code directly after this function might NOT be able to execute - that includes logging. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$CountdownSeconds = 60, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$CountdownNoHideSeconds = 30, - [Parameter(Mandatory=$false)] - [switch]$NoCountdown = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Bypass if in non-interactive mode - If ($deployModeSilent) { - Write-Log -Message "Bypass Installation Restart Prompt [Mode: $deployMode]." -Source ${CmdletName} - Return - } - ## Get the parameters passed to the function for invoking the function asynchronously - [hashtable]$installRestartPromptParameters = $psBoundParameters - - ## Check if we are already displaying a restart prompt - If (Get-Process | Where-Object { $_.MainWindowTitle -match $configRestartPromptTitle }) { - Write-Log -Message "${CmdletName} was invoked, but an existing restart prompt was detected. Cancelling restart prompt." -Severity 2 -Source ${CmdletName} - Return - } - - [datetime]$startTime = Get-Date - [datetime]$countdownTime = $startTime - - [Windows.Forms.Application]::EnableVisualStyles() - $formRestart = New-Object -TypeName 'System.Windows.Forms.Form' - $labelCountdown = New-Object -TypeName 'System.Windows.Forms.Label' - $labelTimeRemaining = New-Object -TypeName 'System.Windows.Forms.Label' - $labelMessage = New-Object -TypeName 'System.Windows.Forms.Label' - $buttonRestartLater = New-Object -TypeName 'System.Windows.Forms.Button' - $picturebox = New-Object -TypeName 'System.Windows.Forms.PictureBox' - $buttonRestartNow = New-Object -TypeName 'System.Windows.Forms.Button' - $timerCountdown = New-Object -TypeName 'System.Windows.Forms.Timer' - $InitialFormWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' - - [scriptblock]$RestartComputer = { - Write-Log -Message 'Force restart the computer...' -Source ${CmdletName} - Restart-Computer -Force - } - - [scriptblock]$FormEvent_Load = { - ## Initialize the countdown timer - [datetime]$currentTime = Get-Date - [datetime]$countdownTime = $startTime.AddSeconds($countdownSeconds) - $timerCountdown.Start() - ## Set up the form - [timespan]$remainingTime = $countdownTime.Subtract($currentTime) - $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($remainingTime.TotalSeconds -le $countdownNoHideSeconds) { $buttonRestartLater.Enabled = $false } - $formRestart.WindowState = 'Normal' - $formRestart.TopMost = $true - $formRestart.BringToFront() - } - - [scriptblock]$Form_StateCorrection_Load = { - ## Correct the initial state of the form to prevent the .NET maximized form issue - $formRestart.WindowState = $InitialFormWindowState - $formRestart.AutoSize = $true - $formRestart.TopMost = $true - $formRestart.BringToFront() - ## Get the start position of the form so we can return the form to this position if PersistPrompt is enabled - Set-Variable -Name 'formInstallationRestartPromptStartPosition' -Value $formRestart.Location -Scope 'Script' - } - - ## Persistence Timer - If ($NoCountdown) { - $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' - $timerPersist.Interval = ($configInstallationRestartPersistInterval * 1000) - [scriptblock]$timerPersist_Tick = { - # Show the Restart Popup - $formRestart.WindowState = 'Normal' - $formRestart.TopMost = $true - $formRestart.BringToFront() - $formRestart.Location = "$($formInstallationRestartPromptStartPosition.X),$($formInstallationRestartPromptStartPosition.Y)" - $formRestart.Refresh() - [Windows.Forms.Application]::DoEvents() - } - $timerPersist.add_Tick($timerPersist_Tick) - $timerPersist.Start() - } - - [scriptblock]$buttonRestartLater_Click = { - ## Minimize the form - $formRestart.WindowState = 'Minimized' - If ($NoCountdown) { - ## Reset the persistence timer - $timerPersist.Stop() - $timerPersist.Start() - } - } - - ## Restart the computer - [scriptblock]$buttonRestartNow_Click = { & $RestartComputer } - - ## Hide the form if minimized - [scriptblock]$formRestart_Resize = { If ($formRestart.WindowState -eq 'Minimized') { $formRestart.WindowState = 'Minimized' } } - - [scriptblock]$timerCountdown_Tick = { - ## Get the time information - [datetime]$currentTime = Get-Date - [datetime]$countdownTime = $startTime.AddSeconds($countdownSeconds) - [timespan]$remainingTime = $countdownTime.Subtract($currentTime) - ## If the countdown is complete, restart the machine - If ($countdownTime -lt $currentTime) { - $buttonRestartNow.PerformClick() - } - Else { - ## Update the form - $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) - If ($remainingTime.TotalSeconds -le $countdownNoHideSeconds) { - $buttonRestartLater.Enabled = $false - # If the form is hidden when we hit the "No Hide", bring it back up - If ($formRestart.WindowState -eq 'Minimized') { - # Show Popup - $formRestart.WindowState = 'Normal' - $formRestart.TopMost = $true - $formRestart.BringToFront() - $formRestart.Location = "$($formInstallationRestartPromptStartPosition.X),$($formInstallationRestartPromptStartPosition.Y)" - $formRestart.Refresh() - [Windows.Forms.Application]::DoEvents() - } - } - [Windows.Forms.Application]::DoEvents() - } - } - - ## Remove all event handlers from the controls - [scriptblock]$Form_Cleanup_FormClosed = { - Try { - $buttonRestartLater.remove_Click($buttonRestartLater_Click) - $buttonRestartNow.remove_Click($buttonRestartNow_Click) - $formRestart.remove_Load($FormEvent_Load) - $formRestart.remove_Resize($formRestart_Resize) - $timerCountdown.remove_Tick($timerCountdown_Tick) - $timerPersist.remove_Tick($timerPersist_Tick) - $formRestart.remove_Load($Form_StateCorrection_Load) - $formRestart.remove_FormClosed($Form_Cleanup_FormClosed) - } - Catch { } - } - - ## Form - If (-not $NoCountdown) { - $formRestart.Controls.Add($labelCountdown) - $formRestart.Controls.Add($labelTimeRemaining) - } - $formRestart.Controls.Add($labelMessage) - $formRestart.Controls.Add($buttonRestartLater) - $formRestart.Controls.Add($picturebox) - $formRestart.Controls.Add($buttonRestartNow) - $clientSizeY = 260 + $appDeployLogoBannerHeightDifference - $formRestart.ClientSize = "450,$clientSizeY" - $formRestart.ControlBox = $false - $formRestart.FormBorderStyle = 'FixedDialog' - $formRestart.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - $formRestart.MaximizeBox = $false - $formRestart.MinimizeBox = $false - $formRestart.Name = 'formRestart' - $formRestart.StartPosition = 'CenterScreen' - $formRestart.Text = "$($configRestartPromptTitle): $installTitle" - $formRestart.add_Load($FormEvent_Load) - $formRestart.add_Resize($formRestart_Resize) - - ## Banner - $picturebox.Anchor = 'Top' - $picturebox.Image = [Drawing.Image]::Fromfile($AppDeployLogoBanner) - $picturebox.Location = '0,0' - $picturebox.Name = 'picturebox' - $pictureboxSizeY = $appDeployLogoBannerHeight - $picturebox.Size = "450,$pictureboxSizeY" - $picturebox.SizeMode = 'CenterImage' - $picturebox.TabIndex = 1 - $picturebox.TabStop = $false - - ## Label Message - $labelMessageLocationY = 58 + $appDeployLogoBannerHeightDifference - $labelMessage.Location = "20,$labelMessageLocationY" - $labelMessage.Name = 'labelMessage' - $labelMessage.Size = '400,79' - $labelMessage.TabIndex = 3 - $labelMessage.Text = "$configRestartPromptMessage $configRestartPromptMessageTime `n`n$configRestartPromptMessageRestart" - If ($NoCountdown) { $labelMessage.Text = $configRestartPromptMessage } - $labelMessage.TextAlign = 'MiddleCenter' - - ## Label Time Remaining - $labelTimeRemainingLocationY = 138 + $appDeployLogoBannerHeightDifference - $labelTimeRemaining.Location = "20,$labelTimeRemainingLocationY" - $labelTimeRemaining.Name = 'labelTimeRemaining' - $labelTimeRemaining.Size = '400,23' - $labelTimeRemaining.TabIndex = 4 - $labelTimeRemaining.Text = $configRestartPromptTimeRemaining - $labelTimeRemaining.TextAlign = 'MiddleCenter' - - ## Label Countdown - $labelCountdown.Font = 'Microsoft Sans Serif, 18pt, style=Bold' - $labelCountdownLocationY = 165 + $appDeployLogoBannerHeightDifference - $labelCountdown.Location = "20,$labelCountdownLocationY" - $labelCountdown.Name = 'labelCountdown' - $labelCountdown.Size = '400,30' - $labelCountdown.TabIndex = 5 - $labelCountdown.Text = '00:00:00' - $labelCountdown.TextAlign = 'MiddleCenter' - - # Generic Y location for buttons - $buttonsLocationY = 216 + $appDeployLogoBannerHeightDifference - - ## Label Restart Later - $buttonRestartLater.Anchor = 'Bottom,Left' - $buttonRestartLater.Location = "20,$buttonsLocationY" - $buttonRestartLater.Name = 'buttonRestartLater' - $buttonRestartLater.Size = '159,23' - $buttonRestartLater.TabIndex = 0 - $buttonRestartLater.Text = $configRestartPromptButtonRestartLater - $buttonRestartLater.UseVisualStyleBackColor = $true - $buttonRestartLater.add_Click($buttonRestartLater_Click) - - ## Label Restart Now - $buttonRestartNow.Anchor = 'Bottom,Right' - $buttonRestartNow.Location = "265,$buttonsLocationY" - $buttonRestartNow.Name = 'buttonRestartNow' - $buttonRestartNow.Size = '159,23' - $buttonRestartNow.TabIndex = 2 - $buttonRestartNow.Text = $configRestartPromptButtonRestartNow - $buttonRestartNow.UseVisualStyleBackColor = $true - $buttonRestartNow.add_Click($buttonRestartNow_Click) - - ## Timer Countdown - If (-not $NoCountdown) { $timerCountdown.add_Tick($timerCountdown_Tick) } - - ##---------------------------------------------- - - ## Save the initial state of the form - $InitialFormWindowState = $formRestart.WindowState - # Init the OnLoad event to correct the initial state of the form - $formRestart.add_Load($Form_StateCorrection_Load) - # Clean up the control events - $formRestart.add_FormClosed($Form_Cleanup_FormClosed) - $formRestartClosing = [Windows.Forms.FormClosingEventHandler]{ If ($_.CloseReason -eq 'UserClosing') { $_.Cancel = $true } } - $formRestart.add_FormClosing($formRestartClosing) - - ## If the script has been dot-source invoked by the deploy app script, display the restart prompt asynchronously - If ($deployAppScriptFriendlyName) { - If ($NoCountdown) { - Write-Log -Message "Invoking ${CmdletName} asynchronously with no countdown..." -Source ${CmdletName} - } - Else { - Write-Log -Message "Invoking ${CmdletName} asynchronously with a [$countDownSeconds] second countdown..." -Source ${CmdletName} - } - [string]$installRestartPromptParameters = ($installRestartPromptParameters.GetEnumerator() | ForEach-Object { - If ($_.Value.GetType().Name -eq 'SwitchParameter') { - "-$($_.Key)" - } - ElseIf ($_.Value.GetType().Name -eq 'Boolean') { - "-$($_.Key) `$" + "$($_.Value)".ToLower() - } - ElseIf ($_.Value.GetType().Name -eq 'Int32') { - "-$($_.Key) $($_.Value)" - } - Else { - "-$($_.Key) `"$($_.Value)`"" - } - }) -join ' ' - Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$scriptPath`" -ReferredInstallTitle `"$installTitle`" -ReferredInstallName `"$installName`" -ReferredLogName `"$logName`" -ShowInstallationRestartPrompt $installRestartPromptParameters -AsyncToolkitLaunch" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' - } - Else { - If ($NoCountdown) { - Write-Log -Message 'Display restart prompt with no countdown.' -Source ${CmdletName} - } - Else { - Write-Log -Message "Display restart prompt with a [$countDownSeconds] second countdown." -Source ${CmdletName} - } - - # Show the Form - Write-Output -InputObject $formRestart.ShowDialog() - $formRestart.Dispose() - - # Activate the Window - [Diagnostics.Process]$powershellProcess = Get-Process | Where-Object { $_.MainWindowTitle -match $installTitle } - [Microsoft.VisualBasic.Interaction]::AppActivate($powershellProcess.ID) - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$CountdownSeconds = 60, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$CountdownNoHideSeconds = 30, + [Parameter(Mandatory=$false)] + [bool]$NoSilentRestart = $true, + [Parameter(Mandatory=$false)] + [switch]$NoCountdown = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$SilentCountdownSeconds = 5 + ) + + 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 in non-interactive mode + If ($deployModeSilent) { + If ($NoSilentRestart -eq $false) { + Write-Log -Message "Triggering restart silently, because the deploy mode is set to [$deployMode] and [NoSilentRestart] is disabled. Timeout is set to [$SilentCountdownSeconds] seconds." -Source ${CmdletName} + Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command `"& {Start-Sleep -Seconds $SilentCountdownSeconds;Restart-Computer -Force;}`"" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' + } + Else { + Write-Log -Message "Skipping restart, because the deploy mode is set to [$deployMode] and [NoSilentRestart] is enabled." -Source ${CmdletName} + } + Return + } + ## Get the parameters passed to the function for invoking the function asynchronously + [hashtable]$installRestartPromptParameters = $psBoundParameters + + ## Check if we are already displaying a restart prompt + If (Get-Process | Where-Object { $_.MainWindowTitle -match $configRestartPromptTitle }) { + Write-Log -Message "${CmdletName} was invoked, but an existing restart prompt was detected. Cancelling restart prompt." -Severity 2 -Source ${CmdletName} + Return + } + + [datetime]$startTime = Get-Date + [datetime]$countdownTime = $startTime + + [Windows.Forms.Application]::EnableVisualStyles() + $formRestart = New-Object -TypeName 'System.Windows.Forms.Form' + $labelCountdown = New-Object -TypeName 'System.Windows.Forms.Label' + $labelTimeRemaining = New-Object -TypeName 'System.Windows.Forms.Label' + $labelMessage = New-Object -TypeName 'System.Windows.Forms.Label' + $buttonRestartLater = New-Object -TypeName 'System.Windows.Forms.Button' + $picturebox = New-Object -TypeName 'System.Windows.Forms.PictureBox' + $buttonRestartNow = New-Object -TypeName 'System.Windows.Forms.Button' + $timerCountdown = New-Object -TypeName 'System.Windows.Forms.Timer' + $InitialFormWindowState = New-Object -TypeName 'System.Windows.Forms.FormWindowState' + + [scriptblock]$RestartComputer = { + Write-Log -Message 'Force restart the computer...' -Source ${CmdletName} + Restart-Computer -Force + } + + [scriptblock]$FormEvent_Load = { + ## Initialize the countdown timer + [datetime]$currentTime = Get-Date + [datetime]$countdownTime = $startTime.AddSeconds($countdownSeconds) + $timerCountdown.Start() + ## Set up the form + [timespan]$remainingTime = $countdownTime.Subtract($currentTime) + $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) + If ($remainingTime.TotalSeconds -le $countdownNoHideSeconds) { $buttonRestartLater.Enabled = $false } + $formRestart.WindowState = 'Normal' + $formRestart.TopMost = $true + $formRestart.BringToFront() + } + + [scriptblock]$Form_StateCorrection_Load = { + ## Correct the initial state of the form to prevent the .NET maximized form issue + $formRestart.WindowState = $InitialFormWindowState + $formRestart.AutoSize = $true + $formRestart.TopMost = $true + $formRestart.BringToFront() + ## Get the start position of the form so we can return the form to this position if PersistPrompt is enabled + Set-Variable -Name 'formInstallationRestartPromptStartPosition' -Value $formRestart.Location -Scope 'Script' + } + + ## Persistence Timer + If ($NoCountdown) { + $timerPersist = New-Object -TypeName 'System.Windows.Forms.Timer' + $timerPersist.Interval = ($configInstallationRestartPersistInterval * 1000) + [scriptblock]$timerPersist_Tick = { + # Show the Restart Popup + $formRestart.WindowState = 'Normal' + $formRestart.TopMost = $true + $formRestart.BringToFront() + $formRestart.Location = "$($formInstallationRestartPromptStartPosition.X),$($formInstallationRestartPromptStartPosition.Y)" + $formRestart.Refresh() + [Windows.Forms.Application]::DoEvents() + } + $timerPersist.add_Tick($timerPersist_Tick) + $timerPersist.Start() + } + + [scriptblock]$buttonRestartLater_Click = { + ## Minimize the form + $formRestart.WindowState = 'Minimized' + If ($NoCountdown) { + ## Reset the persistence timer + $timerPersist.Stop() + $timerPersist.Start() + } + } + + ## Restart the computer + [scriptblock]$buttonRestartNow_Click = { & $RestartComputer } + + ## Hide the form if minimized + [scriptblock]$formRestart_Resize = { If ($formRestart.WindowState -eq 'Minimized') { $formRestart.WindowState = 'Minimized' } } + + [scriptblock]$timerCountdown_Tick = { + ## Get the time information + [datetime]$currentTime = Get-Date + [datetime]$countdownTime = $startTime.AddSeconds($countdownSeconds) + [timespan]$remainingTime = $countdownTime.Subtract($currentTime) + ## If the countdown is complete, restart the machine + If ($countdownTime -lt $currentTime) { + $buttonRestartNow.PerformClick() + } + Else { + ## Update the form + $labelCountdown.Text = [string]::Format('{0}:{1:d2}:{2:d2}', $remainingTime.Days * 24 + $remainingTime.Hours, $remainingTime.Minutes, $remainingTime.Seconds) + If ($remainingTime.TotalSeconds -le $countdownNoHideSeconds) { + $buttonRestartLater.Enabled = $false + # If the form is hidden when we hit the "No Hide", bring it back up + If ($formRestart.WindowState -eq 'Minimized') { + # Show Popup + $formRestart.WindowState = 'Normal' + $formRestart.TopMost = $true + $formRestart.BringToFront() + $formRestart.Location = "$($formInstallationRestartPromptStartPosition.X),$($formInstallationRestartPromptStartPosition.Y)" + $formRestart.Refresh() + [Windows.Forms.Application]::DoEvents() + } + } + [Windows.Forms.Application]::DoEvents() + } + } + + ## Remove all event handlers from the controls + [scriptblock]$Form_Cleanup_FormClosed = { + Try { + $buttonRestartLater.remove_Click($buttonRestartLater_Click) + $buttonRestartNow.remove_Click($buttonRestartNow_Click) + $formRestart.remove_Load($FormEvent_Load) + $formRestart.remove_Resize($formRestart_Resize) + $timerCountdown.remove_Tick($timerCountdown_Tick) + $timerPersist.remove_Tick($timerPersist_Tick) + $formRestart.remove_Load($Form_StateCorrection_Load) + $formRestart.remove_FormClosed($Form_Cleanup_FormClosed) + } + Catch { } + } + + ## Form + If (-not $NoCountdown) { + $formRestart.Controls.Add($labelCountdown) + $formRestart.Controls.Add($labelTimeRemaining) + } + $formRestart.Controls.Add($labelMessage) + $formRestart.Controls.Add($buttonRestartLater) + $formRestart.Controls.Add($picturebox) + $formRestart.Controls.Add($buttonRestartNow) + $clientSizeY = 260 + $appDeployLogoBannerHeightDifference + $formRestart.ClientSize = "450,$clientSizeY" + $formRestart.ControlBox = $false + $formRestart.FormBorderStyle = 'FixedDialog' + $formRestart.Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + $formRestart.MaximizeBox = $false + $formRestart.MinimizeBox = $false + $formRestart.Name = 'formRestart' + $formRestart.StartPosition = 'CenterScreen' + $formRestart.Text = "$($configRestartPromptTitle): $installTitle" + $formRestart.add_Load($FormEvent_Load) + $formRestart.add_Resize($formRestart_Resize) + + ## Banner + $picturebox.Anchor = 'Top' + $picturebox.Image = [Drawing.Image]::Fromfile($AppDeployLogoBanner) + $picturebox.Location = '0,0' + $picturebox.Name = 'picturebox' + $pictureboxSizeY = $appDeployLogoBannerHeight + $picturebox.Size = "450,$pictureboxSizeY" + $picturebox.SizeMode = 'CenterImage' + $picturebox.TabIndex = 1 + $picturebox.TabStop = $false + + ## Label Message + $labelMessageLocationY = 58 + $appDeployLogoBannerHeightDifference + $labelMessage.Location = "20,$labelMessageLocationY" + $labelMessage.Name = 'labelMessage' + $labelMessage.Size = '400,79' + $labelMessage.TabIndex = 3 + $labelMessage.Text = "$configRestartPromptMessage $configRestartPromptMessageTime `n`n$configRestartPromptMessageRestart" + If ($NoCountdown) { $labelMessage.Text = $configRestartPromptMessage } + $labelMessage.TextAlign = 'MiddleCenter' + + ## Label Time Remaining + $labelTimeRemainingLocationY = 138 + $appDeployLogoBannerHeightDifference + $labelTimeRemaining.Location = "20,$labelTimeRemainingLocationY" + $labelTimeRemaining.Name = 'labelTimeRemaining' + $labelTimeRemaining.Size = '400,23' + $labelTimeRemaining.TabIndex = 4 + $labelTimeRemaining.Text = $configRestartPromptTimeRemaining + $labelTimeRemaining.TextAlign = 'MiddleCenter' + + ## Label Countdown + $labelCountdown.Font = 'Microsoft Sans Serif, 18pt, style=Bold' + $labelCountdownLocationY = 165 + $appDeployLogoBannerHeightDifference + $labelCountdown.Location = "20,$labelCountdownLocationY" + $labelCountdown.Name = 'labelCountdown' + $labelCountdown.Size = '400,30' + $labelCountdown.TabIndex = 5 + $labelCountdown.Text = '00:00:00' + $labelCountdown.TextAlign = 'MiddleCenter' + + # Generic Y location for buttons + $buttonsLocationY = 216 + $appDeployLogoBannerHeightDifference + + ## Label Restart Later + $buttonRestartLater.Anchor = 'Bottom,Left' + $buttonRestartLater.Location = "20,$buttonsLocationY" + $buttonRestartLater.Name = 'buttonRestartLater' + $buttonRestartLater.Size = '159,23' + $buttonRestartLater.TabIndex = 0 + $buttonRestartLater.Text = $configRestartPromptButtonRestartLater + $buttonRestartLater.UseVisualStyleBackColor = $true + $buttonRestartLater.add_Click($buttonRestartLater_Click) + + ## Label Restart Now + $buttonRestartNow.Anchor = 'Bottom,Right' + $buttonRestartNow.Location = "265,$buttonsLocationY" + $buttonRestartNow.Name = 'buttonRestartNow' + $buttonRestartNow.Size = '159,23' + $buttonRestartNow.TabIndex = 2 + $buttonRestartNow.Text = $configRestartPromptButtonRestartNow + $buttonRestartNow.UseVisualStyleBackColor = $true + $buttonRestartNow.add_Click($buttonRestartNow_Click) + + ## Timer Countdown + If (-not $NoCountdown) { $timerCountdown.add_Tick($timerCountdown_Tick) } + + ##---------------------------------------------- + + ## Save the initial state of the form + $InitialFormWindowState = $formRestart.WindowState + # Init the OnLoad event to correct the initial state of the form + $formRestart.add_Load($Form_StateCorrection_Load) + # Clean up the control events + $formRestart.add_FormClosed($Form_Cleanup_FormClosed) + $formRestartClosing = [Windows.Forms.FormClosingEventHandler]{ If ($_.CloseReason -eq 'UserClosing') { $_.Cancel = $true } } + $formRestart.add_FormClosing($formRestartClosing) + + ## If the script has been dot-source invoked by the deploy app script, display the restart prompt asynchronously + If ($deployAppScriptFriendlyName) { + If ($NoCountdown) { + Write-Log -Message "Invoking ${CmdletName} asynchronously with no countdown..." -Source ${CmdletName} + } + Else { + Write-Log -Message "Invoking ${CmdletName} asynchronously with a [$countDownSeconds] second countdown..." -Source ${CmdletName} + } + ## Remove Silent reboot parameters from the list that is being forwarded to the main script for asynchronous function execution. This is only for Interactive mode so we dont need silent mode reboot parameters. + $installRestartPromptParameters.Remove("NoSilentRestart") + $installRestartPromptParameters.Remove("SilentCountdownSeconds") + ## Prepare a list of parameters of this function as a string + [string]$installRestartPromptParameters = ($installRestartPromptParameters.GetEnumerator() | ForEach-Object { + If ($_.Value.GetType().Name -eq 'SwitchParameter') { + "-$($_.Key)" + } + ElseIf ($_.Value.GetType().Name -eq 'Boolean') { + "-$($_.Key) `$" + "$($_.Value)".ToLower() + } + ElseIf ($_.Value.GetType().Name -eq 'Int32') { + "-$($_.Key) $($_.Value)" + } + Else { + "-$($_.Key) `"$($_.Value)`"" + } + }) -join ' ' + ## Start another powershell instance silently with function parameters from this function + Start-Process -FilePath "$PSHOME\powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -File `"$scriptPath`" -ReferredInstallTitle `"$installTitle`" -ReferredInstallName `"$installName`" -ReferredLogName `"$logName`" -ShowInstallationRestartPrompt $installRestartPromptParameters -AsyncToolkitLaunch" -WindowStyle 'Hidden' -ErrorAction 'SilentlyContinue' + } + Else { + If ($NoCountdown) { + Write-Log -Message 'Display restart prompt with no countdown.' -Source ${CmdletName} + } + Else { + Write-Log -Message "Display restart prompt with a [$countDownSeconds] second countdown." -Source ${CmdletName} + } + + # Show the Form + Write-Output -InputObject $formRestart.ShowDialog() + $formRestart.Dispose() + + # Activate the Window + [Diagnostics.Process]$powershellProcess = Get-Process | Where-Object { $_.MainWindowTitle -match $installTitle } + [Microsoft.VisualBasic.Interaction]::AppActivate($powershellProcess.ID) + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7205,126 +7392,126 @@ Function Show-InstallationRestartPrompt { Function Show-BalloonTip { <# .SYNOPSIS - Displays a balloon tip notification in the system tray. + Displays a balloon tip notification in the system tray. .DESCRIPTION - Displays a balloon tip notification in the system tray. + Displays a balloon tip notification in the system tray. .PARAMETER BalloonTipText - Text of the balloon tip. + Text of the balloon tip. .PARAMETER BalloonTipTitle - Title of the balloon tip. + Title of the balloon tip. .PARAMETER BalloonTipIcon - Icon to be used. Options: 'Error', 'Info', 'None', 'Warning'. Default is: Info. + Icon to be used. Options: 'Error', 'Info', 'None', 'Warning'. Default is: Info. .PARAMETER BalloonTipTime - Time in milliseconds to display the balloon tip. Default: 500. + Time in milliseconds to display the balloon tip. Default: 500. .EXAMPLE - Show-BalloonTip -BalloonTipText 'Installation Started' -BalloonTipTitle 'Application Name' + Show-BalloonTip -BalloonTipText 'Installation Started' -BalloonTipTitle 'Application Name' .EXAMPLE - Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText 'Installation Started' -BalloonTipTitle 'Application Name' -BalloonTipTime 1000 + Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText 'Installation Started' -BalloonTipTitle 'Application Name' -BalloonTipTime 1000 .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNullOrEmpty()] - [string]$BalloonTipText, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [string]$BalloonTipTitle = $installTitle, - [Parameter(Mandatory=$false,Position=2)] - [ValidateSet('Error','Info','None','Warning')] - [Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'Info', - [Parameter(Mandatory=$false,Position=3)] - [ValidateNotNullorEmpty()] - [int32]$BalloonTipTime = 10000 - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - ## Skip balloon if in silent mode - If (($deployModeSilent) -or (-not $configShowBalloonNotifications) -or (Test-PowerPoint)) { Return } - - ## Dispose of previous balloon - If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } - - ## Get the calling function so we know when to display the exiting balloon tip notification in an asynchronous script - Try { [string]$callingFunction = (Get-Variable -Name MyInvocation -Scope 1).Value.Mycommand.Name } Catch { } - - If ($callingFunction -eq 'Exit-Script') { - Write-Log -Message "Display balloon tip notification asynchronously with message [$BalloonTipText]." -Source ${CmdletName} - ## Create a script block to display the balloon notification in a new PowerShell process so that we can wait to cleanly dispose of the balloon tip without having to make the deployment script wait - [scriptblock]$notifyIconScriptBlock = { - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNullOrEmpty()] - [string]$BalloonTipText, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [string]$BalloonTipTitle, - [Parameter(Mandatory=$false,Position=2)] - [ValidateSet('Error','Info','None','Warning')] - $BalloonTipIcon, # Don't strongly type variable as System.Drawing; assembly not loaded yet in asynchronous scriptblock so will throw error - [Parameter(Mandatory=$false,Position=3)] - [ValidateNotNullorEmpty()] - [int32]$BalloonTipTime, - [Parameter(Mandatory=$false,Position=4)] - [ValidateNotNullorEmpty()] - [string]$AppDeployLogoIcon - ) - - ## Load assembly containing class System.Windows.Forms and System.Drawing - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' - - [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon - $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ - BalloonTipIcon = $BalloonTipIcon - BalloonTipText = $BalloonTipText - BalloonTipTitle = $BalloonTipTitle - Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - Text = -join $BalloonTipText[0..62] - Visible = $true - } - - ## Display the balloon tip notification asynchronously - $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) - - ## Keep the asynchronous PowerShell process running so that we can dispose of the balloon tip icon - Start-Sleep -Milliseconds ($BalloonTipTime) - $script:notifyIcon.Dispose() - } - - ## Invoke a separate PowerShell process passing the script block as a command and associated parameters to display the balloon tip notification asynchronously - Try { - Execute-Process -Path "$PSHOME\powershell.exe" -Parameters "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command & {$notifyIconScriptBlock} '$BalloonTipText' '$BalloonTipTitle' '$BalloonTipIcon' '$BalloonTipTime' '$AppDeployLogoIcon'" -NoWait -WindowStyle 'Hidden' -CreateNoWindow - } - Catch { } - } - ## Otherwise create the balloontip icon synchronously - Else { - Write-Log -Message "Display balloon tip notification with message [$BalloonTipText]." -Source ${CmdletName} - [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon - $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ - BalloonTipIcon = $BalloonTipIcon - BalloonTipText = $BalloonTipText - BalloonTipTitle = $BalloonTipTitle - Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon - Text = -join $BalloonTipText[0..62] - Visible = $true - } - - ## Display the balloon tip notification - $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,Position=0)] + [ValidateNotNullOrEmpty()] + [string]$BalloonTipText, + [Parameter(Mandatory=$false,Position=1)] + [ValidateNotNullorEmpty()] + [string]$BalloonTipTitle = $installTitle, + [Parameter(Mandatory=$false,Position=2)] + [ValidateSet('Error','Info','None','Warning')] + [Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'Info', + [Parameter(Mandatory=$false,Position=3)] + [ValidateNotNullorEmpty()] + [int32]$BalloonTipTime = 10000 + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + ## Skip balloon if in silent mode + If (($deployModeSilent) -or (-not $configShowBalloonNotifications) -or (Test-PowerPoint)) { Return } + + ## Dispose of previous balloon + If ($script:notifyIcon) { Try { $script:notifyIcon.Dispose() } Catch {} } + + ## Get the calling function so we know when to display the exiting balloon tip notification in an asynchronous script + Try { [string]$callingFunction = (Get-Variable -Name MyInvocation -Scope 1).Value.Mycommand.Name } Catch { } + + If ($callingFunction -eq 'Exit-Script') { + Write-Log -Message "Display balloon tip notification asynchronously with message [$BalloonTipText]." -Source ${CmdletName} + ## Create a script block to display the balloon notification in a new PowerShell process so that we can wait to cleanly dispose of the balloon tip without having to make the deployment script wait + [scriptblock]$notifyIconScriptBlock = { + Param ( + [Parameter(Mandatory=$true,Position=0)] + [ValidateNotNullOrEmpty()] + [string]$BalloonTipText, + [Parameter(Mandatory=$false,Position=1)] + [ValidateNotNullorEmpty()] + [string]$BalloonTipTitle, + [Parameter(Mandatory=$false,Position=2)] + [ValidateSet('Error','Info','None','Warning')] + $BalloonTipIcon, # Don't strongly type variable as System.Drawing; assembly not loaded yet in asynchronous scriptblock so will throw error + [Parameter(Mandatory=$false,Position=3)] + [ValidateNotNullorEmpty()] + [int32]$BalloonTipTime, + [Parameter(Mandatory=$false,Position=4)] + [ValidateNotNullorEmpty()] + [string]$AppDeployLogoIcon + ) + + ## Load assembly containing class System.Windows.Forms and System.Drawing + Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' + Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' + + [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon + $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ + BalloonTipIcon = $BalloonTipIcon + BalloonTipText = $BalloonTipText + BalloonTipTitle = $BalloonTipTitle + Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + Text = -join $BalloonTipText[0..62] + Visible = $true + } + + ## Display the balloon tip notification asynchronously + $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) + + ## Keep the asynchronous PowerShell process running so that we can dispose of the balloon tip icon + Start-Sleep -Milliseconds ($BalloonTipTime) + $script:notifyIcon.Dispose() + } + + ## Invoke a separate PowerShell process passing the script block as a command and associated parameters to display the balloon tip notification asynchronously + Try { + Execute-Process -Path "$PSHOME\powershell.exe" -Parameters "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command & {$notifyIconScriptBlock} '$BalloonTipText' '$BalloonTipTitle' '$BalloonTipIcon' '$BalloonTipTime' '$AppDeployLogoIcon'" -NoWait -WindowStyle 'Hidden' -CreateNoWindow + } + Catch { } + } + ## Otherwise create the balloontip icon synchronously + Else { + Write-Log -Message "Display balloon tip notification with message [$BalloonTipText]." -Source ${CmdletName} + [Windows.Forms.ToolTipIcon]$BalloonTipIcon = $BalloonTipIcon + $script:notifyIcon = New-Object -TypeName 'System.Windows.Forms.NotifyIcon' -Property @{ + BalloonTipIcon = $BalloonTipIcon + BalloonTipText = $BalloonTipText + BalloonTipTitle = $BalloonTipTitle + Icon = New-Object -TypeName 'System.Drawing.Icon' -ArgumentList $AppDeployLogoIcon + Text = -join $BalloonTipText[0..62] + Visible = $true + } + + ## Display the balloon tip notification + $script:NotifyIcon.ShowBalloonTip($BalloonTipTime) + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7333,223 +7520,228 @@ Function Show-BalloonTip { Function Show-InstallationProgress { <# .SYNOPSIS - Displays a progress dialog in a separate thread with an updateable custom message. + Displays a progress dialog in a separate thread with an updateable custom message. .DESCRIPTION - Create a WPF window in a separate thread to display a marquee style progress ellipse with a custom message that can be updated. - The status message supports line breaks. - The first time this function is called in a script, it will display a balloon tip notification to indicate that the installation has started (provided balloon tips are enabled in the configuration). + Create a WPF window in a separate thread to display a marquee style progress ellipse with a custom message that can be updated. + The status message supports line breaks. + The first time this function is called in a script, it will display a balloon tip notification to indicate that the installation has started (provided balloon tips are enabled in the configuration). .PARAMETER StatusMessage - The status message to be displayed. The default status message is taken from the XML configuration file. + The status message to be displayed. The default status message is taken from the XML configuration file. .PARAMETER WindowLocation - The location of the progress window. Default: just below top, centered. + The location of the progress window. Default: just below top, centered. .PARAMETER TopMost - Specifies whether the progress window should be topmost. Default: $true. + Specifies whether the progress window should be topmost. Default: $true. .EXAMPLE - Show-InstallationProgress - Uses the default status message from the XML configuration file. + Show-InstallationProgress + Uses the default status message from the XML configuration file. .EXAMPLE - Show-InstallationProgress -StatusMessage 'Installation in Progress...' + Show-InstallationProgress -StatusMessage 'Installation in Progress...' .EXAMPLE - Show-InstallationProgress -StatusMessage "Installation in Progress...`nThe installation may take 20 minutes to complete." + Show-InstallationProgress -StatusMessage "Installation in Progress...`nThe installation may take 20 minutes to complete." .EXAMPLE - Show-InstallationProgress -StatusMessage 'Installation in Progress...' -WindowLocation 'BottomRight' -TopMost $false + Show-InstallationProgress -StatusMessage 'Installation in Progress...' -WindowLocation 'BottomRight' -TopMost $false .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$StatusMessage = $configProgressMessageInstall, - [Parameter(Mandatory=$false)] - [ValidateSet('Default','BottomRight')] - [string]$WindowLocation = 'Default', - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$TopMost = $true - ) - - 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 ($deployModeSilent) { Return } - - ## If the default progress message hasn't been overridden and the deployment type is uninstall, use the default uninstallation message - If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Uninstall')) { - $StatusMessage = $configProgressMessageUninstall - } - If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Repair')) { - $StatusMessage = $configProgressMessageRepair - } - - If ($envHost.Name -match 'PowerGUI') { - Write-Log -Message "$($envHost.Name) is not a supported host for WPF multi-threading. Progress dialog with message [$statusMessage] will not be displayed." -Severity 2 -Source ${CmdletName} - Return - } - - ## Check if the progress thread is running before invoking methods on it - If ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -ne 'Running') { - # Notify user that the software installation has started - $balloonText = "$deploymentTypeName $configBalloonTextStart" - Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText - # Create a synchronized hashtable to share objects between runspaces - $script:ProgressSyncHash = [hashtable]::Synchronized(@{ }) - # Create a new runspace for the progress bar - $script:ProgressRunspace = [runspacefactory]::CreateRunspace() - $script:ProgressRunspace.ApartmentState = 'STA' - $script:ProgressRunspace.ThreadOptions = 'ReuseThread' - $script:ProgressRunspace.Open() - # Add the sync hash to the runspace - $script:ProgressRunspace.SessionStateProxy.SetVariable('progressSyncHash', $script:ProgressSyncHash) - # Add other variables from the parent thread required in the progress runspace - $script:ProgressRunspace.SessionStateProxy.SetVariable('installTitle', $installTitle) - $script:ProgressRunspace.SessionStateProxy.SetVariable('windowLocation', $windowLocation) - $script:ProgressRunspace.SessionStateProxy.SetVariable('topMost', $topMost.ToString()) - $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBanner', $appDeployLogoBanner) - $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBannerHeight', $appDeployLogoBannerHeight) - $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBannerHeightDifference', $appDeployLogoBannerHeightDifference) - $script:ProgressRunspace.SessionStateProxy.SetVariable('ProgressStatusMessage', $statusMessage) - $script:ProgressRunspace.SessionStateProxy.SetVariable('AppDeployLogoIcon', $AppDeployLogoIcon) - $script:ProgressRunspace.SessionStateProxy.SetVariable('dpiScale', $dpiScale) - - # Add the script block to be executed in the progress runspace - $progressCmd = [PowerShell]::Create().AddScript({ - [string]$xamlProgressString = @' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$StatusMessage = $configProgressMessageInstall, + [Parameter(Mandatory=$false)] + [ValidateSet('Default','BottomRight','TopCenter')] + [string]$WindowLocation = 'Default', + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$TopMost = $true + ) + + 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 ($deployModeSilent) { Return } + + ## If the default progress message hasn't been overridden and the deployment type is uninstall, use the default uninstallation message + If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Uninstall')) { + $StatusMessage = $configProgressMessageUninstall + } + If (($StatusMessage -eq $configProgressMessageInstall) -and ($deploymentType -eq 'Repair')) { + $StatusMessage = $configProgressMessageRepair + } + + If ($envHost.Name -match 'PowerGUI') { + Write-Log -Message "$($envHost.Name) is not a supported host for WPF multi-threading. Progress dialog with message [$statusMessage] will not be displayed." -Severity 2 -Source ${CmdletName} + Return + } + + ## Check if the progress thread is running before invoking methods on it + If ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -ne 'Running') { + # Notify user that the software installation has started + $balloonText = "$deploymentTypeName $configBalloonTextStart" + Show-BalloonTip -BalloonTipIcon 'Info' -BalloonTipText $balloonText + # Create a synchronized hashtable to share objects between runspaces + $script:ProgressSyncHash = [hashtable]::Synchronized(@{ }) + # Create a new runspace for the progress bar + $script:ProgressRunspace = [runspacefactory]::CreateRunspace() + $script:ProgressRunspace.ApartmentState = 'STA' + $script:ProgressRunspace.ThreadOptions = 'ReuseThread' + $script:ProgressRunspace.Open() + # Add the sync hash to the runspace + $script:ProgressRunspace.SessionStateProxy.SetVariable('progressSyncHash', $script:ProgressSyncHash) + # Add other variables from the parent thread required in the progress runspace + $script:ProgressRunspace.SessionStateProxy.SetVariable('installTitle', $installTitle) + $script:ProgressRunspace.SessionStateProxy.SetVariable('windowLocation', $windowLocation) + $script:ProgressRunspace.SessionStateProxy.SetVariable('topMost', $topMost.ToString()) + $script:ProgressRunspace.SessionStateProxy.SetVariable('appDeployLogoBanner', $appDeployLogoBanner) + $script:ProgressRunspace.SessionStateProxy.SetVariable('ProgressStatusMessage', $statusMessage) + $script:ProgressRunspace.SessionStateProxy.SetVariable('AppDeployLogoIcon', $AppDeployLogoIcon) + + # Add the script block to be executed in the progress runspace + $progressCmd = [PowerShell]::Create().AddScript({ + [string]$xamlProgressString = @' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + '@ - ## Replace dummy text with values and turn the string into an xml document variable - $xamlProgressString = $xamlProgressString.replace('%BannerHeight%', $appDeployLogoBannerHeight).replace('%Height%', 180 + $appDeployLogoBannerHeightDifference).replace('%MinHeight%', 180 + $appDeployLogoBannerHeightDifference).replace('%MaxHeight%', 200 + $appDeployLogoBannerHeightDifference) - [Xml.XmlDocument]$xamlProgress = New-Object 'System.Xml.XmlDocument' - $xamlProgress.LoadXml($xamlProgressString) - ## Set the configurable values using variables added to the runspace from the parent thread - # Calculate the position on the screen where the progress dialog should be placed - $screen = [Windows.Forms.Screen]::PrimaryScreen - $screenWorkingArea = $screen.WorkingArea - [int32]$screenWidth = $screenWorkingArea | Select-Object -ExpandProperty 'Width' - [int32]$screenHeight = $screenWorkingArea | Select-Object -ExpandProperty 'Height' - # Set the start position of the Window based on the screen size - If ($windowLocation -eq 'BottomRight') { - $xamlProgress.Window.Left = [string](($screenWidth / ($dpiscale / 100)) - ($xamlProgress.Window.Width)) - $xamlProgress.Window.Top = [string](($screenHeight / ($dpiscale / 100)) - ($xamlProgress.Window.Height)) - } - # Show the default location (Top center) - Else { - # Center the progress window by calculating the center of the workable screen based on the width of the screen relative to the DPI scale minus half the width of the progress bar - $xamlProgress.Window.Left = [string](($screenWidth / (2 * ($dpiscale / 100) )) - (($xamlProgress.Window.Width / 2))) - $xamlProgress.Window.Top = [string]($screenHeight / 9.5) - } - $xamlProgress.Window.TopMost = $topMost - $xamlProgress.Window.Icon = $AppDeployLogoIcon - $xamlProgress.Window.Grid.Image.Source = $appDeployLogoBanner - $xamlProgress.Window.Grid.TextBlock.Text = $ProgressStatusMessage - $xamlProgress.Window.Title = $installTitle - # Parse the XAML - $progressReader = New-Object -TypeName 'System.Xml.XmlNodeReader' -ArgumentList $xamlProgress - $script:ProgressSyncHash.Window = [Windows.Markup.XamlReader]::Load($progressReader) - # Grey out the X button - $script:ProgressSyncHash.Window.add_Loaded({ - [IntPtr]$windowHandle = (New-Object -TypeName System.Windows.Interop.WindowInteropHelper -ArgumentList $this).Handle - If ($null -ne $windowHandle) { - [IntPtr]$menuHandle = [PSADT.UiAutomation]::GetSystemMenu($windowHandle, $false) - If ($menuHandle -ne [IntPtr]::Zero) { - [PSADT.UiAutomation]::EnableMenuItem($menuHandle, 0xF060, 0x00000001) - [PSADT.UiAutomation]::DestroyMenu($menuHandle) - } - } - }) - # Prepare the ProgressText variable so we can use it to change the text in the text area - $script:ProgressSyncHash.ProgressText = $script:ProgressSyncHash.Window.FindName('ProgressText') - # Add an action to the Window.Closing event handler to disable the close button - $script:ProgressSyncHash.Window.Add_Closing({ $_.Cancel = $true }) - # Allow the window to be dragged by clicking on it anywhere - $script:ProgressSyncHash.Window.Add_MouseLeftButtonDown({ $script:ProgressSyncHash.Window.DragMove() }) - # Add a tooltip - $script:ProgressSyncHash.Window.ToolTip = $installTitle - $null = $script:ProgressSyncHash.Window.ShowDialog() - $script:ProgressSyncHash.Error = $Error - }) - - $progressCmd.Runspace = $script:ProgressRunspace - Write-Log -Message "Spin up progress dialog in a separate thread with message: [$statusMessage]." -Source ${CmdletName} - # Invoke the progress runspace - $progressData = $progressCmd.BeginInvoke() - # Allow the thread to be spun up safely before invoking actions against it. - Start-Sleep -Seconds 1 - If ($script:ProgressSyncHash.Error) { - Write-Log -Message "Failure while displaying progress dialog. `n$(Resolve-Error -ErrorRecord $script:ProgressSyncHash.Error)" -Severity 3 -Source ${CmdletName} - } - } - ## Check if the progress thread is running before invoking methods on it - ElseIf ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { - # Update the progress text - Try { - $script:ProgressSyncHash.Window.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]'Send', [Windows.Input.InputEventHandler]{ $script:ProgressSyncHash.ProgressText.Text = $statusMessage }, $null, $null) - Write-Log -Message "Updated progress message: [$statusMessage]." -Source ${CmdletName} - } - Catch { - Write-Log -Message "Unable to update the progress message. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [Xml.XmlDocument]$xamlProgress = New-Object 'System.Xml.XmlDocument' + $xamlProgress.LoadXml($xamlProgressString) + ## Set the configurable values using variables added to the runspace from the parent thread + $xamlProgress.Window.TopMost = $topMost + $xamlProgress.Window.Icon = $AppDeployLogoIcon + $xamlProgress.Window.Grid.Image.Source = $appDeployLogoBanner + $xamlProgress.Window.Grid.TextBlock.Text = $ProgressStatusMessage + $xamlProgress.Window.Title = $installTitle + # Parse the XAML + $progressReader = New-Object -TypeName 'System.Xml.XmlNodeReader' -ArgumentList $xamlProgress + $script:ProgressSyncHash.Window = [Windows.Markup.XamlReader]::Load($progressReader) + # Grey out the X button + $script:ProgressSyncHash.Window.add_Loaded({ + # Calculate the position on the screen where the progress dialog should be placed + [int32]$screenWidth = [System.Windows.SystemParameters]::WorkArea.Width + [int32]$screenHeight = [System.Windows.SystemParameters]::WorkArea.Height + [int32]$screenCenterWidth = $screenWidth - $script:ProgressSyncHash.Window.ActualWidth + [int32]$screenCenterHeight = $screenHeight - $script:ProgressSyncHash.Window.ActualHeight + # Set the start position of the Window based on the screen size + If ($windowLocation -eq 'BottomRight') { + # Put the window in the corner + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight) + } + ElseIf($windowLocation -eq 'TopCenter'){ + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth / 2) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight / 6) + } + Else { + # Center the progress window by calculating the center of the workable screen based on the width of the screen minus half the width of the progress bar + $script:ProgressSyncHash.Window.Left = [Double]($screenCenterWidth / 2) + $script:ProgressSyncHash.Window.Top = [Double]($screenCenterHeight / 2) + } + # Grey out the X button + try { + $windowHandle = (New-Object -TypeName System.Windows.Interop.WindowInteropHelper -ArgumentList $this).Handle + If ($windowHandle -and ($windowHandle -ne [IntPtr]::Zero)) { + $menuHandle = [PSADT.UiAutomation]::GetSystemMenu($windowHandle, $false) + If ($menuHandle -and ($menuHandle -ne [IntPtr]::Zero)) { + [PSADT.UiAutomation]::EnableMenuItem($menuHandle, 0xF060, 0x00000001) + [PSADT.UiAutomation]::DestroyMenu($menuHandle) + } + } + } + catch { + # Not a terminating error if we can't grey out the button + Write-Log "Failed to grey out the Close button." -Severity 2 -Source ${CmdletName} + } + }) + # Prepare the ProgressText variable so we can use it to change the text in the text area + $script:ProgressSyncHash.ProgressText = $script:ProgressSyncHash.Window.FindName('ProgressText') + # Add an action to the Window.Closing event handler to disable the close button + $script:ProgressSyncHash.Window.Add_Closing({ $_.Cancel = $true }) + # Allow the window to be dragged by clicking on it anywhere + $script:ProgressSyncHash.Window.Add_MouseLeftButtonDown({ $script:ProgressSyncHash.Window.DragMove() }) + # Add a tooltip + $script:ProgressSyncHash.Window.ToolTip = $installTitle + $null = $script:ProgressSyncHash.Window.ShowDialog() + $script:ProgressSyncHash.Error = $Error + }) + + $progressCmd.Runspace = $script:ProgressRunspace + Write-Log -Message "Spin up progress dialog in a separate thread with message: [$statusMessage]." -Source ${CmdletName} + # Invoke the progress runspace + $null = $progressCmd.BeginInvoke() + # Allow the thread to be spun up safely before invoking actions against it. + Start-Sleep -Seconds 1 + If ($script:ProgressSyncHash.Error) { + Write-Log -Message "Failure while displaying progress dialog. `n$(Resolve-Error -ErrorRecord $script:ProgressSyncHash.Error)" -Severity 3 -Source ${CmdletName} + } + } + ## Check if the progress thread is running before invoking methods on it + ElseIf ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { + # Update the progress text + Try { + $script:ProgressSyncHash.Window.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]'Send', [Windows.Input.InputEventHandler]{ $script:ProgressSyncHash.ProgressText.Text = $statusMessage }, $null, $null) + Write-Log -Message "Updated progress message: [$statusMessage]." -Source ${CmdletName} + } + Catch { + Write-Log -Message "Unable to update the progress message. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7558,38 +7750,38 @@ Function Show-InstallationProgress { Function Close-InstallationProgress { <# .SYNOPSIS - Closes the dialog created by Show-InstallationProgress. + Closes the dialog created by Show-InstallationProgress. .DESCRIPTION - Closes the dialog created by Show-InstallationProgress. - This function is called by the Exit-Script function to close a running instance of the progress dialog if found. + Closes the dialog created by Show-InstallationProgress. + This function is called by the Exit-Script function to close a running instance of the progress dialog if found. .EXAMPLE - Close-InstallationProgress + Close-InstallationProgress .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - 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 ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { - ## Close the progress thread - Write-Log -Message 'Close the installation progress dialog.' -Source ${CmdletName} - $script:ProgressSyncHash.Window.Dispatcher.InvokeShutdown() - $script:ProgressSyncHash.Clear() - $script:ProgressRunspace.Close() - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + 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 ($script:ProgressSyncHash.Window.Dispatcher.Thread.ThreadState -eq 'Running') { + ## Close the progress thread + Write-Log -Message 'Close the installation progress dialog.' -Source ${CmdletName} + $script:ProgressSyncHash.Window.Dispatcher.InvokeShutdown() + $script:ProgressSyncHash.Clear() + $script:ProgressRunspace.Close() + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7598,192 +7790,192 @@ Function Close-InstallationProgress { Function Set-PinnedApplication { <# .SYNOPSIS - Pins or unpins a shortcut to the start menu or task bar. + Pins or unpins a shortcut to the start menu or task bar. .DESCRIPTION - Pins or unpins a shortcut to the start menu or task bar. - This should typically be run in the user context, as pinned items are stored in the user profile. + Pins or unpins a shortcut to the start menu or task bar. + This should typically be run in the user context, as pinned items are stored in the user profile. .PARAMETER Action - Action to be performed. Options: 'PintoStartMenu','UnpinfromStartMenu','PintoTaskbar','UnpinfromTaskbar'. + Action to be performed. Options: 'PintoStartMenu','UnpinfromStartMenu','PintoTaskbar','UnpinfromTaskbar'. .PARAMETER FilePath - Path to the shortcut file to be pinned or unpinned. + Path to the shortcut file to be pinned or unpinned. .EXAMPLE - Set-PinnedApplication -Action 'PintoStartMenu' -FilePath "$envProgramFilesX86\IBM\Lotus\Notes\notes.exe" + Set-PinnedApplication -Action 'PintoStartMenu' -FilePath "$envProgramFilesX86\IBM\Lotus\Notes\notes.exe" .EXAMPLE - Set-PinnedApplication -Action 'UnpinfromTaskbar' -FilePath "$envProgramFilesX86\IBM\Lotus\Notes\notes.exe" + Set-PinnedApplication -Action 'UnpinfromTaskbar' -FilePath "$envProgramFilesX86\IBM\Lotus\Notes\notes.exe" .NOTES - Windows 10 logic borrowed from Stuart Pearson (https://pinto10blog.wordpress.com/2016/09/10/pinto10/) + Windows 10 logic borrowed from Stuart Pearson (https://pinto10blog.wordpress.com/2016/09/10/pinto10/) .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateSet('PintoStartMenu','UnpinfromStartMenu','PintoTaskbar','UnpinfromTaskbar')] - [string]$Action, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$FilePath - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - #region Function Get-PinVerb - Function Get-PinVerb { - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [int32]$VerbId - ) - - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - Write-Log -Message "Get localized pin verb for verb id [$VerbID]." -Source ${CmdletName} - [string]$PinVerb = [PSADT.FileVerb]::GetPinVerb($VerbId) - Write-Log -Message "Verb ID [$VerbID] has a localized pin verb of [$PinVerb]." -Source ${CmdletName} - Write-Output -InputObject $PinVerb - } - #endregion - - #region Function Invoke-Verb - Function Invoke-Verb { - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$FilePath, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Verb - ) - - Try { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - $verb = $verb.Replace('&','') - $path = Split-Path -Path $FilePath -Parent -ErrorAction 'Stop' - $folder = $shellApp.Namespace($path) - $item = $folder.ParseName((Split-Path -Path $FilePath -Leaf -ErrorAction 'Stop')) - $itemVerb = $item.Verbs() | Where-Object { $_.Name.Replace('&','') -eq $verb } -ErrorAction 'Stop' - - If ($null -eq $itemVerb) { - Write-Log -Message "Performing action [$verb] is not programmatically supported for this file [$FilePath]." -Severity 2 -Source ${CmdletName} - } - Else { - Write-Log -Message "Perform action [$verb] on [$FilePath]." -Source ${CmdletName} - $itemVerb.DoIt() - } - } - Catch { - Write-Log -Message "Failed to perform action [$verb] on [$FilePath]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - #endregion - - If (([version]$envOSVersion).Major -ge 10) { - Write-Log -Message "Detected Windows 10 or higher, using Windows 10 verb codes." -Source ${CmdletName} - [hashtable]$Verbs = @{ - 'PintoStartMenu' = 51201 - 'UnpinfromStartMenu' = 51394 - 'PintoTaskbar' = 5386 - 'UnpinfromTaskbar' = 5387 - } - } - Else { - [hashtable]$Verbs = @{ - 'PintoStartMenu' = 5381 - 'UnpinfromStartMenu' = 5382 - 'PintoTaskbar' = 5386 - 'UnpinfromTaskbar' = 5387 - } - } - - } - Process { - Try { - Write-Log -Message "Execute action [$Action] for file [$FilePath]." -Source ${CmdletName} - - If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf' -ErrorAction 'Stop')) { - Throw "Path [$filePath] does not exist." - } - - If (-not ($Verbs.$Action)) { - Throw "Action [$Action] not supported. Supported actions are [$($Verbs.Keys -join ', ')]." - } - - If ($Action.Contains("StartMenu")) - { - If ([int]$envOSVersionMajor -ge 10) { - If ((Get-Item -Path $FilePath).Extension -ne '.lnk') { - Throw "Only shortcut files (.lnk) are supported on Windows 10 and higher." - } - ElseIf (-not ($FilePath.StartsWith($envUserStartMenu, 'CurrentCultureIgnoreCase') -or $FilePath.StartsWith($envCommonStartMenu, 'CurrentCultureIgnoreCase'))) { - Throw "Only shortcut files (.lnk) in [$envUserStartMenu] and [$envCommonStartMenu] are supported on Windows 10 and higher." - } - } - - [string]$PinVerbAction = Get-PinVerb -VerbId $Verbs.$Action - If (-not ($PinVerbAction)) { - Throw "Failed to get a localized pin verb for action [$Action]. Action is not supported on this operating system." - } - - Invoke-Verb -FilePath $FilePath -Verb $PinVerbAction - } - ElseIf ($Action.Contains("Taskbar")) { - If ([int]$envOSVersionMajor -ge 10) { - $FileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) - $PinExists = Test-Path -Path "$envAppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\$($FileNameWithoutExtension).lnk" - - If ($Action -eq 'PintoTaskbar' -and $PinExists) { - If($(Invoke-ObjectMethod -InputObject $Shell -MethodName 'CreateShortcut' -ArgumentList "$envAppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\$($FileNameWithoutExtension).lnk").TargetPath -eq $FilePath) { - Write-Log -Message "Pin [$FileNameWithoutExtension] already exists." -Source ${CmdletName} - return - } - } - ElseIf ($Action -eq 'UnpinfromTaskbar' -and $PinExists -eq $false) { - Write-Log -Message "Pin [$FileNameWithoutExtension] does not exist." -Source ${CmdletName} - return - } - - $ExplorerCommandHandler = Get-RegistryKey -Key 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin' -Value 'ExplorerCommandHandler' - $classesStarKey = (Get-Item "Registry::HKEY_USERS\$($RunasActiveUser.SID)\SOFTWARE\Classes").OpenSubKey("*", $true) - $shellKey = $classesStarKey.CreateSubKey("shell", $true) - $specialKey = $shellKey.CreateSubKey("{:}", $true) - $specialKey.SetValue("ExplorerCommandHandler", $ExplorerCommandHandler) - - $Folder = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'Namespace' -ArgumentList $(Split-Path -Path $FilePath -Parent) - $Item = Invoke-ObjectMethod -InputObject $Folder -MethodName 'ParseName' -ArgumentList $(Split-Path -Path $FilePath -Leaf) - - $Item.InvokeVerb("{:}") - - $shellKey.DeleteSubKey("{:}") - If ($shellKey.SubKeyCount -eq 0 -and $shellKey.ValueCount -eq 0) { - $classesStarKey.DeleteSubKey("shell") - } - } - Else { - [string]$PinVerbAction = Get-PinVerb -VerbId $Verbs.$Action - If (-not ($PinVerbAction)) { - Throw "Failed to get a localized pin verb for action [$Action]. Action is not supported on this operating system." - } - - Invoke-Verb -FilePath $FilePath -Verb $PinVerbAction - } - } - } - Catch { - Write-Log -Message "Failed to execute action [$Action]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - Finally { - Try { If ($shellKey) { $shellKey.Close() } } Catch { } - Try { If ($classesStarKey) { $classesStarKey.Close() } } Catch { } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateSet('PintoStartMenu','UnpinfromStartMenu','PintoTaskbar','UnpinfromTaskbar')] + [string]$Action, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$FilePath + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + #region Function Get-PinVerb + Function Get-PinVerb { + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [int32]$VerbId + ) + + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-Log -Message "Get localized pin verb for verb id [$VerbID]." -Source ${CmdletName} + [string]$PinVerb = [PSADT.FileVerb]::GetPinVerb($VerbId) + Write-Log -Message "Verb ID [$VerbID] has a localized pin verb of [$PinVerb]." -Source ${CmdletName} + Write-Output -InputObject $PinVerb + } + #endregion + + #region Function Invoke-Verb + Function Invoke-Verb { + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$FilePath, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Verb + ) + + Try { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + $verb = $verb.Replace('&','') + $path = Split-Path -Path $FilePath -Parent -ErrorAction 'Stop' + $folder = $shellApp.Namespace($path) + $item = $folder.ParseName((Split-Path -Path $FilePath -Leaf -ErrorAction 'Stop')) + $itemVerb = $item.Verbs() | Where-Object { $_.Name.Replace('&','') -eq $verb } -ErrorAction 'Stop' + + If ($null -eq $itemVerb) { + Write-Log -Message "Performing action [$verb] is not programmatically supported for this file [$FilePath]." -Severity 2 -Source ${CmdletName} + } + Else { + Write-Log -Message "Perform action [$verb] on [$FilePath]." -Source ${CmdletName} + $itemVerb.DoIt() + } + } + Catch { + Write-Log -Message "Failed to perform action [$verb] on [$FilePath]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + #endregion + + If (([version]$envOSVersion).Major -ge 10) { + Write-Log -Message "Detected Windows 10 or higher, using Windows 10 verb codes." -Source ${CmdletName} + [hashtable]$Verbs = @{ + 'PintoStartMenu' = 51201 + 'UnpinfromStartMenu' = 51394 + 'PintoTaskbar' = 5386 + 'UnpinfromTaskbar' = 5387 + } + } + Else { + [hashtable]$Verbs = @{ + 'PintoStartMenu' = 5381 + 'UnpinfromStartMenu' = 5382 + 'PintoTaskbar' = 5386 + 'UnpinfromTaskbar' = 5387 + } + } + + } + Process { + Try { + Write-Log -Message "Execute action [$Action] for file [$FilePath]." -Source ${CmdletName} + + If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf' -ErrorAction 'Stop')) { + Throw "Path [$filePath] does not exist." + } + + If (-not ($Verbs.$Action)) { + Throw "Action [$Action] not supported. Supported actions are [$($Verbs.Keys -join ', ')]." + } + + If ($Action.Contains("StartMenu")) + { + If ([int]$envOSVersionMajor -ge 10) { + If ((Get-Item -Path $FilePath).Extension -ne '.lnk') { + Throw "Only shortcut files (.lnk) are supported on Windows 10 and higher." + } + ElseIf (-not ($FilePath.StartsWith($envUserStartMenu, 'CurrentCultureIgnoreCase') -or $FilePath.StartsWith($envCommonStartMenu, 'CurrentCultureIgnoreCase'))) { + Throw "Only shortcut files (.lnk) in [$envUserStartMenu] and [$envCommonStartMenu] are supported on Windows 10 and higher." + } + } + + [string]$PinVerbAction = Get-PinVerb -VerbId $Verbs.$Action + If (-not ($PinVerbAction)) { + Throw "Failed to get a localized pin verb for action [$Action]. Action is not supported on this operating system." + } + + Invoke-Verb -FilePath $FilePath -Verb $PinVerbAction + } + ElseIf ($Action.Contains("Taskbar")) { + If ([int]$envOSVersionMajor -ge 10) { + $FileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) + $PinExists = Test-Path -Path "$envAppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\$($FileNameWithoutExtension).lnk" + + If ($Action -eq 'PintoTaskbar' -and $PinExists) { + If($(Invoke-ObjectMethod -InputObject $Shell -MethodName 'CreateShortcut' -ArgumentList "$envAppData\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\$($FileNameWithoutExtension).lnk").TargetPath -eq $FilePath) { + Write-Log -Message "Pin [$FileNameWithoutExtension] already exists." -Source ${CmdletName} + return + } + } + ElseIf ($Action -eq 'UnpinfromTaskbar' -and $PinExists -eq $false) { + Write-Log -Message "Pin [$FileNameWithoutExtension] does not exist." -Source ${CmdletName} + return + } + + $ExplorerCommandHandler = Get-RegistryKey -Key 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\CommandStore\shell\Windows.taskbarpin' -Value 'ExplorerCommandHandler' + $classesStarKey = (Get-Item "Registry::HKEY_USERS\$($RunasActiveUser.SID)\SOFTWARE\Classes").OpenSubKey("*", $true) + $shellKey = $classesStarKey.CreateSubKey("shell", $true) + $specialKey = $shellKey.CreateSubKey("{:}", $true) + $specialKey.SetValue("ExplorerCommandHandler", $ExplorerCommandHandler) + + $Folder = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'Namespace' -ArgumentList $(Split-Path -Path $FilePath -Parent) + $Item = Invoke-ObjectMethod -InputObject $Folder -MethodName 'ParseName' -ArgumentList $(Split-Path -Path $FilePath -Leaf) + + $Item.InvokeVerb("{:}") + + $shellKey.DeleteSubKey("{:}") + If ($shellKey.SubKeyCount -eq 0 -and $shellKey.ValueCount -eq 0) { + $classesStarKey.DeleteSubKey("shell") + } + } + Else { + [string]$PinVerbAction = Get-PinVerb -VerbId $Verbs.$Action + If (-not ($PinVerbAction)) { + Throw "Failed to get a localized pin verb for action [$Action]. Action is not supported on this operating system." + } + + Invoke-Verb -FilePath $FilePath -Verb $PinVerbAction + } + } + } + Catch { + Write-Log -Message "Failed to execute action [$Action]. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + Finally { + Try { If ($shellKey) { $shellKey.Close() } } Catch { } + Try { If ($classesStarKey) { $classesStarKey.Close() } } Catch { } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7792,65 +7984,65 @@ Function Set-PinnedApplication { Function Get-IniValue { <# .SYNOPSIS - Parses an INI file and returns the value of the specified section and key. + Parses an INI file and returns the value of the specified section and key. .DESCRIPTION - Parses an INI file and returns the value of the specified section and key. + Parses an INI file and returns the value of the specified section and key. .PARAMETER FilePath - Path to the INI file. + Path to the INI file. .PARAMETER Section - Section within the INI file. + Section within the INI file. .PARAMETER Key - Key within the section of the INI file. + Key within the section of the INI file. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section 'Notes' -Key 'KeyFileName' + Get-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section 'Notes' -Key 'KeyFileName' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$FilePath, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Section, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Read INI Key: [Section = $Section] [Key = $Key]." -Source ${CmdletName} - - If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } - - $IniValue = [PSADT.IniFile]::GetIniValue($Section, $Key, $FilePath) - Write-Log -Message "INI Key Value: [Section = $Section] [Key = $Key] [Value = $IniValue]." -Source ${CmdletName} - - Write-Output -InputObject $IniValue - } - Catch { - Write-Log -Message "Failed to read INI file key value. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to read INI file key value: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$FilePath, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Section, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Read INI Key: [Section = $Section] [Key = $Key]." -Source ${CmdletName} + + If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } + + $IniValue = [PSADT.IniFile]::GetIniValue($Section, $Key, $FilePath) + Write-Log -Message "INI Key Value: [Section = $Section] [Key = $Key] [Value = $IniValue]." -Source ${CmdletName} + + Write-Output -InputObject $IniValue + } + Catch { + Write-Log -Message "Failed to read INI file key value. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to read INI file key value: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7859,68 +8051,68 @@ Function Get-IniValue { Function Set-IniValue { <# .SYNOPSIS - Opens an INI file and sets the value of the specified section and key. + Opens an INI file and sets the value of the specified section and key. .DESCRIPTION - Opens an INI file and sets the value of the specified section and key. + Opens an INI file and sets the value of the specified section and key. .PARAMETER FilePath - Path to the INI file. + Path to the INI file. .PARAMETER Section - Section within the INI file. + Section within the INI file. .PARAMETER Key - Key within the section of the INI file. + Key within the section of the INI file. .PARAMETER Value - Value for the key within the section of the INI file. To remove a value, set this variable to $null. + Value for the key within the section of the INI file. To remove a value, set this variable to $null. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Set-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section 'Notes' -Key 'KeyFileName' -Value 'MyFile.ID' + Set-IniValue -FilePath "$envProgramFilesX86\IBM\Notes\notes.ini" -Section 'Notes' -Key 'KeyFileName' -Value 'MyFile.ID' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$FilePath, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Section, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Key, - # Don't strongly type this variable as [string] b/c PowerShell replaces [string]$Value = $null with an empty string - [Parameter(Mandatory=$true)] - [AllowNull()] - $Value, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Write INI Key Value: [Section = $Section] [Key = $Key] [Value = $Value]." -Source ${CmdletName} - - If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } - - [PSADT.IniFile]::SetIniValue($Section, $Key, ([Text.StringBuilder]$Value), $FilePath) - } - Catch { - Write-Log -Message "Failed to write INI file key value. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to write INI file key value: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$FilePath, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Section, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Key, + # Don't strongly type this variable as [string] b/c PowerShell replaces [string]$Value = $null with an empty string + [Parameter(Mandatory=$true)] + [AllowNull()] + $Value, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Write INI Key Value: [Section = $Section] [Key = $Key] [Value = $Value]." -Source ${CmdletName} + + If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } + + [PSADT.IniFile]::SetIniValue($Section, $Key, ([Text.StringBuilder]$Value), $FilePath) + } + Catch { + Write-Log -Message "Failed to write INI file key value. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to write INI file key value: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -7929,87 +8121,87 @@ Function Set-IniValue { Function Get-PEFileArchitecture { <# .SYNOPSIS - Determine if a PE file is a 32-bit or a 64-bit file. + Determine if a PE file is a 32-bit or a 64-bit file. .DESCRIPTION - Determine if a PE file is a 32-bit or a 64-bit file by examining the file's image file header. - PE file extensions: .exe, .dll, .ocx, .drv, .sys, .scr, .efi, .cpl, .fon + Determine if a PE file is a 32-bit or a 64-bit file by examining the file's image file header. + PE file extensions: .exe, .dll, .ocx, .drv, .sys, .scr, .efi, .cpl, .fon .PARAMETER FilePath - Path to the PE file to examine. + Path to the PE file to examine. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .PARAMETER PassThru - Get the file object, attach a property indicating the file binary type, and write to pipeline + Get the file object, attach a property indicating the file binary type, and write to pipeline .EXAMPLE - Get-PEFileArchitecture -FilePath "$env:windir\notepad.exe" + Get-PEFileArchitecture -FilePath "$env:windir\notepad.exe" .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [IO.FileInfo[]]$FilePath, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true, - [Parameter(Mandatory=$false)] - [switch]$PassThru - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - [string[]]$PEFileExtensions = '.exe', '.dll', '.ocx', '.drv', '.sys', '.scr', '.efi', '.cpl', '.fon' - [int32]$MACHINE_OFFSET = 4 - [int32]$PE_POINTER_OFFSET = 60 - } - Process { - ForEach ($Path in $filePath) { - Try { - If ($PEFileExtensions -notcontains $Path.Extension) { - Throw "Invalid file type. Please specify one of the following PE file types: $($PEFileExtensions -join ', ')" - } - - [byte[]]$data = New-Object -TypeName 'System.Byte[]' -ArgumentList 4096 - $stream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($Path.FullName, 'Open', 'Read') - $null = $stream.Read($data, 0, 4096) - $stream.Flush() - $stream.Close() - - [int32]$PE_HEADER_ADDR = [BitConverter]::ToInt32($data, $PE_POINTER_OFFSET) - [uint16]$PE_IMAGE_FILE_HEADER = [BitConverter]::ToUInt16($data, $PE_HEADER_ADDR + $MACHINE_OFFSET) - Switch ($PE_IMAGE_FILE_HEADER) { - 0 { $PEArchitecture = 'Native' } # The contents of this file are assumed to be applicable to any machine type - 0x014c { $PEArchitecture = '32BIT' } # File for Windows 32-bit systems - 0x0200 { $PEArchitecture = 'Itanium-x64' } # File for Intel Itanium x64 processor family - 0x8664 { $PEArchitecture = '64BIT' } # File for Windows 64-bit systems - Default { $PEArchitecture = 'Unknown' } - } - Write-Log -Message "File [$($Path.FullName)] has a detected file architecture of [$PEArchitecture]." -Source ${CmdletName} - - If ($PassThru) { - # Get the file object, attach a property indicating the type, and write to pipeline - Get-Item -LiteralPath $Path.FullName -Force | Add-Member -MemberType 'NoteProperty' -Name 'BinaryType' -Value $PEArchitecture -Force -PassThru | Write-Output - } - Else { - Write-Output -InputObject $PEArchitecture - } - } - Catch { - Write-Log -Message "Failed to get the PE file architecture. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to get the PE file architecture: $($_.Exception.Message)" - } - Continue - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [IO.FileInfo[]]$FilePath, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true, + [Parameter(Mandatory=$false)] + [switch]$PassThru + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + [string[]]$PEFileExtensions = '.exe', '.dll', '.ocx', '.drv', '.sys', '.scr', '.efi', '.cpl', '.fon' + [int32]$MACHINE_OFFSET = 4 + [int32]$PE_POINTER_OFFSET = 60 + } + Process { + ForEach ($Path in $filePath) { + Try { + If ($PEFileExtensions -notcontains $Path.Extension) { + Throw "Invalid file type. Please specify one of the following PE file types: $($PEFileExtensions -join ', ')" + } + + [byte[]]$data = New-Object -TypeName 'System.Byte[]' -ArgumentList 4096 + $stream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($Path.FullName, 'Open', 'Read') + $null = $stream.Read($data, 0, 4096) + $stream.Flush() + $stream.Close() + + [int32]$PE_HEADER_ADDR = [BitConverter]::ToInt32($data, $PE_POINTER_OFFSET) + [uint16]$PE_IMAGE_FILE_HEADER = [BitConverter]::ToUInt16($data, $PE_HEADER_ADDR + $MACHINE_OFFSET) + Switch ($PE_IMAGE_FILE_HEADER) { + 0 { $PEArchitecture = 'Native' } # The contents of this file are assumed to be applicable to any machine type + 0x014c { $PEArchitecture = '32BIT' } # File for Windows 32-bit systems + 0x0200 { $PEArchitecture = 'Itanium-x64' } # File for Intel Itanium x64 processor family + 0x8664 { $PEArchitecture = '64BIT' } # File for Windows 64-bit systems + Default { $PEArchitecture = 'Unknown' } + } + Write-Log -Message "File [$($Path.FullName)] has a detected file architecture of [$PEArchitecture]." -Source ${CmdletName} + + If ($PassThru) { + # Get the file object, attach a property indicating the type, and write to pipeline + Get-Item -LiteralPath $Path.FullName -Force | Add-Member -MemberType 'NoteProperty' -Name 'BinaryType' -Value $PEArchitecture -Force -PassThru | Write-Output + } + Else { + Write-Output -InputObject $PEArchitecture + } + } + Catch { + Write-Log -Message "Failed to get the PE file architecture. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to get the PE file architecture: $($_.Exception.Message)" + } + Continue + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8018,116 +8210,116 @@ Function Get-PEFileArchitecture { Function Invoke-RegisterOrUnregisterDLL { <# .SYNOPSIS - Register or unregister a DLL file. + Register or unregister a DLL file. .DESCRIPTION - Register or unregister a DLL file using regsvr32.exe. Function can be invoked using alias: 'Register-DLL' or 'Unregister-DLL'. + Register or unregister a DLL file using regsvr32.exe. Function can be invoked using alias: 'Register-DLL' or 'Unregister-DLL'. .PARAMETER FilePath - Path to the DLL file. + Path to the DLL file. .PARAMETER DLLAction - Specify whether to register or unregister the DLL. Optional if function is invoked using 'Register-DLL' or 'Unregister-DLL' alias. + Specify whether to register or unregister the DLL. Optional if function is invoked using 'Register-DLL' or 'Unregister-DLL' alias. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Register-DLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" - Register DLL file using the "Register-DLL" alias for this function + Register-DLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" + Register DLL file using the "Register-DLL" alias for this function .EXAMPLE - UnRegister-DLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" - Unregister DLL file using the "Unregister-DLL" alias for this function + UnRegister-DLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" + Unregister DLL file using the "Unregister-DLL" alias for this function .EXAMPLE - Invoke-RegisterOrUnregisterDLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" -DLLAction 'Register' - Register DLL file using the actual name of this function + Invoke-RegisterOrUnregisterDLL -FilePath "C:\Test\DcTLSFileToDMSComp.dll" -DLLAction 'Register' + Register DLL file using the actual name of this function .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$FilePath, - [Parameter(Mandatory=$false)] - [ValidateSet('Register','Unregister')] - [string]$DLLAction, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Get name used to invoke this function in case the 'Register-DLL' or 'Unregister-DLL' alias was used and set the correct DLL action - [string]${InvokedCmdletName} = $MyInvocation.InvocationName - # Set the correct register/unregister action based on the alias used to invoke this function - If (${InvokedCmdletName} -ne ${CmdletName}) { - Switch (${InvokedCmdletName}) { - 'Register-DLL' { [string]$DLLAction = 'Register' } - 'Unregister-DLL' { [string]$DLLAction = 'Unregister' } - } - } - # Set the correct DLL register/unregister action parameters - If (-not $DLLAction) { Throw 'Parameter validation failed. Please specify the [-DLLAction] parameter to determine whether to register or unregister the DLL.' } - [string]$DLLAction = ((Get-Culture).TextInfo).ToTitleCase($DLLAction.ToLower()) - Switch ($DLLAction) { - 'Register' { [string]$DLLActionParameters = "/s `"$FilePath`"" } - 'Unregister' { [string]$DLLActionParameters = "/s /u `"$FilePath`"" } - } - } - Process { - Try { - Write-Log -Message "$DLLAction DLL file [$filePath]." -Source ${CmdletName} - If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } - - [string]$DLLFileBitness = Get-PEFileArchitecture -FilePath $filePath -ContinueOnError $false -ErrorAction 'Stop' - If (($DLLFileBitness -ne '64BIT') -and ($DLLFileBitness -ne '32BIT')) { - Throw "File [$filePath] has a detected file architecture of [$DLLFileBitness]. Only 32-bit or 64-bit DLL files can be $($DLLAction.ToLower() + 'ed')." - } - - If ($Is64Bit) { - If ($DLLFileBitness -eq '64BIT') { - If ($Is64BitProcess) { - [string]$RegSvr32Path = "$envWinDir\system32\regsvr32.exe" - } - Else { - [string]$RegSvr32Path = "$envWinDir\sysnative\regsvr32.exe" - } - } - ElseIf ($DLLFileBitness -eq '32BIT') { - [string]$RegSvr32Path = "$envWinDir\SysWOW64\regsvr32.exe" - } - } - Else { - If ($DLLFileBitness -eq '64BIT') { - Throw "File [$filePath] cannot be $($DLLAction.ToLower()) because it is a 64-bit file on a 32-bit operating system." - } - ElseIf ($DLLFileBitness -eq '32BIT') { - [string]$RegSvr32Path = "$envWinDir\system32\regsvr32.exe" - } - } - - [psobject]$ExecuteResult = Execute-Process -Path $RegSvr32Path -Parameters $DLLActionParameters -WindowStyle 'Hidden' -PassThru - - If ($ExecuteResult.ExitCode -ne 0) { - If ($ExecuteResult.ExitCode -eq 60002) { - Throw "Execute-Process function failed with exit code [$($ExecuteResult.ExitCode)]." - } - Else { - Throw "regsvr32.exe failed with exit code [$($ExecuteResult.ExitCode)]." - } - } - } - Catch { - Write-Log -Message "Failed to $($DLLAction.ToLower()) DLL file. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to $($DLLAction.ToLower()) DLL file: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$FilePath, + [Parameter(Mandatory=$false)] + [ValidateSet('Register','Unregister')] + [string]$DLLAction, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Get name used to invoke this function in case the 'Register-DLL' or 'Unregister-DLL' alias was used and set the correct DLL action + [string]${InvokedCmdletName} = $MyInvocation.InvocationName + # Set the correct register/unregister action based on the alias used to invoke this function + If (${InvokedCmdletName} -ne ${CmdletName}) { + Switch (${InvokedCmdletName}) { + 'Register-DLL' { [string]$DLLAction = 'Register' } + 'Unregister-DLL' { [string]$DLLAction = 'Unregister' } + } + } + # Set the correct DLL register/unregister action parameters + If (-not $DLLAction) { Throw 'Parameter validation failed. Please specify the [-DLLAction] parameter to determine whether to register or unregister the DLL.' } + [string]$DLLAction = ((Get-Culture).TextInfo).ToTitleCase($DLLAction.ToLower()) + Switch ($DLLAction) { + 'Register' { [string]$DLLActionParameters = "/s `"$FilePath`"" } + 'Unregister' { [string]$DLLActionParameters = "/s /u `"$FilePath`"" } + } + } + Process { + Try { + Write-Log -Message "$DLLAction DLL file [$filePath]." -Source ${CmdletName} + If (-not (Test-Path -LiteralPath $FilePath -PathType 'Leaf')) { Throw "File [$filePath] could not be found." } + + [string]$DLLFileBitness = Get-PEFileArchitecture -FilePath $filePath -ContinueOnError $false -ErrorAction 'Stop' + If (($DLLFileBitness -ne '64BIT') -and ($DLLFileBitness -ne '32BIT')) { + Throw "File [$filePath] has a detected file architecture of [$DLLFileBitness]. Only 32-bit or 64-bit DLL files can be $($DLLAction.ToLower() + 'ed')." + } + + If ($Is64Bit) { + If ($DLLFileBitness -eq '64BIT') { + If ($Is64BitProcess) { + [string]$RegSvr32Path = "$envWinDir\system32\regsvr32.exe" + } + Else { + [string]$RegSvr32Path = "$envWinDir\sysnative\regsvr32.exe" + } + } + ElseIf ($DLLFileBitness -eq '32BIT') { + [string]$RegSvr32Path = "$envWinDir\SysWOW64\regsvr32.exe" + } + } + Else { + If ($DLLFileBitness -eq '64BIT') { + Throw "File [$filePath] cannot be $($DLLAction.ToLower()) because it is a 64-bit file on a 32-bit operating system." + } + ElseIf ($DLLFileBitness -eq '32BIT') { + [string]$RegSvr32Path = "$envWinDir\system32\regsvr32.exe" + } + } + + [psobject]$ExecuteResult = Execute-Process -Path $RegSvr32Path -Parameters $DLLActionParameters -WindowStyle 'Hidden' -PassThru -ExitOnProcessFailure $false + + If ($ExecuteResult.ExitCode -ne 0) { + If ($ExecuteResult.ExitCode -eq 60002) { + Throw "Execute-Process function failed with exit code [$($ExecuteResult.ExitCode)]." + } + Else { + Throw "regsvr32.exe failed with exit code [$($ExecuteResult.ExitCode)]." + } + } + } + Catch { + Write-Log -Message "Failed to $($DLLAction.ToLower()) DLL file. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to $($DLLAction.ToLower()) DLL file: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } Set-Alias -Name 'Register-DLL' -Value 'Invoke-RegisterOrUnregisterDLL' -Scope 'Script' -Force -ErrorAction 'SilentlyContinue' Set-Alias -Name 'Unregister-DLL' -Value 'Invoke-RegisterOrUnregisterDLL' -Scope 'Script' -Force -ErrorAction 'SilentlyContinue' @@ -8138,57 +8330,57 @@ Set-Alias -Name 'Unregister-DLL' -Value 'Invoke-RegisterOrUnregisterDLL' -Scope Function Invoke-ObjectMethod { <# .SYNOPSIS - Invoke method on any object. + Invoke method on any object. .DESCRIPTION - Invoke method on any object with or without using named parameters. + Invoke method on any object with or without using named parameters. .PARAMETER InputObject - Specifies an object which has methods that can be invoked. + Specifies an object which has methods that can be invoked. .PARAMETER MethodName - Specifies the name of a method to invoke. + Specifies the name of a method to invoke. .PARAMETER ArgumentList - Argument to pass to the method being executed. Allows execution of method without specifying named parameters. + Argument to pass to the method being executed. Allows execution of method without specifying named parameters. .PARAMETER Parameter - Argument to pass to the method being executed. Allows execution of method by using named parameters. + Argument to pass to the method being executed. Allows execution of method by using named parameters. .EXAMPLE - $ShellApp = New-Object -ComObject 'Shell.Application' - $null = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'MinimizeAll' - Minimizes all windows. + $ShellApp = New-Object -ComObject 'Shell.Application' + $null = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'MinimizeAll' + Minimizes all windows. .EXAMPLE - $ShellApp = New-Object -ComObject 'Shell.Application' - $null = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'Explore' -Parameter @{'vDir'='C:\Windows'} - Opens the C:\Windows folder in a Windows Explorer window. + $ShellApp = New-Object -ComObject 'Shell.Application' + $null = Invoke-ObjectMethod -InputObject $ShellApp -MethodName 'Explore' -Parameter @{'vDir'='C:\Windows'} + Opens the C:\Windows folder in a Windows Explorer window. .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding(DefaultParameterSetName='Positional')] - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNull()] - [object]$InputObject, - [Parameter(Mandatory=$true,Position=1)] - [ValidateNotNullorEmpty()] - [string]$MethodName, - [Parameter(Mandatory=$false,Position=2,ParameterSetName='Positional')] - [object[]]$ArgumentList, - [Parameter(Mandatory=$true,Position=2,ParameterSetName='Named')] - [ValidateNotNull()] - [hashtable]$Parameter - ) - - Begin { } - Process { - If ($PSCmdlet.ParameterSetName -eq 'Named') { - ## Invoke method by using parameter names - Write-Output -InputObject $InputObject.GetType().InvokeMember($MethodName, [Reflection.BindingFlags]::InvokeMethod, $null, $InputObject, ([object[]]($Parameter.Values)), $null, $null, ([string[]]($Parameter.Keys))) - } - Else { - ## Invoke method without using parameter names - Write-Output -InputObject $InputObject.GetType().InvokeMember($MethodName, [Reflection.BindingFlags]::InvokeMethod, $null, $InputObject, $ArgumentList, $null, $null, $null) - } - } - End { } + [CmdletBinding(DefaultParameterSetName='Positional')] + Param ( + [Parameter(Mandatory=$true,Position=0)] + [ValidateNotNull()] + [object]$InputObject, + [Parameter(Mandatory=$true,Position=1)] + [ValidateNotNullorEmpty()] + [string]$MethodName, + [Parameter(Mandatory=$false,Position=2,ParameterSetName='Positional')] + [object[]]$ArgumentList, + [Parameter(Mandatory=$true,Position=2,ParameterSetName='Named')] + [ValidateNotNull()] + [hashtable]$Parameter + ) + + Begin { } + Process { + If ($PSCmdlet.ParameterSetName -eq 'Named') { + ## Invoke method by using parameter names + Write-Output -InputObject $InputObject.GetType().InvokeMember($MethodName, [Reflection.BindingFlags]::InvokeMethod, $null, $InputObject, ([object[]]($Parameter.Values)), $null, $null, ([string[]]($Parameter.Keys))) + } + Else { + ## Invoke method without using parameter names + Write-Output -InputObject $InputObject.GetType().InvokeMember($MethodName, [Reflection.BindingFlags]::InvokeMethod, $null, $InputObject, $ArgumentList, $null, $null, $null) + } + } + End { } } #endregion @@ -8197,40 +8389,40 @@ Function Invoke-ObjectMethod { Function Get-ObjectProperty { <# .SYNOPSIS - Get a property from any object. + Get a property from any object. .DESCRIPTION - Get a property from any object. + Get a property from any object. .PARAMETER InputObject - Specifies an object which has properties that can be retrieved. + Specifies an object which has properties that can be retrieved. .PARAMETER PropertyName - Specifies the name of a property to retrieve. + Specifies the name of a property to retrieve. .PARAMETER ArgumentList - Argument to pass to the property being retrieved. + Argument to pass to the property being retrieved. .EXAMPLE - Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @(1) + Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @(1) .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,Position=0)] - [ValidateNotNull()] - [object]$InputObject, - [Parameter(Mandatory=$true,Position=1)] - [ValidateNotNullorEmpty()] - [string]$PropertyName, - [Parameter(Mandatory=$false,Position=2)] - [object[]]$ArgumentList - ) - - Begin { } - Process { - ## Retrieve property - Write-Output -InputObject $InputObject.GetType().InvokeMember($PropertyName, [Reflection.BindingFlags]::GetProperty, $null, $InputObject, $ArgumentList, $null, $null, $null) - } - End { } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,Position=0)] + [ValidateNotNull()] + [object]$InputObject, + [Parameter(Mandatory=$true,Position=1)] + [ValidateNotNullorEmpty()] + [string]$PropertyName, + [Parameter(Mandatory=$false,Position=2)] + [object[]]$ArgumentList + ) + + Begin { } + Process { + ## Retrieve property + Write-Output -InputObject $InputObject.GetType().InvokeMember($PropertyName, [Reflection.BindingFlags]::GetProperty, $null, $InputObject, $ArgumentList, $null, $null, $null) + } + End { } } #endregion @@ -8239,167 +8431,167 @@ Function Get-ObjectProperty { Function Get-MsiTableProperty { <# .SYNOPSIS - Get all of the properties from a Windows Installer database table or the Summary Information stream and return as a custom object. + Get all of the properties from a Windows Installer database table or the Summary Information stream and return as a custom object. .DESCRIPTION - Use the Windows Installer object to read all of the properties from a Windows Installer database table or the Summary Information stream. + Use the Windows Installer object to read all of the properties from a Windows Installer database table or the Summary Information stream. .PARAMETER Path - The fully qualified path to an database file. Supports .msi and .msp files. + The fully qualified path to an database file. Supports .msi and .msp files. .PARAMETER TransformPath - The fully qualified path to a list of MST file(s) which should be applied to the MSI file. + The fully qualified path to a list of MST file(s) which should be applied to the MSI file. .PARAMETER Table - The name of the the MSI table from which all of the properties must be retrieved. Default is: 'Property'. + The name of the the MSI table from which all of the properties must be retrieved. Default is: 'Property'. .PARAMETER TablePropertyNameColumnNum - Specify the table column number which contains the name of the properties. Default is: 1 for MSIs and 2 for MSPs. + Specify the table column number which contains the name of the properties. Default is: 1 for MSIs and 2 for MSPs. .PARAMETER TablePropertyValueColumnNum - Specify the table column number which contains the value of the properties. Default is: 2 for MSIs and 3 for MSPs. + Specify the table column number which contains the value of the properties. Default is: 2 for MSIs and 3 for MSPs. .PARAMETER GetSummaryInformation - Retrieves the Summary Information for the Windows Installer database. - Summary Information property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx + Retrieves the Summary Information for the Windows Installer database. + Summary Information property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' - Retrieve all of the properties from the default 'Property' table. + Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' + Retrieve all of the properties from the default 'Property' table. .EXAMPLE - Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' -Table 'Property' | Select-Object -ExpandProperty ProductCode - Retrieve all of the properties from the 'Property' table and then pipe to Select-Object to select the ProductCode property. + Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -TransformPath 'C:\Package\AppDeploy.mst' -Table 'Property' | Select-Object -ExpandProperty ProductCode + Retrieve all of the properties from the 'Property' table and then pipe to Select-Object to select the ProductCode property. .EXAMPLE - Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -GetSummaryInformation - Retrieves the Summary Information for the Windows Installer database. + Get-MsiTableProperty -Path 'C:\Package\AppDeploy.msi' -GetSummaryInformation + Retrieves the Summary Information for the Windows Installer database. .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding(DefaultParameterSetName='TableInfo')] - Param ( - [Parameter(Mandatory=$true)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [string]$Path, - [Parameter(Mandatory=$false)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [string[]]$TransformPath, - [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] - [ValidateNotNullOrEmpty()] - [string]$Table = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 'Property' } Else { 'MsiPatchMetadata' }), - [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] - [ValidateNotNullorEmpty()] - [int32]$TablePropertyNameColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 1 } Else { 2 }), - [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] - [ValidateNotNullorEmpty()] - [int32]$TablePropertyValueColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 2 } Else { 3 }), - [Parameter(Mandatory=$true,ParameterSetName='SummaryInfo')] - [ValidateNotNullorEmpty()] - [switch]$GetSummaryInformation = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - If ($PSCmdlet.ParameterSetName -eq 'TableInfo') { - Write-Log -Message "Read data from Windows Installer database file [$Path] in table [$Table]." -Source ${CmdletName} - } - Else { - Write-Log -Message "Read the Summary Information from the Windows Installer database file [$Path]." -Source ${CmdletName} - } - - ## Create a Windows Installer object - [__comobject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop' - ## Determine if the database file is a patch (.msp) or not - If ([IO.Path]::GetExtension($Path) -eq '.msp') { [boolean]$IsMspFile = $true } - ## Define properties for how the MSI database is opened - [int32]$msiOpenDatabaseModeReadOnly = 0 - [int32]$msiSuppressApplyTransformErrors = 63 - [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModeReadOnly - [int32]$msiOpenDatabaseModePatchFile = 32 - If ($IsMspFile) { [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModePatchFile } - ## Open database in read only mode - [__comobject]$Database = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($Path, $msiOpenDatabaseMode) - ## Apply a list of transform(s) to the database - If (($TransformPath) -and (-not $IsMspFile)) { - ForEach ($Transform in $TransformPath) { - $null = Invoke-ObjectMethod -InputObject $Database -MethodName 'ApplyTransform' -ArgumentList @($Transform, $msiSuppressApplyTransformErrors) - } - } - - ## Get either the requested windows database table information or summary information - If ($PSCmdlet.ParameterSetName -eq 'TableInfo') { - ## Open the requested table view from the database - [__comobject]$View = Invoke-ObjectMethod -InputObject $Database -MethodName 'OpenView' -ArgumentList @("SELECT * FROM $Table") - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' - - ## Create an empty object to store properties in - [psobject]$TableProperties = New-Object -TypeName 'PSObject' - - ## Retrieve the first row from the requested table. If the first row was successfully retrieved, then save data and loop through the entire table. - # https://msdn.microsoft.com/en-us/library/windows/desktop/aa371136(v=vs.85).aspx - [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' - While ($Record) { - # Read string data from record and add property/value pair to custom object - $TableProperties | Add-Member -MemberType 'NoteProperty' -Name (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyNameColumnNum)) -Value (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyValueColumnNum)) -Force - # Retrieve the next row in the table - [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' - } - Write-Output -InputObject $TableProperties - } - Else { - ## Get the SummaryInformation from the windows installer database - [__comobject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation' - [hashtable]$SummaryInfoProperty = @{} - ## Summary property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx - $SummaryInfoProperty.Add('CodePage', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(1))) - $SummaryInfoProperty.Add('Title', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(2))) - $SummaryInfoProperty.Add('Subject', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(3))) - $SummaryInfoProperty.Add('Author', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(4))) - $SummaryInfoProperty.Add('Keywords', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(5))) - $SummaryInfoProperty.Add('Comments', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(6))) - $SummaryInfoProperty.Add('Template', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7))) - $SummaryInfoProperty.Add('LastSavedBy', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(8))) - $SummaryInfoProperty.Add('RevisionNumber', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(9))) - $SummaryInfoProperty.Add('LastPrinted', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(11))) - $SummaryInfoProperty.Add('CreateTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(12))) - $SummaryInfoProperty.Add('LastSaveTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(13))) - $SummaryInfoProperty.Add('PageCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(14))) - $SummaryInfoProperty.Add('WordCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(15))) - $SummaryInfoProperty.Add('CharacterCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(16))) - $SummaryInfoProperty.Add('CreatingApplication', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(18))) - $SummaryInfoProperty.Add('Security', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(19))) - [psobject]$SummaryInfoProperties = New-Object -TypeName 'PSObject' -Property $SummaryInfoProperty - Write-Output -InputObject $SummaryInfoProperties - } - } - Catch { - Write-Log -Message "Failed to get the MSI table [$Table]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to get the MSI table [$Table]: $($_.Exception.Message)" - } - } - Finally { - Try { - If ($View) { - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) } Catch { } - } - ElseIf($SummaryInformation) { - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation) } Catch { } - } - } - Catch { } - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($DataBase) } Catch { } - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding(DefaultParameterSetName='TableInfo')] + Param ( + [Parameter(Mandatory=$true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [string]$Path, + [Parameter(Mandatory=$false)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [string[]]$TransformPath, + [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] + [ValidateNotNullOrEmpty()] + [string]$Table = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 'Property' } Else { 'MsiPatchMetadata' }), + [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] + [ValidateNotNullorEmpty()] + [int32]$TablePropertyNameColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 1 } Else { 2 }), + [Parameter(Mandatory=$false,ParameterSetName='TableInfo')] + [ValidateNotNullorEmpty()] + [int32]$TablePropertyValueColumnNum = $(If ([IO.Path]::GetExtension($Path) -eq '.msi') { 2 } Else { 3 }), + [Parameter(Mandatory=$true,ParameterSetName='SummaryInfo')] + [ValidateNotNullorEmpty()] + [switch]$GetSummaryInformation = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + If ($PSCmdlet.ParameterSetName -eq 'TableInfo') { + Write-Log -Message "Read data from Windows Installer database file [$Path] in table [$Table]." -Source ${CmdletName} + } + Else { + Write-Log -Message "Read the Summary Information from the Windows Installer database file [$Path]." -Source ${CmdletName} + } + + ## Create a Windows Installer object + [__comobject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop' + ## Determine if the database file is a patch (.msp) or not + If ([IO.Path]::GetExtension($Path) -eq '.msp') { [boolean]$IsMspFile = $true } + ## Define properties for how the MSI database is opened + [int32]$msiOpenDatabaseModeReadOnly = 0 + [int32]$msiSuppressApplyTransformErrors = 63 + [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModeReadOnly + [int32]$msiOpenDatabaseModePatchFile = 32 + If ($IsMspFile) { [int32]$msiOpenDatabaseMode = $msiOpenDatabaseModePatchFile } + ## Open database in read only mode + [__comobject]$Database = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($Path, $msiOpenDatabaseMode) + ## Apply a list of transform(s) to the database + If (($TransformPath) -and (-not $IsMspFile)) { + ForEach ($Transform in $TransformPath) { + $null = Invoke-ObjectMethod -InputObject $Database -MethodName 'ApplyTransform' -ArgumentList @($Transform, $msiSuppressApplyTransformErrors) + } + } + + ## Get either the requested windows database table information or summary information + If ($PSCmdlet.ParameterSetName -eq 'TableInfo') { + ## Open the requested table view from the database + [__comobject]$View = Invoke-ObjectMethod -InputObject $Database -MethodName 'OpenView' -ArgumentList @("SELECT * FROM $Table") + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' + + ## Create an empty object to store properties in + [psobject]$TableProperties = New-Object -TypeName 'PSObject' + + ## Retrieve the first row from the requested table. If the first row was successfully retrieved, then save data and loop through the entire table. + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa371136(v=vs.85).aspx + [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' + While ($Record) { + # Read string data from record and add property/value pair to custom object + $TableProperties | Add-Member -MemberType 'NoteProperty' -Name (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyNameColumnNum)) -Value (Get-ObjectProperty -InputObject $Record -PropertyName 'StringData' -ArgumentList @($TablePropertyValueColumnNum)) -Force + # Retrieve the next row in the table + [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' + } + Write-Output -InputObject $TableProperties + } + Else { + ## Get the SummaryInformation from the windows installer database + [__comobject]$SummaryInformation = Get-ObjectProperty -InputObject $Database -PropertyName 'SummaryInformation' + [hashtable]$SummaryInfoProperty = @{} + ## Summary property descriptions: https://msdn.microsoft.com/en-us/library/aa372049(v=vs.85).aspx + $SummaryInfoProperty.Add('CodePage', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(1))) + $SummaryInfoProperty.Add('Title', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(2))) + $SummaryInfoProperty.Add('Subject', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(3))) + $SummaryInfoProperty.Add('Author', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(4))) + $SummaryInfoProperty.Add('Keywords', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(5))) + $SummaryInfoProperty.Add('Comments', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(6))) + $SummaryInfoProperty.Add('Template', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(7))) + $SummaryInfoProperty.Add('LastSavedBy', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(8))) + $SummaryInfoProperty.Add('RevisionNumber', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(9))) + $SummaryInfoProperty.Add('LastPrinted', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(11))) + $SummaryInfoProperty.Add('CreateTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(12))) + $SummaryInfoProperty.Add('LastSaveTimeDate', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(13))) + $SummaryInfoProperty.Add('PageCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(14))) + $SummaryInfoProperty.Add('WordCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(15))) + $SummaryInfoProperty.Add('CharacterCount', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(16))) + $SummaryInfoProperty.Add('CreatingApplication', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(18))) + $SummaryInfoProperty.Add('Security', (Get-ObjectProperty -InputObject $SummaryInformation -PropertyName 'Property' -ArgumentList @(19))) + [psobject]$SummaryInfoProperties = New-Object -TypeName 'PSObject' -Property $SummaryInfoProperty + Write-Output -InputObject $SummaryInfoProperties + } + } + Catch { + Write-Log -Message "Failed to get the MSI table [$Table]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to get the MSI table [$Table]: $($_.Exception.Message)" + } + } + Finally { + Try { + If ($View) { + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) } Catch { } + } + ElseIf($SummaryInformation) { + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SummaryInformation) } Catch { } + } + } + Catch { } + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($DataBase) } Catch { } + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8408,93 +8600,93 @@ Function Get-MsiTableProperty { Function Set-MsiProperty { <# .SYNOPSIS - Set a property in the MSI property table. + Set a property in the MSI property table. .DESCRIPTION - Set a property in the MSI property table. + Set a property in the MSI property table. .PARAMETER DataBase - Specify a ComObject representing an MSI database opened in view/modify/update mode. + Specify a ComObject representing an MSI database opened in view/modify/update mode. .PARAMETER PropertyName - The name of the property to be set/modified. + The name of the property to be set/modified. .PARAMETER PropertyValue - The value of the property to be set/modified. + The value of the property to be set/modified. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Set-MsiProperty -DataBase $TempMsiPathDatabase -PropertyName 'ALLUSERS' -PropertyValue '1' + Set-MsiProperty -DataBase $TempMsiPathDatabase -PropertyName 'ALLUSERS' -PropertyValue '1' .NOTES - This is an internal script function and should typically not be called directly. + This is an internal script function and should typically not be called directly. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [__comobject]$DataBase, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$PropertyName, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$PropertyValue, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]." -Source ${CmdletName} - - ## Open the requested table view from the database - [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("SELECT * FROM Property WHERE Property='$PropertyName'") - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' - - ## Retrieve the requested property from the requested table. - # https://msdn.microsoft.com/en-us/library/windows/desktop/aa371136(v=vs.85).aspx - [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' - - ## Close the previous view on the MSI database - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) - - ## Set the MSI property - If ($Record) { - # If the property already exists, then create the view for updating the property - [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("UPDATE Property SET Value='$PropertyValue' WHERE Property='$PropertyName'") - } - Else { - # If property does not exist, then create view for inserting the property - [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("INSERT INTO Property (Property, Value) VALUES ('$PropertyName','$PropertyValue')") - } - # Execute the view to set the MSI property - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' - } - Catch { - Write-Log -Message "Failed to set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]: $($_.Exception.Message)" - } - } - Finally { - Try { - If ($View) { - $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) - } - } - Catch { } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [__comobject]$DataBase, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$PropertyName, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$PropertyValue, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]." -Source ${CmdletName} + + ## Open the requested table view from the database + [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("SELECT * FROM Property WHERE Property='$PropertyName'") + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' + + ## Retrieve the requested property from the requested table. + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa371136(v=vs.85).aspx + [__comobject]$Record = Invoke-ObjectMethod -InputObject $View -MethodName 'Fetch' + + ## Close the previous view on the MSI database + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) + + ## Set the MSI property + If ($Record) { + # If the property already exists, then create the view for updating the property + [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("UPDATE Property SET Value='$PropertyValue' WHERE Property='$PropertyName'") + } + Else { + # If property does not exist, then create view for inserting the property + [__comobject]$View = Invoke-ObjectMethod -InputObject $DataBase -MethodName 'OpenView' -ArgumentList @("INSERT INTO Property (Property, Value) VALUES ('$PropertyName','$PropertyValue')") + } + # Execute the view to set the MSI property + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Execute' + } + Catch { + Write-Log -Message "Failed to set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to set the MSI Property Name [$PropertyName] with Property Value [$PropertyValue]: $($_.Exception.Message)" + } + } + Finally { + Try { + If ($View) { + $null = Invoke-ObjectMethod -InputObject $View -MethodName 'Close' -ArgumentList @() + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($View) + } + } + Catch { } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8503,165 +8695,165 @@ Function Set-MsiProperty { Function New-MsiTransform { <# .SYNOPSIS - Create a transform file for an MSI database. + Create a transform file for an MSI database. .DESCRIPTION - Create a transform file for an MSI database and create/modify properties in the Properties table. + Create a transform file for an MSI database and create/modify properties in the Properties table. .PARAMETER MsiPath - Specify the path to an MSI file. + Specify the path to an MSI file. .PARAMETER ApplyTransformPath - Specify the path to a transform which should be applied to the MSI database before any new properties are created or modified. + Specify the path to a transform which should be applied to the MSI database before any new properties are created or modified. .PARAMETER NewTransformPath - Specify the path where the new transform file with the desired properties will be created. If a transform file of the same name already exists, it will be deleted before a new one is created. - Default is: a) If -ApplyTransformPath was specified but not -NewTransformPath, then .new.mst - b) If only -MsiPath was specified, then .mst + Specify the path where the new transform file with the desired properties will be created. If a transform file of the same name already exists, it will be deleted before a new one is created. + Default is: a) If -ApplyTransformPath was specified but not -NewTransformPath, then .new.mst + b) If only -MsiPath was specified, then .mst .PARAMETER TransformProperties - Hashtable which contains calls to Set-MsiProperty for configuring the desired properties which should be included in new transform file. - Example hashtable: [hashtable]$TransformProperties = @{ 'ALLUSERS' = '1' } + Hashtable which contains calls to Set-MsiProperty for configuring the desired properties which should be included in new transform file. + Example hashtable: [hashtable]$TransformProperties = @{ 'ALLUSERS' = '1' } .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. -.EXAMPLE - [hashtable]$TransformProperties = { - 'ALLUSERS' = '1' - 'AgreeToLicense' = 'Yes' - 'REBOOT' = 'ReallySuppress' - 'RebootYesNo' = 'No' - 'ROOTDRIVE' = 'C:' - } - New-MsiTransform -MsiPath 'C:\Temp\PSADTInstall.msi' -TransformProperties $TransformProperties + Continue if an error is encountered. Default is: $true. +.EXAMPLE + [hashtable]$TransformProperties = { + 'ALLUSERS' = '1' + 'AgreeToLicense' = 'Yes' + 'REBOOT' = 'ReallySuppress' + 'RebootYesNo' = 'No' + 'ROOTDRIVE' = 'C:' + } + New-MsiTransform -MsiPath 'C:\Temp\PSADTInstall.msi' -TransformProperties $TransformProperties .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [string]$MsiPath, - [Parameter(Mandatory=$false)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] - [string]$ApplyTransformPath, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$NewTransformPath, - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [hashtable]$TransformProperties, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Define properties for how the MSI database is opened - [int32]$msiOpenDatabaseModeReadOnly = 0 - [int32]$msiOpenDatabaseModeTransact = 1 - [int32]$msiViewModifyUpdate = 2 - [int32]$msiViewModifyReplace = 4 - [int32]$msiViewModifyDelete = 6 - [int32]$msiTransformErrorNone = 0 - [int32]$msiTransformValidationNone = 0 - [int32]$msiSuppressApplyTransformErrors = 63 - } - Process { - Try { - Write-Log -Message "Create a transform file for MSI [$MsiPath]." -Source ${CmdletName} - - ## Discover the parent folder that the MSI file resides in - [string]$MsiParentFolder = Split-Path -Path $MsiPath -Parent -ErrorAction 'Stop' - - ## Create a temporary file name for storing a second copy of the MSI database - [string]$TempMsiPath = Join-Path -Path $MsiParentFolder -ChildPath ([IO.Path]::GetFileName(([IO.Path]::GetTempFileName()))) -ErrorAction 'Stop' - - ## Create a second copy of the MSI database - Write-Log -Message "Copy MSI database in path [$MsiPath] to destination [$TempMsiPath]." -Source ${CmdletName} - $null = Copy-Item -LiteralPath $MsiPath -Destination $TempMsiPath -Force -ErrorAction 'Stop' - - ## Create a Windows Installer object - [__comobject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop' - - ## Open both copies of the MSI database - # Open the original MSI database in read only mode - Write-Log -Message "Open the MSI database [$MsiPath] in read only mode." -Source ${CmdletName} - [__comobject]$MsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($MsiPath, $msiOpenDatabaseModeReadOnly) - # Open the temporary copy of the MSI database in view/modify/update mode - Write-Log -Message "Open the MSI database [$TempMsiPath] in view/modify/update mode." -Source ${CmdletName} - [__comobject]$TempMsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($TempMsiPath, $msiViewModifyUpdate) - - ## If a MSI transform file was specified, then apply it to the temporary copy of the MSI database - If ($ApplyTransformPath) { - Write-Log -Message "Apply transform file [$ApplyTransformPath] to MSI database [$TempMsiPath]." -Source ${CmdletName} - $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'ApplyTransform' -ArgumentList @($ApplyTransformPath, $msiSuppressApplyTransformErrors) - } - - ## Determine the path for the new transform file that will be generated - If (-not $NewTransformPath) { - If ($ApplyTransformPath) { - [string]$NewTransformFileName = [IO.Path]::GetFileNameWithoutExtension($ApplyTransformPath) + '.new' + [IO.Path]::GetExtension($ApplyTransformPath) - } - Else { - [string]$NewTransformFileName = [IO.Path]::GetFileNameWithoutExtension($MsiPath) + '.mst' - } - [string]$NewTransformPath = Join-Path -Path $MsiParentFolder -ChildPath $NewTransformFileName -ErrorAction 'Stop' - } - - ## Set the MSI properties in the temporary copy of the MSI database - $TransformProperties.GetEnumerator() | ForEach-Object { Set-MsiProperty -DataBase $TempMsiPathDatabase -PropertyName $_.Key -PropertyValue $_.Value } - - ## Commit the new properties to the temporary copy of the MSI database - $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'Commit' - - ## Reopen the temporary copy of the MSI database in read only mode - # Release the database object for the temporary copy of the MSI database - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($TempMsiPathDatabase) - # Open the temporary copy of the MSI database in read only mode - Write-Log -Message "Re-open the MSI database [$TempMsiPath] in read only mode." -Source ${CmdletName} - [__comobject]$TempMsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($TempMsiPath, $msiOpenDatabaseModeReadOnly) - - ## Delete the new transform file path if it already exists - If (Test-Path -LiteralPath $NewTransformPath -PathType 'Leaf' -ErrorAction 'Stop') { - Write-Log -Message "A transform file of the same name already exists. Deleting transform file [$NewTransformPath]." -Source ${CmdletName} - $null = Remove-Item -LiteralPath $NewTransformPath -Force -ErrorAction 'Stop' - } - - ## Generate the new transform file by taking the difference between the temporary copy of the MSI database and the original MSI database - Write-Log -Message "Generate new transform file [$NewTransformPath]." -Source ${CmdletName} - $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'GenerateTransform' -ArgumentList @($MsiPathDatabase, $NewTransformPath) - $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'CreateTransformSummaryInfo' -ArgumentList @($MsiPathDatabase, $NewTransformPath, $msiTransformErrorNone, $msiTransformValidationNone) - - If (Test-Path -LiteralPath $NewTransformPath -PathType 'Leaf' -ErrorAction 'Stop') { - Write-Log -Message "Successfully created new transform file in path [$NewTransformPath]." -Source ${CmdletName} - } - Else { - Throw "Failed to generate transform file in path [$NewTransformPath]." - } - } - Catch { - Write-Log -Message "Failed to create new transform file in path [$NewTransformPath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to create new transform file in path [$NewTransformPath]: $($_.Exception.Message)" - } - } - Finally { - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($TempMsiPathDatabase) } Catch { } - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($MsiPathDatabase) } Catch { } - Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } - Try { - ## Delete the temporary copy of the MSI database - If (Test-Path -LiteralPath $TempMsiPath -PathType 'Leaf' -ErrorAction 'Stop') { - $null = Remove-Item -LiteralPath $TempMsiPath -Force -ErrorAction 'Stop' - } - } - Catch { } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [string]$MsiPath, + [Parameter(Mandatory=$false)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType 'Leaf' })] + [string]$ApplyTransformPath, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$NewTransformPath, + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [hashtable]$TransformProperties, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Define properties for how the MSI database is opened + [int32]$msiOpenDatabaseModeReadOnly = 0 + [int32]$msiOpenDatabaseModeTransact = 1 + [int32]$msiViewModifyUpdate = 2 + [int32]$msiViewModifyReplace = 4 + [int32]$msiViewModifyDelete = 6 + [int32]$msiTransformErrorNone = 0 + [int32]$msiTransformValidationNone = 0 + [int32]$msiSuppressApplyTransformErrors = 63 + } + Process { + Try { + Write-Log -Message "Create a transform file for MSI [$MsiPath]." -Source ${CmdletName} + + ## Discover the parent folder that the MSI file resides in + [string]$MsiParentFolder = Split-Path -Path $MsiPath -Parent -ErrorAction 'Stop' + + ## Create a temporary file name for storing a second copy of the MSI database + [string]$TempMsiPath = Join-Path -Path $MsiParentFolder -ChildPath ([IO.Path]::GetFileName(([IO.Path]::GetTempFileName()))) -ErrorAction 'Stop' + + ## Create a second copy of the MSI database + Write-Log -Message "Copy MSI database in path [$MsiPath] to destination [$TempMsiPath]." -Source ${CmdletName} + $null = Copy-Item -LiteralPath $MsiPath -Destination $TempMsiPath -Force -ErrorAction 'Stop' + + ## Create a Windows Installer object + [__comobject]$Installer = New-Object -ComObject 'WindowsInstaller.Installer' -ErrorAction 'Stop' + + ## Open both copies of the MSI database + # Open the original MSI database in read only mode + Write-Log -Message "Open the MSI database [$MsiPath] in read only mode." -Source ${CmdletName} + [__comobject]$MsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($MsiPath, $msiOpenDatabaseModeReadOnly) + # Open the temporary copy of the MSI database in view/modify/update mode + Write-Log -Message "Open the MSI database [$TempMsiPath] in view/modify/update mode." -Source ${CmdletName} + [__comobject]$TempMsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($TempMsiPath, $msiViewModifyUpdate) + + ## If a MSI transform file was specified, then apply it to the temporary copy of the MSI database + If ($ApplyTransformPath) { + Write-Log -Message "Apply transform file [$ApplyTransformPath] to MSI database [$TempMsiPath]." -Source ${CmdletName} + $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'ApplyTransform' -ArgumentList @($ApplyTransformPath, $msiSuppressApplyTransformErrors) + } + + ## Determine the path for the new transform file that will be generated + If (-not $NewTransformPath) { + If ($ApplyTransformPath) { + [string]$NewTransformFileName = [IO.Path]::GetFileNameWithoutExtension($ApplyTransformPath) + '.new' + [IO.Path]::GetExtension($ApplyTransformPath) + } + Else { + [string]$NewTransformFileName = [IO.Path]::GetFileNameWithoutExtension($MsiPath) + '.mst' + } + [string]$NewTransformPath = Join-Path -Path $MsiParentFolder -ChildPath $NewTransformFileName -ErrorAction 'Stop' + } + + ## Set the MSI properties in the temporary copy of the MSI database + $TransformProperties.GetEnumerator() | ForEach-Object { Set-MsiProperty -DataBase $TempMsiPathDatabase -PropertyName $_.Key -PropertyValue $_.Value } + + ## Commit the new properties to the temporary copy of the MSI database + $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'Commit' + + ## Reopen the temporary copy of the MSI database in read only mode + # Release the database object for the temporary copy of the MSI database + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($TempMsiPathDatabase) + # Open the temporary copy of the MSI database in read only mode + Write-Log -Message "Re-open the MSI database [$TempMsiPath] in read only mode." -Source ${CmdletName} + [__comobject]$TempMsiPathDatabase = Invoke-ObjectMethod -InputObject $Installer -MethodName 'OpenDatabase' -ArgumentList @($TempMsiPath, $msiOpenDatabaseModeReadOnly) + + ## Delete the new transform file path if it already exists + If (Test-Path -LiteralPath $NewTransformPath -PathType 'Leaf' -ErrorAction 'Stop') { + Write-Log -Message "A transform file of the same name already exists. Deleting transform file [$NewTransformPath]." -Source ${CmdletName} + $null = Remove-Item -LiteralPath $NewTransformPath -Force -ErrorAction 'Stop' + } + + ## Generate the new transform file by taking the difference between the temporary copy of the MSI database and the original MSI database + Write-Log -Message "Generate new transform file [$NewTransformPath]." -Source ${CmdletName} + $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'GenerateTransform' -ArgumentList @($MsiPathDatabase, $NewTransformPath) + $null = Invoke-ObjectMethod -InputObject $TempMsiPathDatabase -MethodName 'CreateTransformSummaryInfo' -ArgumentList @($MsiPathDatabase, $NewTransformPath, $msiTransformErrorNone, $msiTransformValidationNone) + + If (Test-Path -LiteralPath $NewTransformPath -PathType 'Leaf' -ErrorAction 'Stop') { + Write-Log -Message "Successfully created new transform file in path [$NewTransformPath]." -Source ${CmdletName} + } + Else { + Throw "Failed to generate transform file in path [$NewTransformPath]." + } + } + Catch { + Write-Log -Message "Failed to create new transform file in path [$NewTransformPath]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to create new transform file in path [$NewTransformPath]: $($_.Exception.Message)" + } + } + Finally { + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($TempMsiPathDatabase) } Catch { } + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($MsiPathDatabase) } Catch { } + Try { $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($Installer) } Catch { } + Try { + ## Delete the temporary copy of the MSI database + If (Test-Path -LiteralPath $TempMsiPath -PathType 'Leaf' -ErrorAction 'Stop') { + $null = Remove-Item -LiteralPath $TempMsiPath -Force -ErrorAction 'Stop' + } + } + Catch { } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8670,100 +8862,100 @@ Function New-MsiTransform { Function Test-MSUpdates { <# .SYNOPSIS - Test whether a Microsoft Windows update is installed. + Test whether a Microsoft Windows update is installed. .DESCRIPTION - Test whether a Microsoft Windows update is installed. + Test whether a Microsoft Windows update is installed. .PARAMETER KBNumber - KBNumber of the update. + KBNumber of the update. .PARAMETER ContinueOnError - Suppress writing log message to console on failure to write message to log file. Default is: $true. + Suppress writing log message to console on failure to write message to log file. Default is: $true. .EXAMPLE - Test-MSUpdates -KBNumber 'KB2549864' + Test-MSUpdates -KBNumber 'KB2549864' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,Position=0,HelpMessage='Enter the KB Number for the Microsoft Update')] - [ValidateNotNullorEmpty()] - [string]$KBNumber, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Check if Microsoft Update [$kbNumber] is installed." -Source ${CmdletName} - - ## Default is not found - [boolean]$kbFound = $false - - ## Check for update using built in PS cmdlet which uses WMI in the background to gather details - Get-Hotfix -Id $kbNumber -ErrorAction 'SilentlyContinue' | ForEach-Object { $kbFound = $true } - - If (-not $kbFound) { - Write-Log -Message 'Unable to detect Windows update history via Get-Hotfix cmdlet. Trying via COM object.' -Source ${CmdletName} - - ## Check for update using ComObject method (to catch Office updates) - [__comobject]$UpdateSession = New-Object -ComObject "Microsoft.Update.Session" - [__comobject]$UpdateSearcher = $UpdateSession.CreateUpdateSearcher() - # Indicates whether the search results include updates that are superseded by other updates in the search results - $UpdateSearcher.IncludePotentiallySupersededUpdates = $false - # Indicates whether the UpdateSearcher goes online to search for updates. - $UpdateSearcher.Online = $false - [int32]$UpdateHistoryCount = $UpdateSearcher.GetTotalHistoryCount() - If ($UpdateHistoryCount -gt 0) { - [psobject]$UpdateHistory = $UpdateSearcher.QueryHistory(0, $UpdateHistoryCount) | - Select-Object -Property 'Title','Date', - @{Name = 'Operation'; Expression = { Switch ($_.Operation) { 1 {'Installation'}; 2 {'Uninstallation'}; 3 {'Other'} } } }, - @{Name = 'Status'; Expression = { Switch ($_.ResultCode) { 0 {'Not Started'}; 1 {'In Progress'}; 2 {'Successful'}; 3 {'Incomplete'}; 4 {'Failed'}; 5 {'Aborted'} } } }, - 'Description' | - Sort-Object -Property 'Date' -Descending - ForEach ($Update in $UpdateHistory) { - If (($Update.Operation -ne 'Other') -and ($Update.Title -match "\($KBNumber\)")) { - $LatestUpdateHistory = $Update - Break - } - } - If (($LatestUpdateHistory.Operation -eq 'Installation') -and ($LatestUpdateHistory.Status -eq 'Successful')) { - Write-Log -Message "Discovered the following Microsoft Update: `n$($LatestUpdateHistory | Format-List | Out-String)" -Source ${CmdletName} - $kbFound = $true - } - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSession) - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSearcher) - } - Else { - Write-Log -Message 'Unable to detect Windows update history via COM object.' -Source ${CmdletName} - } - } - - ## Return Result - If (-not $kbFound) { - Write-Log -Message "Microsoft Update [$kbNumber] is not installed." -Source ${CmdletName} - Write-Output -InputObject $false - } - Else { - Write-Log -Message "Microsoft Update [$kbNumber] is installed." -Source ${CmdletName} - Write-Output -InputObject $true - } - } - Catch { - Write-Log -Message "Failed discovering Microsoft Update [$kbNumber]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed discovering Microsoft Update [$kbNumber]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,Position=0,HelpMessage='Enter the KB Number for the Microsoft Update')] + [ValidateNotNullorEmpty()] + [string]$KBNumber, + [Parameter(Mandatory=$false,Position=1)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Check if Microsoft Update [$kbNumber] is installed." -Source ${CmdletName} + + ## Default is not found + [boolean]$kbFound = $false + + ## Check for update using built in PS cmdlet which uses WMI in the background to gather details + Get-Hotfix -Id $kbNumber -ErrorAction 'SilentlyContinue' | ForEach-Object { $kbFound = $true } + + If (-not $kbFound) { + Write-Log -Message 'Unable to detect Windows update history via Get-Hotfix cmdlet. Trying via COM object.' -Source ${CmdletName} + + ## Check for update using ComObject method (to catch Office updates) + [__comobject]$UpdateSession = New-Object -ComObject "Microsoft.Update.Session" + [__comobject]$UpdateSearcher = $UpdateSession.CreateUpdateSearcher() + # Indicates whether the search results include updates that are superseded by other updates in the search results + $UpdateSearcher.IncludePotentiallySupersededUpdates = $false + # Indicates whether the UpdateSearcher goes online to search for updates. + $UpdateSearcher.Online = $false + [int32]$UpdateHistoryCount = $UpdateSearcher.GetTotalHistoryCount() + If ($UpdateHistoryCount -gt 0) { + [psobject]$UpdateHistory = $UpdateSearcher.QueryHistory(0, $UpdateHistoryCount) | + Select-Object -Property 'Title','Date', + @{Name = 'Operation'; Expression = { Switch ($_.Operation) { 1 {'Installation'}; 2 {'Uninstallation'}; 3 {'Other'} } } }, + @{Name = 'Status'; Expression = { Switch ($_.ResultCode) { 0 {'Not Started'}; 1 {'In Progress'}; 2 {'Successful'}; 3 {'Incomplete'}; 4 {'Failed'}; 5 {'Aborted'} } } }, + 'Description' | + Sort-Object -Property 'Date' -Descending + ForEach ($Update in $UpdateHistory) { + If (($Update.Operation -ne 'Other') -and ($Update.Title -match "\($KBNumber\)")) { + $LatestUpdateHistory = $Update + Break + } + } + If (($LatestUpdateHistory.Operation -eq 'Installation') -and ($LatestUpdateHistory.Status -eq 'Successful')) { + Write-Log -Message "Discovered the following Microsoft Update: `n$($LatestUpdateHistory | Format-List | Out-String)" -Source ${CmdletName} + $kbFound = $true + } + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSession) + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($UpdateSearcher) + } + Else { + Write-Log -Message 'Unable to detect Windows update history via COM object.' -Source ${CmdletName} + } + } + + ## Return Result + If (-not $kbFound) { + Write-Log -Message "Microsoft Update [$kbNumber] is not installed." -Source ${CmdletName} + Write-Output -InputObject $false + } + Else { + Write-Log -Message "Microsoft Update [$kbNumber] is installed." -Source ${CmdletName} + Write-Output -InputObject $true + } + } + Catch { + Write-Log -Message "Failed discovering Microsoft Update [$kbNumber]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed discovering Microsoft Update [$kbNumber]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8772,77 +8964,77 @@ Function Test-MSUpdates { Function Install-MSUpdates { <# .SYNOPSIS - Install all Microsoft Updates in a given directory. + Install all Microsoft Updates in a given directory. .DESCRIPTION - Install all Microsoft Updates of type ".exe", ".msu", or ".msp" in a given directory (recursively search directory). + Install all Microsoft Updates of type ".exe", ".msu", or ".msp" in a given directory (recursively search directory). .PARAMETER Directory - Directory containing the updates. + Directory containing the updates. .EXAMPLE - Install-MSUpdates -Directory "$dirFiles\MSUpdates" + Install-MSUpdates -Directory "$dirFiles\MSUpdates" .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$Directory - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Write-Log -Message "Recursively install all Microsoft Updates in directory [$Directory]." -Source ${CmdletName} - - ## KB Number pattern match - $kbPattern = '(?i)kb\d{6,8}' - - ## Get all hotfixes and install if required - [IO.FileInfo[]]$files = Get-ChildItem -LiteralPath $Directory -Recurse -Include ('*.exe','*.msu','*.msp') - ForEach ($file in $files) { - If ($file.Name -match 'redist') { - [version]$redistVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName).ProductVersion - [string]$redistDescription = [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName).FileDescription - - Write-Log -Message "Install [$redistDescription $redistVersion]..." -Source ${CmdletName} - # Handle older redistributables (ie, VC++ 2005) - If ($redistDescription -match 'Win32 Cabinet Self-Extractor') { - Execute-Process -Path $file.FullName -Parameters '/q' -WindowStyle 'Hidden' -IgnoreExitCodes "*" - } - Else { - Execute-Process -Path $file.FullName -Parameters '/quiet /norestart' -WindowStyle 'Hidden' -IgnoreExitCodes "*" - } - } - Else { - # Get the KB number of the file - [string]$kbNumber = [regex]::Match($file.Name, $kbPattern).ToString() - If (-not $kbNumber) { Continue } - - # Check to see whether the KB is already installed - If (-not (Test-MSUpdates -KBNumber $kbNumber)) { - Write-Log -Message "KB Number [$KBNumber] was not detected and will be installed." -Source ${CmdletName} - Switch ($file.Extension) { - # Installation type for executables (i.e., Microsoft Office Updates) - '.exe' { Execute-Process -Path $file.FullName -Parameters '/quiet /norestart' -WindowStyle 'Hidden' -IgnoreExitCodes "*" } - # Installation type for Windows updates using Windows Update Standalone Installer - '.msu' { Execute-Process -Path 'wusa.exe' -Parameters "`"$($file.FullName)`" /quiet /norestart" -WindowStyle 'Hidden' -IgnoreExitCodes "*" } - # Installation type for Windows Installer Patch - '.msp' { Execute-MSI -Action 'Patch' -Path $file.FullName -IgnoreExitCodes "*" } - } - } - Else { - Write-Log -Message "KB Number [$kbNumber] is already installed. Continue..." -Source ${CmdletName} - } - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$Directory + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Write-Log -Message "Recursively install all Microsoft Updates in directory [$Directory]." -Source ${CmdletName} + + ## KB Number pattern match + $kbPattern = '(?i)kb\d{6,8}' + + ## Get all hotfixes and install if required + [IO.FileInfo[]]$files = Get-ChildItem -LiteralPath $Directory -Recurse -Include ('*.exe','*.msu','*.msp') + ForEach ($file in $files) { + If ($file.Name -match 'redist') { + [version]$redistVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName).ProductVersion + [string]$redistDescription = [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName).FileDescription + + Write-Log -Message "Install [$redistDescription $redistVersion]..." -Source ${CmdletName} + # Handle older redistributables (ie, VC++ 2005) + If ($redistDescription -match 'Win32 Cabinet Self-Extractor') { + Execute-Process -Path $file.FullName -Parameters '/q' -WindowStyle 'Hidden' -IgnoreExitCodes "*" + } + Else { + Execute-Process -Path $file.FullName -Parameters '/quiet /norestart' -WindowStyle 'Hidden' -IgnoreExitCodes "*" + } + } + Else { + # Get the KB number of the file + [string]$kbNumber = [regex]::Match($file.Name, $kbPattern).ToString() + If (-not $kbNumber) { Continue } + + # Check to see whether the KB is already installed + If (-not (Test-MSUpdates -KBNumber $kbNumber)) { + Write-Log -Message "KB Number [$KBNumber] was not detected and will be installed." -Source ${CmdletName} + Switch ($file.Extension) { + # Installation type for executables (i.e., Microsoft Office Updates) + '.exe' { Execute-Process -Path $file.FullName -Parameters '/quiet /norestart' -WindowStyle 'Hidden' -IgnoreExitCodes "*" } + # Installation type for Windows updates using Windows Update Standalone Installer + '.msu' { Execute-Process -Path $exeWusa -Parameters "`"$($file.FullName)`" /quiet /norestart" -WindowStyle 'Hidden' -IgnoreExitCodes "*" } + # Installation type for Windows Installer Patch + '.msp' { Execute-MSI -Action 'Patch' -Path $file.FullName -IgnoreExitCodes "*" } + } + } + Else { + Write-Log -Message "KB Number [$kbNumber] is already installed. Continue..." -Source ${CmdletName} + } + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8851,102 +9043,102 @@ Function Install-MSUpdates { Function Get-WindowTitle { <# .SYNOPSIS - Search for an open window title and return details about the window. + Search for an open window title and return details about the window. .DESCRIPTION - Search for a window title. If window title searched for returns more than one result, then details for each window will be displayed. - Returns the following properties for each window: WindowTitle, WindowHandle, ParentProcess, ParentProcessMainWindowHandle, ParentProcessId. - Function does not work in SYSTEM context unless launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. + Search for a window title. If window title searched for returns more than one result, then details for each window will be displayed. + Returns the following properties for each window: WindowTitle, WindowHandle, ParentProcess, ParentProcessMainWindowHandle, ParentProcessId. + Function does not work in SYSTEM context unless launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. .PARAMETER WindowTitle - The title of the application window to search for using regex matching. + The title of the application window to search for using regex matching. .PARAMETER GetAllWindowTitles - Get titles for all open windows on the system. + Get titles for all open windows on the system. .PARAMETER DisableFunctionLogging - Disables logging messages to the script log file. + Disables logging messages to the script log file. .EXAMPLE - Get-WindowTitle -WindowTitle 'Microsoft Word' - Gets details for each window that has the words "Microsoft Word" in the title. + Get-WindowTitle -WindowTitle 'Microsoft Word' + Gets details for each window that has the words "Microsoft Word" in the title. .EXAMPLE - Get-WindowTitle -GetAllWindowTitles - Gets details for all windows with a title. + Get-WindowTitle -GetAllWindowTitles + Gets details for all windows with a title. .EXAMPLE - Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.ParentProcess -eq 'WINWORD' } - Get details for all windows belonging to Microsoft Word process with name "WINWORD". + Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.ParentProcess -eq 'WINWORD' } + Get details for all windows belonging to Microsoft Word process with name "WINWORD". .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,ParameterSetName='SearchWinTitle')] - [AllowEmptyString()] - [string]$WindowTitle, - [Parameter(Mandatory=$true,ParameterSetName='GetAllWinTitles')] - [ValidateNotNullorEmpty()] - [switch]$GetAllWindowTitles = $false, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [switch]$DisableFunctionLogging = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - If ($PSCmdlet.ParameterSetName -eq 'SearchWinTitle') { - If (-not $DisableFunctionLogging) { Write-Log -Message "Find open window title(s) [$WindowTitle] using regex matching." -Source ${CmdletName} } - } - ElseIf ($PSCmdlet.ParameterSetName -eq 'GetAllWinTitles') { - If (-not $DisableFunctionLogging) { Write-Log -Message 'Find all open window title(s).' -Source ${CmdletName} } - } - - ## Get all window handles for visible windows - [IntPtr[]]$VisibleWindowHandles = [PSADT.UiAutomation]::EnumWindows() | Where-Object { [PSADT.UiAutomation]::IsWindowVisible($_) } - - ## Discover details about each visible window that was discovered - ForEach ($VisibleWindowHandle in $VisibleWindowHandles) { - If (-not $VisibleWindowHandle) { Continue } - ## Get the window title - [string]$VisibleWindowTitle = [PSADT.UiAutomation]::GetWindowText($VisibleWindowHandle) - If ($VisibleWindowTitle) { - ## Get the process that spawned the window - [Diagnostics.Process]$Process = Get-Process -ErrorAction 'Stop' | Where-Object { $_.Id -eq [PSADT.UiAutomation]::GetWindowThreadProcessId($VisibleWindowHandle) } - If ($Process) { - ## Build custom object with details about the window and the process - [psobject]$VisibleWindow = New-Object -TypeName 'PSObject' -Property @{ - WindowTitle = $VisibleWindowTitle - WindowHandle = $VisibleWindowHandle - ParentProcess= $Process.Name - ParentProcessMainWindowHandle = $Process.MainWindowHandle - ParentProcessId = $Process.Id - } - - ## Only save/return the window and process details which match the search criteria - If ($PSCmdlet.ParameterSetName -eq 'SearchWinTitle') { - $MatchResult = $VisibleWindow.WindowTitle -match $WindowTitle - If ($MatchResult) { - [psobject[]]$VisibleWindows += $VisibleWindow - } - } - ElseIf ($PSCmdlet.ParameterSetName -eq 'GetAllWinTitles') { - [psobject[]]$VisibleWindows += $VisibleWindow - } - } - } - } - } - Catch { - If (-not $DisableFunctionLogging) { Write-Log -Message "Failed to get requested window title(s). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} } - } - } - End { - Write-Output -InputObject $VisibleWindows - - If ($DisableFunctionLogging) { . $RevertScriptLogging } - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,ParameterSetName='SearchWinTitle')] + [AllowEmptyString()] + [string]$WindowTitle, + [Parameter(Mandatory=$true,ParameterSetName='GetAllWinTitles')] + [ValidateNotNullorEmpty()] + [switch]$GetAllWindowTitles = $false, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [switch]$DisableFunctionLogging = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + If ($PSCmdlet.ParameterSetName -eq 'SearchWinTitle') { + If (-not $DisableFunctionLogging) { Write-Log -Message "Find open window title(s) [$WindowTitle] using regex matching." -Source ${CmdletName} } + } + ElseIf ($PSCmdlet.ParameterSetName -eq 'GetAllWinTitles') { + If (-not $DisableFunctionLogging) { Write-Log -Message 'Find all open window title(s).' -Source ${CmdletName} } + } + + ## Get all window handles for visible windows + [IntPtr[]]$VisibleWindowHandles = [PSADT.UiAutomation]::EnumWindows() | Where-Object { [PSADT.UiAutomation]::IsWindowVisible($_) } + + ## Discover details about each visible window that was discovered + ForEach ($VisibleWindowHandle in $VisibleWindowHandles) { + If (-not $VisibleWindowHandle) { Continue } + ## Get the window title + [string]$VisibleWindowTitle = [PSADT.UiAutomation]::GetWindowText($VisibleWindowHandle) + If ($VisibleWindowTitle) { + ## Get the process that spawned the window + [Diagnostics.Process]$Process = Get-Process -ErrorAction 'Stop' | Where-Object { $_.Id -eq [PSADT.UiAutomation]::GetWindowThreadProcessId($VisibleWindowHandle) } + If ($Process) { + ## Build custom object with details about the window and the process + [psobject]$VisibleWindow = New-Object -TypeName 'PSObject' -Property @{ + WindowTitle = $VisibleWindowTitle + WindowHandle = $VisibleWindowHandle + ParentProcess= $Process.Name + ParentProcessMainWindowHandle = $Process.MainWindowHandle + ParentProcessId = $Process.Id + } + + ## Only save/return the window and process details which match the search criteria + If ($PSCmdlet.ParameterSetName -eq 'SearchWinTitle') { + $MatchResult = $VisibleWindow.WindowTitle -match $WindowTitle + If ($MatchResult) { + [psobject[]]$VisibleWindows += $VisibleWindow + } + } + ElseIf ($PSCmdlet.ParameterSetName -eq 'GetAllWinTitles') { + [psobject[]]$VisibleWindows += $VisibleWindow + } + } + } + } + } + Catch { + If (-not $DisableFunctionLogging) { Write-Log -Message "Failed to get requested window title(s). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} } + } + } + End { + Write-Output -InputObject $VisibleWindows + + If ($DisableFunctionLogging) { . $RevertScriptLogging } + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -8955,123 +9147,123 @@ Function Get-WindowTitle { Function Send-Keys { <# .SYNOPSIS - Send a sequence of keys to one or more application windows. + Send a sequence of keys to one or more application windows. .DESCRIPTION - Send a sequence of keys to one or more application window. If window title searched for returns more than one window, then all of them will receive the sent keys. - Function does not work in SYSTEM context unless launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. + Send a sequence of keys to one or more application window. If window title searched for returns more than one window, then all of them will receive the sent keys. + Function does not work in SYSTEM context unless launched with "psexec.exe -s -i" to run it as an interactive process under the SYSTEM account. .PARAMETER WindowTitle - The title of the application window to search for using regex matching. + The title of the application window to search for using regex matching. .PARAMETER GetAllWindowTitles - Get titles for all open windows on the system. + Get titles for all open windows on the system. .PARAMETER WindowHandle - Send keys to a specific window where the Window Handle is already known. + Send keys to a specific window where the Window Handle is already known. .PARAMETER Keys - The sequence of keys to send. Info on Key input at: http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx + The sequence of keys to send. Info on Key input at: http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx .PARAMETER WaitSeconds - An optional number of seconds to wait after the sending of the keys. + An optional number of seconds to wait after the sending of the keys. .EXAMPLE - Send-Keys -WindowTitle 'foobar - Notepad' -Key 'Hello world' - Send the sequence of keys "Hello world" to the application titled "foobar - Notepad". + Send-Keys -WindowTitle 'foobar - Notepad' -Key 'Hello world' + Send the sequence of keys "Hello world" to the application titled "foobar - Notepad". .EXAMPLE - Send-Keys -WindowTitle 'foobar - Notepad' -Key 'Hello world' -WaitSeconds 5 - Send the sequence of keys "Hello world" to the application titled "foobar - Notepad" and wait 5 seconds. + Send-Keys -WindowTitle 'foobar - Notepad' -Key 'Hello world' -WaitSeconds 5 + Send the sequence of keys "Hello world" to the application titled "foobar - Notepad" and wait 5 seconds. .EXAMPLE - Send-Keys -WindowHandle ([IntPtr]17368294) -Key 'Hello world' - Send the sequence of keys "Hello world" to the application with a Window Handle of '17368294'. + Send-Keys -WindowHandle ([IntPtr]17368294) -Key 'Hello world' + Send the sequence of keys "Hello world" to the application with a Window Handle of '17368294'. .NOTES .LINK - http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx - http://psappdeploytoolkit.com + http://msdn.microsoft.com/en-us/library/System.Windows.Forms.SendKeys(v=vs.100).aspx + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false,Position=0)] - [AllowEmptyString()] - [ValidateNotNull()] - [string]$WindowTitle, - [Parameter(Mandatory=$false,Position=1)] - [ValidateNotNullorEmpty()] - [switch]$GetAllWindowTitles = $false, - [Parameter(Mandatory=$false,Position=2)] - [ValidateNotNullorEmpty()] - [IntPtr]$WindowHandle, - [Parameter(Mandatory=$false,Position=3)] - [ValidateNotNullorEmpty()] - [string]$Keys, - [Parameter(Mandatory=$false,Position=4)] - [ValidateNotNullorEmpty()] - [int32]$WaitSeconds - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Load assembly containing class System.Windows.Forms.SendKeys - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - - [scriptblock]$SendKeys = { - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [IntPtr]$WindowHandle - ) - Try { - ## Bring the window to the foreground - [boolean]$IsBringWindowToFrontSuccess = [PSADT.UiAutomation]::BringWindowToFront($WindowHandle) - If (-not $IsBringWindowToFrontSuccess) { Throw 'Failed to bring window to foreground.'} - - ## Send the Key sequence - If ($Keys) { - [boolean]$IsWindowModal = If ([PSADT.UiAutomation]::IsWindowEnabled($WindowHandle)) { $false } Else { $true } - If ($IsWindowModal) { Throw 'Unable to send keys to window because it may be disabled due to a modal dialog being shown.' } - [Windows.Forms.SendKeys]::SendWait($Keys) - Write-Log -Message "Sent key(s) [$Keys] to window title [$($Window.WindowTitle)] with window handle [$WindowHandle]." -Source ${CmdletName} - - If ($WaitSeconds) { - Write-Log -Message "Sleeping for [$WaitSeconds] seconds." -Source ${CmdletName} - Start-Sleep -Seconds $WaitSeconds - } - } - } - Catch { - Write-Log -Message "Failed to send keys to window title [$($Window.WindowTitle)] with window handle [$WindowHandle]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - } - Process { - Try { - If ($WindowHandle) { - [psobject]$Window = Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.WindowHandle -eq $WindowHandle } - If (-not $Window) { - Write-Log -Message "No windows with Window Handle [$WindowHandle] were discovered." -Severity 2 -Source ${CmdletName} - Return - } - & $SendKeys -WindowHandle $Window.WindowHandle - } - Else { - [hashtable]$GetWindowTitleSplat = @{} - If ($GetAllWindowTitles) { $GetWindowTitleSplat.Add( 'GetAllWindowTitles', $GetAllWindowTitles) } - Else { $GetWindowTitleSplat.Add( 'WindowTitle', $WindowTitle) } - [psobject[]]$AllWindows = Get-WindowTitle @GetWindowTitleSplat - If (-not $AllWindows) { - Write-Log -Message 'No windows with the specified details were discovered.' -Severity 2 -Source ${CmdletName} - Return - } - - ForEach ($Window in $AllWindows) { - & $SendKeys -WindowHandle $Window.WindowHandle - } - } - } - Catch { - Write-Log -Message "Failed to send keys to specified window. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false,Position=0)] + [AllowEmptyString()] + [ValidateNotNull()] + [string]$WindowTitle, + [Parameter(Mandatory=$false,Position=1)] + [ValidateNotNullorEmpty()] + [switch]$GetAllWindowTitles = $false, + [Parameter(Mandatory=$false,Position=2)] + [ValidateNotNullorEmpty()] + [IntPtr]$WindowHandle, + [Parameter(Mandatory=$false,Position=3)] + [ValidateNotNullorEmpty()] + [string]$Keys, + [Parameter(Mandatory=$false,Position=4)] + [ValidateNotNullorEmpty()] + [int32]$WaitSeconds + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Load assembly containing class System.Windows.Forms.SendKeys + Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' + + [scriptblock]$SendKeys = { + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [IntPtr]$WindowHandle + ) + Try { + ## Bring the window to the foreground + [boolean]$IsBringWindowToFrontSuccess = [PSADT.UiAutomation]::BringWindowToFront($WindowHandle) + If (-not $IsBringWindowToFrontSuccess) { Throw 'Failed to bring window to foreground.'} + + ## Send the Key sequence + If ($Keys) { + [boolean]$IsWindowModal = If ([PSADT.UiAutomation]::IsWindowEnabled($WindowHandle)) { $false } Else { $true } + If ($IsWindowModal) { Throw 'Unable to send keys to window because it may be disabled due to a modal dialog being shown.' } + [Windows.Forms.SendKeys]::SendWait($Keys) + Write-Log -Message "Sent key(s) [$Keys] to window title [$($Window.WindowTitle)] with window handle [$WindowHandle]." -Source ${CmdletName} + + If ($WaitSeconds) { + Write-Log -Message "Sleeping for [$WaitSeconds] seconds." -Source ${CmdletName} + Start-Sleep -Seconds $WaitSeconds + } + } + } + Catch { + Write-Log -Message "Failed to send keys to window title [$($Window.WindowTitle)] with window handle [$WindowHandle]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + } + Process { + Try { + If ($WindowHandle) { + [psobject]$Window = Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.WindowHandle -eq $WindowHandle } + If (-not $Window) { + Write-Log -Message "No windows with Window Handle [$WindowHandle] were discovered." -Severity 2 -Source ${CmdletName} + Return + } + & $SendKeys -WindowHandle $Window.WindowHandle + } + Else { + [hashtable]$GetWindowTitleSplat = @{} + If ($GetAllWindowTitles) { $GetWindowTitleSplat.Add( 'GetAllWindowTitles', $GetAllWindowTitles) } + Else { $GetWindowTitleSplat.Add( 'WindowTitle', $WindowTitle) } + [psobject[]]$AllWindows = Get-WindowTitle @GetWindowTitleSplat + If (-not $AllWindows) { + Write-Log -Message 'No windows with the specified details were discovered.' -Severity 2 -Source ${CmdletName} + Return + } + + ForEach ($Window in $AllWindows) { + & $SendKeys -WindowHandle $Window.WindowHandle + } + } + } + Catch { + Write-Log -Message "Failed to send keys to specified window. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9080,124 +9272,124 @@ Function Send-Keys { Function Test-Battery { <# .SYNOPSIS - Tests whether the local machine is running on AC power or not. + Tests whether the local machine is running on AC power or not. .DESCRIPTION - Tests whether the local machine is running on AC power and returns true/false. For detailed information, use -PassThru option. + Tests whether the local machine is running on AC power and returns true/false. For detailed information, use -PassThru option. .PARAMETER PassThru - Outputs a hashtable containing the following properties: - IsLaptop, IsUsingACPower, ACPowerLineStatus, BatteryChargeStatus, BatteryLifePercent, BatteryLifeRemaining, BatteryFullLifetime + Outputs a hashtable containing the following properties: + IsLaptop, IsUsingACPower, ACPowerLineStatus, BatteryChargeStatus, BatteryLifePercent, BatteryLifeRemaining, BatteryFullLifetime .EXAMPLE - Test-Battery + Test-Battery .EXAMPLE - (Test-Battery -PassThru).IsLaptop - Determines if the current system is a laptop or not. + (Test-Battery -PassThru).IsLaptop + Determines if the current system is a laptop or not. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$PassThru = $false - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## PowerStatus class found in this assembly is more reliable than WMI in cases where the battery is failing. - Add-Type -Assembly 'System.Windows.Forms' -ErrorAction 'SilentlyContinue' - - ## Initialize a hashtable to store information about system type and power status - [hashtable]$SystemTypePowerStatus = @{ } - } - Process { - Write-Log -Message 'Check if system is using AC power or if it is running on battery...' -Source ${CmdletName} - - [Windows.Forms.PowerStatus]$PowerStatus = [Windows.Forms.SystemInformation]::PowerStatus - - ## Get the system power status. Indicates whether the system is using AC power or if the status is unknown. Possible values: - # Offline : The system is not using AC power. - # Online : The system is using AC power. - # Unknown : The power status of the system is unknown. - [string]$PowerLineStatus = $PowerStatus.PowerLineStatus - $SystemTypePowerStatus.Add('ACPowerLineStatus', $PowerStatus.PowerLineStatus) - - ## Get the current battery charge status. Possible values: High, Low, Critical, Charging, NoSystemBattery, Unknown. - [string]$BatteryChargeStatus = $PowerStatus.BatteryChargeStatus - $SystemTypePowerStatus.Add('BatteryChargeStatus', $PowerStatus.BatteryChargeStatus) - - ## Get the approximate amount, from 0.00 to 1.0, of full battery charge remaining. - # This property can report 1.0 when the battery is damaged and Windows can't detect a battery. - # Therefore, this property is only indicative of battery charge remaining if 'BatteryChargeStatus' property is not reporting 'NoSystemBattery' or 'Unknown'. - [single]$BatteryLifePercent = $PowerStatus.BatteryLifePercent - If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { - [single]$BatteryLifePercent = 0.0 - } - $SystemTypePowerStatus.Add('BatteryLifePercent', $PowerStatus.BatteryLifePercent) - - ## The reported approximate number of seconds of battery life remaining. It will report –1 if the remaining life is unknown because the system is on AC power. - [int32]$BatteryLifeRemaining = $PowerStatus.BatteryLifeRemaining - $SystemTypePowerStatus.Add('BatteryLifeRemaining', $PowerStatus.BatteryLifeRemaining) - - ## Get the manufacturer reported full charge lifetime of the primary battery power source in seconds. - # The reported number of seconds of battery life available when the battery is fully charged, or -1 if it is unknown. - # This will only be reported if the battery supports reporting this information. You will most likely get -1, indicating unknown. - [int32]$BatteryFullLifetime = $PowerStatus.BatteryFullLifetime - $SystemTypePowerStatus.Add('BatteryFullLifetime', $PowerStatus.BatteryFullLifetime) - - ## Determine if the system is using AC power - [boolean]$OnACPower = $false - If ($PowerLineStatus -eq 'Online') { - Write-Log -Message 'System is using AC power.' -Source ${CmdletName} - $OnACPower = $true - } - ElseIf ($PowerLineStatus -eq 'Offline') { - Write-Log -Message 'System is using battery power.' -Source ${CmdletName} - } - ElseIf ($PowerLineStatus -eq 'Unknown') { - If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { - Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. This is most likely due to a damaged battery so we will report system is using AC power." -Source ${CmdletName} - $OnACPower = $true - } - Else { - Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. Therefore, we will report system is using battery power." -Source ${CmdletName} - } - } - $SystemTypePowerStatus.Add('IsUsingACPower', $OnACPower) - - ## Determine if the system is a laptop - [boolean]$IsLaptop = $false - If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { - $IsLaptop = $false - } - Else { - $IsLaptop = $true - } - # Chassis Types - [int32[]]$ChassisTypes = Get-WmiObject -Class 'Win32_SystemEnclosure' | Where-Object { $_.ChassisTypes } | Select-Object -ExpandProperty 'ChassisTypes' - Write-Log -Message "The following system chassis types were detected [$($ChassisTypes -join ',')]." -Source ${CmdletName} - ForEach ($ChassisType in $ChassisTypes) { - Switch ($ChassisType) { - { $_ -eq 9 -or $_ -eq 10 -or $_ -eq 14 } { $IsLaptop = $true } # 9=Laptop, 10=Notebook, 14=Sub Notebook - { $_ -eq 3 } { $IsLaptop = $false } # 3=Desktop - } - } - # Add IsLaptop property to hashtable - $SystemTypePowerStatus.Add('IsLaptop', $IsLaptop) - - If ($PassThru) { - Write-Output -InputObject $SystemTypePowerStatus - } - Else { - Write-Output -InputObject $OnACPower - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$PassThru = $false + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## PowerStatus class found in this assembly is more reliable than WMI in cases where the battery is failing. + Add-Type -Assembly 'System.Windows.Forms' -ErrorAction 'SilentlyContinue' + + ## Initialize a hashtable to store information about system type and power status + [hashtable]$SystemTypePowerStatus = @{ } + } + Process { + Write-Log -Message 'Check if system is using AC power or if it is running on battery...' -Source ${CmdletName} + + [Windows.Forms.PowerStatus]$PowerStatus = [Windows.Forms.SystemInformation]::PowerStatus + + ## Get the system power status. Indicates whether the system is using AC power or if the status is unknown. Possible values: + # Offline : The system is not using AC power. + # Online : The system is using AC power. + # Unknown : The power status of the system is unknown. + [string]$PowerLineStatus = $PowerStatus.PowerLineStatus + $SystemTypePowerStatus.Add('ACPowerLineStatus', $PowerStatus.PowerLineStatus) + + ## Get the current battery charge status. Possible values: High, Low, Critical, Charging, NoSystemBattery, Unknown. + [string]$BatteryChargeStatus = $PowerStatus.BatteryChargeStatus + $SystemTypePowerStatus.Add('BatteryChargeStatus', $PowerStatus.BatteryChargeStatus) + + ## Get the approximate amount, from 0.00 to 1.0, of full battery charge remaining. + # This property can report 1.0 when the battery is damaged and Windows can't detect a battery. + # Therefore, this property is only indicative of battery charge remaining if 'BatteryChargeStatus' property is not reporting 'NoSystemBattery' or 'Unknown'. + [single]$BatteryLifePercent = $PowerStatus.BatteryLifePercent + If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { + [single]$BatteryLifePercent = 0.0 + } + $SystemTypePowerStatus.Add('BatteryLifePercent', $PowerStatus.BatteryLifePercent) + + ## The reported approximate number of seconds of battery life remaining. It will report -1 if the remaining life is unknown because the system is on AC power. + [int32]$BatteryLifeRemaining = $PowerStatus.BatteryLifeRemaining + $SystemTypePowerStatus.Add('BatteryLifeRemaining', $PowerStatus.BatteryLifeRemaining) + + ## Get the manufacturer reported full charge lifetime of the primary battery power source in seconds. + # The reported number of seconds of battery life available when the battery is fully charged, or -1 if it is unknown. + # This will only be reported if the battery supports reporting this information. You will most likely get -1, indicating unknown. + [int32]$BatteryFullLifetime = $PowerStatus.BatteryFullLifetime + $SystemTypePowerStatus.Add('BatteryFullLifetime', $PowerStatus.BatteryFullLifetime) + + ## Determine if the system is using AC power + [boolean]$OnACPower = $false + If ($PowerLineStatus -eq 'Online') { + Write-Log -Message 'System is using AC power.' -Source ${CmdletName} + $OnACPower = $true + } + ElseIf ($PowerLineStatus -eq 'Offline') { + Write-Log -Message 'System is using battery power.' -Source ${CmdletName} + } + ElseIf ($PowerLineStatus -eq 'Unknown') { + If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { + Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. This is most likely due to a damaged battery so we will report system is using AC power." -Source ${CmdletName} + $OnACPower = $true + } + Else { + Write-Log -Message "System power status is [$PowerLineStatus] and battery charge status is [$BatteryChargeStatus]. Therefore, we will report system is using battery power." -Source ${CmdletName} + } + } + $SystemTypePowerStatus.Add('IsUsingACPower', $OnACPower) + + ## Determine if the system is a laptop + [boolean]$IsLaptop = $false + If (($BatteryChargeStatus -eq 'NoSystemBattery') -or ($BatteryChargeStatus -eq 'Unknown')) { + $IsLaptop = $false + } + Else { + $IsLaptop = $true + } + # Chassis Types + [int32[]]$ChassisTypes = Get-WmiObject -Class 'Win32_SystemEnclosure' | Where-Object { $_.ChassisTypes } | Select-Object -ExpandProperty 'ChassisTypes' + Write-Log -Message "The following system chassis types were detected [$($ChassisTypes -join ',')]." -Source ${CmdletName} + ForEach ($ChassisType in $ChassisTypes) { + Switch ($ChassisType) { + { $_ -eq 9 -or $_ -eq 10 -or $_ -eq 14 } { $IsLaptop = $true } # 9=Laptop, 10=Notebook, 14=Sub Notebook + { $_ -eq 3 } { $IsLaptop = $false } # 3=Desktop + } + } + # Add IsLaptop property to hashtable + $SystemTypePowerStatus.Add('IsLaptop', $IsLaptop) + + If ($PassThru) { + Write-Output -InputObject $SystemTypePowerStatus + } + Else { + Write-Output -InputObject $OnACPower + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9206,42 +9398,42 @@ Function Test-Battery { Function Test-NetworkConnection { <# .SYNOPSIS - Tests for an active local network connection, excluding wireless and virtual network adapters. + Tests for an active local network connection, excluding wireless and virtual network adapters. .DESCRIPTION - Tests for an active local network connection, excluding wireless and virtual network adapters, by querying the Win32_NetworkAdapter WMI class. + Tests for an active local network connection, excluding wireless and virtual network adapters, by querying the Win32_NetworkAdapter WMI class. .EXAMPLE - Test-NetworkConnection + Test-NetworkConnection .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Write-Log -Message 'Check if system is using a wired network connection...' -Source ${CmdletName} - - [psobject[]]$networkConnected = Get-WmiObject -Class 'Win32_NetworkAdapter' | Where-Object { ($_.NetConnectionStatus -eq 2) -and ($_.NetConnectionID -match 'Local' -or $_.NetConnectionID -match 'Ethernet') -and ($_.NetConnectionID -notmatch 'Wireless') -and ($_.Name -notmatch 'Virtual') } -ErrorAction 'SilentlyContinue' - [boolean]$onNetwork = $false - If ($networkConnected) { - Write-Log -Message 'Wired network connection found.' -Source ${CmdletName} - [boolean]$onNetwork = $true - } - Else { - Write-Log -Message 'Wired network connection not found.' -Source ${CmdletName} - } - - Write-Output -InputObject $onNetwork - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Write-Log -Message 'Check if system is using a wired network connection...' -Source ${CmdletName} + + [psobject[]]$networkConnected = Get-WmiObject -Class 'Win32_NetworkAdapter' | Where-Object { ($_.NetConnectionStatus -eq 2) -and ($_.NetConnectionID -match 'Local' -or $_.NetConnectionID -match 'Ethernet') -and ($_.NetConnectionID -notmatch 'Wireless') -and ($_.Name -notmatch 'Virtual') } -ErrorAction 'SilentlyContinue' + [boolean]$onNetwork = $false + If ($networkConnected) { + Write-Log -Message 'Wired network connection found.' -Source ${CmdletName} + [boolean]$onNetwork = $true + } + Else { + Write-Log -Message 'Wired network connection not found.' -Source ${CmdletName} + } + + Write-Output -InputObject $onNetwork + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9250,101 +9442,101 @@ Function Test-NetworkConnection { Function Test-PowerPoint { <# .SYNOPSIS - Tests whether PowerPoint is running in either fullscreen slideshow mode or presentation mode. + Tests whether PowerPoint is running in either fullscreen slideshow mode or presentation mode. .DESCRIPTION - Tests whether someone is presenting using PowerPoint in either fullscreen slideshow mode or presentation mode. + Tests whether someone is presenting using PowerPoint in either fullscreen slideshow mode or presentation mode. .EXAMPLE - Test-PowerPoint + Test-PowerPoint .NOTES - This function can only execute detection logic if the process is in interactive mode. - There is a possiblity of a false positive if the PowerPoint filename starts with "PowerPoint Slide Show". + This function can only execute detection logic if the process is in interactive mode. + There is a possiblity of a false positive if the PowerPoint filename starts with "PowerPoint Slide Show". .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Check if PowerPoint is in either fullscreen slideshow mode or presentation mode...' -Source ${CmdletName} - Try { - [Diagnostics.Process[]]$PowerPointProcess = Get-Process -ErrorAction 'Stop' | Where-Object { $_.ProcessName -eq 'POWERPNT' } - If ($PowerPointProcess) { - [boolean]$IsPowerPointRunning = $true - Write-Log -Message 'PowerPoint application is running.' -Source ${CmdletName} - } - Else { - [boolean]$IsPowerPointRunning = $false - Write-Log -Message 'PowerPoint application is not running.' -Source ${CmdletName} - } - } - Catch { - Throw - } - - [nullable[boolean]]$IsPowerPointFullScreen = $false - If ($IsPowerPointRunning) { - ## Detect if PowerPoint is in fullscreen mode or Presentation Mode, detection method only works if process is interactive - If ([Environment]::UserInteractive) { - # Check if "POWERPNT" process has a window with a title that begins with "PowerPoint Slide Show" or "Powerpoint-" for non-English language systems. - # There is a possiblity of a false positive if the PowerPoint filename starts with "PowerPoint Slide Show" - [psobject]$PowerPointWindow = Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.WindowTitle -match '^PowerPoint Slide Show' -or $_.WindowTitle -match '^PowerPoint-' } | Where-Object { $_.ParentProcess -eq 'POWERPNT'} | Select-Object -First 1 - If ($PowerPointWindow) { - [nullable[boolean]]$IsPowerPointFullScreen = $true - Write-Log -Message 'Detected that PowerPoint process [POWERPNT] has a window with a title that beings with [PowerPoint Slide Show] or [PowerPoint-].' -Source ${CmdletName} - } - Else { - Write-Log -Message 'Detected that PowerPoint process [POWERPNT] does not have a window with a title that beings with [PowerPoint Slide Show] or [PowerPoint-].' -Source ${CmdletName} - Try { - [int32[]]$PowerPointProcessIDs = $PowerPointProcess | Select-Object -ExpandProperty 'Id' -ErrorAction 'Stop' - Write-Log -Message "PowerPoint process [POWERPNT] has process id(s) [$($PowerPointProcessIDs -join ', ')]." -Source ${CmdletName} - } - Catch { - Write-Log -Message "Unable to retrieve process id(s) for [POWERPNT] process. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - } - } - - ## If previous detection method did not detect PowerPoint in fullscreen mode, then check if PowerPoint is in Presentation Mode (check only works on Windows Vista or higher) - If ((-not $IsPowerPointFullScreen) -and (([version]$envOSVersion).Major -gt 5)) { - # Note: below method does not detect PowerPoint presentation mode if the presentation is on a monitor that does not have current mouse input control - [string]$UserNotificationState = [PSADT.UiAutomation]::GetUserNotificationState() - Write-Log -Message "Detected user notification state [$UserNotificationState]." -Source ${CmdletName} - Switch ($UserNotificationState) { - 'PresentationMode' { - Write-Log -Message "Detected that system is in [Presentation Mode]." -Source ${CmdletName} - [nullable[boolean]]$IsPowerPointFullScreen = $true - } - 'FullScreenOrPresentationModeOrLoginScreen' { - If (([string]$PowerPointProcessIDs) -and ($PowerPointProcessIDs -contains [PSADT.UIAutomation]::GetWindowThreadProcessID([PSADT.UIAutomation]::GetForeGroundWindow()))) { - Write-Log -Message "Detected that fullscreen foreground window matches PowerPoint process id." -Source ${CmdletName} - [nullable[boolean]]$IsPowerPointFullScreen = $true - } - } - } - } - } - Else { - [nullable[boolean]]$IsPowerPointFullScreen = $null - Write-Log -Message 'Unable to run check to see if PowerPoint is in fullscreen mode or Presentation Mode because current process is not interactive. Configure script to run in interactive mode in your deployment tool. If using SCCM Application Model, then make sure "Allow users to view and interact with the program installation" is selected. If using SCCM Package Model, then make sure "Allow users to interact with this program" is selected.' -Severity 2 -Source ${CmdletName} - } - } - } - Catch { - [nullable[boolean]]$IsPowerPointFullScreen = $null - Write-Log -Message "Failed check to see if PowerPoint is running in fullscreen slideshow mode. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-Log -Message "PowerPoint is running in fullscreen mode [$IsPowerPointFullScreen]." -Source ${CmdletName} - Write-Output -InputObject $IsPowerPointFullScreen - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Check if PowerPoint is in either fullscreen slideshow mode or presentation mode...' -Source ${CmdletName} + Try { + [Diagnostics.Process[]]$PowerPointProcess = Get-Process -ErrorAction 'Stop' | Where-Object { $_.ProcessName -eq 'POWERPNT' } + If ($PowerPointProcess) { + [boolean]$IsPowerPointRunning = $true + Write-Log -Message 'PowerPoint application is running.' -Source ${CmdletName} + } + Else { + [boolean]$IsPowerPointRunning = $false + Write-Log -Message 'PowerPoint application is not running.' -Source ${CmdletName} + } + } + Catch { + Throw + } + + [nullable[boolean]]$IsPowerPointFullScreen = $false + If ($IsPowerPointRunning) { + ## Detect if PowerPoint is in fullscreen mode or Presentation Mode, detection method only works if process is interactive + If ([Environment]::UserInteractive) { + # Check if "POWERPNT" process has a window with a title that begins with "PowerPoint Slide Show" or "Powerpoint-" for non-English language systems. + # There is a possiblity of a false positive if the PowerPoint filename starts with "PowerPoint Slide Show" + [psobject]$PowerPointWindow = Get-WindowTitle -GetAllWindowTitles | Where-Object { $_.WindowTitle -match '^PowerPoint Slide Show' -or $_.WindowTitle -match '^PowerPoint-' } | Where-Object { $_.ParentProcess -eq 'POWERPNT'} | Select-Object -First 1 + If ($PowerPointWindow) { + [nullable[boolean]]$IsPowerPointFullScreen = $true + Write-Log -Message 'Detected that PowerPoint process [POWERPNT] has a window with a title that beings with [PowerPoint Slide Show] or [PowerPoint-].' -Source ${CmdletName} + } + Else { + Write-Log -Message 'Detected that PowerPoint process [POWERPNT] does not have a window with a title that beings with [PowerPoint Slide Show] or [PowerPoint-].' -Source ${CmdletName} + Try { + [int32[]]$PowerPointProcessIDs = $PowerPointProcess | Select-Object -ExpandProperty 'Id' -ErrorAction 'Stop' + Write-Log -Message "PowerPoint process [POWERPNT] has process id(s) [$($PowerPointProcessIDs -join ', ')]." -Source ${CmdletName} + } + Catch { + Write-Log -Message "Unable to retrieve process id(s) for [POWERPNT] process. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + } + } + + ## If previous detection method did not detect PowerPoint in fullscreen mode, then check if PowerPoint is in Presentation Mode (check only works on Windows Vista or higher) + If ((-not $IsPowerPointFullScreen) -and (([version]$envOSVersion).Major -gt 5)) { + # Note: below method does not detect PowerPoint presentation mode if the presentation is on a monitor that does not have current mouse input control + [string]$UserNotificationState = [PSADT.UiAutomation]::GetUserNotificationState() + Write-Log -Message "Detected user notification state [$UserNotificationState]." -Source ${CmdletName} + Switch ($UserNotificationState) { + 'PresentationMode' { + Write-Log -Message "Detected that system is in [Presentation Mode]." -Source ${CmdletName} + [nullable[boolean]]$IsPowerPointFullScreen = $true + } + 'FullScreenOrPresentationModeOrLoginScreen' { + If (([string]$PowerPointProcessIDs) -and ($PowerPointProcessIDs -contains [PSADT.UIAutomation]::GetWindowThreadProcessID([PSADT.UIAutomation]::GetForeGroundWindow()))) { + Write-Log -Message "Detected that fullscreen foreground window matches PowerPoint process id." -Source ${CmdletName} + [nullable[boolean]]$IsPowerPointFullScreen = $true + } + } + } + } + } + Else { + [nullable[boolean]]$IsPowerPointFullScreen = $null + Write-Log -Message 'Unable to run check to see if PowerPoint is in fullscreen mode or Presentation Mode because current process is not interactive. Configure script to run in interactive mode in your deployment tool. If using SCCM Application Model, then make sure "Allow users to view and interact with the program installation" is selected. If using SCCM Package Model, then make sure "Allow users to interact with this program" is selected.' -Severity 2 -Source ${CmdletName} + } + } + } + Catch { + [nullable[boolean]]$IsPowerPointFullScreen = $null + Write-Log -Message "Failed check to see if PowerPoint is running in fullscreen slideshow mode. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-Log -Message "PowerPoint is running in fullscreen mode [$IsPowerPointFullScreen]." -Source ${CmdletName} + Write-Output -InputObject $IsPowerPointFullScreen + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9353,121 +9545,121 @@ Function Test-PowerPoint { Function Invoke-SCCMTask { <# .SYNOPSIS - Triggers SCCM to invoke the requested schedule task id. + Triggers SCCM to invoke the requested schedule task id. .DESCRIPTION - Triggers SCCM to invoke the requested schedule task id. + Triggers SCCM to invoke the requested schedule task id. .PARAMETER ScheduleId - Name of the schedule id to trigger. - Options: HardwareInventory, SoftwareInventory, HeartbeatDiscovery, SoftwareInventoryFileCollection, RequestMachinePolicy, EvaluateMachinePolicy, - LocationServicesCleanup, SoftwareMeteringReport, SourceUpdate, PolicyAgentCleanup, RequestMachinePolicy2, CertificateMaintenance, PeerDistributionPointStatus, - PeerDistributionPointProvisioning, ComplianceIntervalEnforcement, SoftwareUpdatesAgentAssignmentEvaluation, UploadStateMessage, StateMessageManager, - SoftwareUpdatesScan, AMTProvisionCycle, UpdateStorePolicy, StateSystemBulkSend, ApplicationManagerPolicyAction, PowerManagementStartSummarizer + Name of the schedule id to trigger. + Options: HardwareInventory, SoftwareInventory, HeartbeatDiscovery, SoftwareInventoryFileCollection, RequestMachinePolicy, EvaluateMachinePolicy, + LocationServicesCleanup, SoftwareMeteringReport, SourceUpdate, PolicyAgentCleanup, RequestMachinePolicy2, CertificateMaintenance, PeerDistributionPointStatus, + PeerDistributionPointProvisioning, ComplianceIntervalEnforcement, SoftwareUpdatesAgentAssignmentEvaluation, UploadStateMessage, StateMessageManager, + SoftwareUpdatesScan, AMTProvisionCycle, UpdateStorePolicy, StateSystemBulkSend, ApplicationManagerPolicyAction, PowerManagementStartSummarizer .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Invoke-SCCMTask 'SoftwareUpdatesScan' + Invoke-SCCMTask 'SoftwareUpdatesScan' .EXAMPLE - Invoke-SCCMTask + Invoke-SCCMTask .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateSet('HardwareInventory','SoftwareInventory','HeartbeatDiscovery','SoftwareInventoryFileCollection','RequestMachinePolicy','EvaluateMachinePolicy','LocationServicesCleanup','SoftwareMeteringReport','SourceUpdate','PolicyAgentCleanup','RequestMachinePolicy2','CertificateMaintenance','PeerDistributionPointStatus','PeerDistributionPointProvisioning','ComplianceIntervalEnforcement','SoftwareUpdatesAgentAssignmentEvaluation','UploadStateMessage','StateMessageManager','SoftwareUpdatesScan','AMTProvisionCycle','UpdateStorePolicy','StateSystemBulkSend','ApplicationManagerPolicyAction','PowerManagementStartSummarizer')] - [string]$ScheduleID, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Invoke SCCM Schedule Task ID [$ScheduleId]..." -Source ${CmdletName} - - ## Make sure SCCM client is installed and running - Write-Log -Message 'Check to see if SCCM Client service [ccmexec] is installed and running.' -Source ${CmdletName} - If (Test-ServiceExists -Name 'ccmexec') { - If ($(Get-Service -Name 'ccmexec' -ErrorAction 'SilentlyContinue').Status -ne 'Running') { - Throw "SCCM Client Service [ccmexec] exists but it is not in a 'Running' state." - } - } Else { - Throw 'SCCM Client Service [ccmexec] does not exist. The SCCM Client may not be installed.' - } - - ## Determine the SCCM Client Version - Try { - [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' - Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} - } - Catch { - Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - Throw 'Failed to determine the SCCM client version number.' - } - - ## Create a hashtable of Schedule IDs compatible with SCCM Client 2007 - [hashtable]$ScheduleIds = @{ - HardwareInventory = '{00000000-0000-0000-0000-000000000001}'; # Hardware Inventory Collection Task - SoftwareInventory = '{00000000-0000-0000-0000-000000000002}'; # Software Inventory Collection Task - HeartbeatDiscovery = '{00000000-0000-0000-0000-000000000003}'; # Heartbeat Discovery Cycle - SoftwareInventoryFileCollection = '{00000000-0000-0000-0000-000000000010}'; # Software Inventory File Collection Task - RequestMachinePolicy = '{00000000-0000-0000-0000-000000000021}'; # Request Machine Policy Assignments - EvaluateMachinePolicy = '{00000000-0000-0000-0000-000000000022}'; # Evaluate Machine Policy Assignments - RefreshDefaultMp = '{00000000-0000-0000-0000-000000000023}'; # Refresh Default MP Task - RefreshLocationServices = '{00000000-0000-0000-0000-000000000024}'; # Refresh Location Services Task - LocationServicesCleanup = '{00000000-0000-0000-0000-000000000025}'; # Location Services Cleanup Task - SoftwareMeteringReport = '{00000000-0000-0000-0000-000000000031}'; # Software Metering Report Cycle - SourceUpdate = '{00000000-0000-0000-0000-000000000032}'; # Source Update Manage Update Cycle - PolicyAgentCleanup = '{00000000-0000-0000-0000-000000000040}'; # Policy Agent Cleanup Cycle - RequestMachinePolicy2 = '{00000000-0000-0000-0000-000000000042}'; # Request Machine Policy Assignments - CertificateMaintenance = '{00000000-0000-0000-0000-000000000051}'; # Certificate Maintenance Cycle - PeerDistributionPointStatus = '{00000000-0000-0000-0000-000000000061}'; # Peer Distribution Point Status Task - PeerDistributionPointProvisioning = '{00000000-0000-0000-0000-000000000062}'; # Peer Distribution Point Provisioning Status Task - ComplianceIntervalEnforcement = '{00000000-0000-0000-0000-000000000071}'; # Compliance Interval Enforcement - SoftwareUpdatesAgentAssignmentEvaluation = '{00000000-0000-0000-0000-000000000108}'; # Software Updates Agent Assignment Evaluation Cycle - UploadStateMessage = '{00000000-0000-0000-0000-000000000111}'; # Send Unsent State Messages - StateMessageManager = '{00000000-0000-0000-0000-000000000112}'; # State Message Manager Task - SoftwareUpdatesScan = '{00000000-0000-0000-0000-000000000113}'; # Force Update Scan - AMTProvisionCycle = '{00000000-0000-0000-0000-000000000120}'; # AMT Provision Cycle - } - - ## If SCCM 2012 Client or higher, modify hashtabe containing Schedule IDs so that it only has the ones compatible with this version of the SCCM client - If ($SCCMClientVersion.Major -ge 5) { - $ScheduleIds.Remove('PeerDistributionPointStatus') - $ScheduleIds.Remove('PeerDistributionPointProvisioning') - $ScheduleIds.Remove('ComplianceIntervalEnforcement') - $ScheduleIds.Add('UpdateStorePolicy','{00000000-0000-0000-0000-000000000114}') # Update Store Policy - $ScheduleIds.Add('StateSystemBulkSend','{00000000-0000-0000-0000-000000000116}') # State System Policy Bulk Send Low - $ScheduleIds.Add('ApplicationManagerPolicyAction','{00000000-0000-0000-0000-000000000121}') # Application Manager Policy Action - $ScheduleIds.Add('PowerManagementStartSummarizer','{00000000-0000-0000-0000-000000000131}') # Power Management Start Summarizer - } - - ## Determine if the requested Schedule ID is available on this version of the SCCM Client - If (-not ($ScheduleIds.ContainsKey($ScheduleId))) { - Throw "The requested ScheduleId [$ScheduleId] is not available with this version of the SCCM Client [$SCCMClientVersion]." - } - - ## Trigger SCCM task - Write-Log -Message "Trigger SCCM Task ID [$ScheduleId]." -Source ${CmdletName} - [Management.ManagementClass]$SmsClient = [WMIClass]'ROOT\CCM:SMS_Client' - $null = $SmsClient.TriggerSchedule($ScheduleIds.$ScheduleID) - } - Catch { - Write-Log -Message "Failed to trigger SCCM Schedule Task ID [$($ScheduleIds.$ScheduleId)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to trigger SCCM Schedule Task ID [$($ScheduleIds.$ScheduleId)]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateSet('HardwareInventory','SoftwareInventory','HeartbeatDiscovery','SoftwareInventoryFileCollection','RequestMachinePolicy','EvaluateMachinePolicy','LocationServicesCleanup','SoftwareMeteringReport','SourceUpdate','PolicyAgentCleanup','RequestMachinePolicy2','CertificateMaintenance','PeerDistributionPointStatus','PeerDistributionPointProvisioning','ComplianceIntervalEnforcement','SoftwareUpdatesAgentAssignmentEvaluation','UploadStateMessage','StateMessageManager','SoftwareUpdatesScan','AMTProvisionCycle','UpdateStorePolicy','StateSystemBulkSend','ApplicationManagerPolicyAction','PowerManagementStartSummarizer')] + [string]$ScheduleID, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Invoke SCCM Schedule Task ID [$ScheduleId]..." -Source ${CmdletName} + + ## Make sure SCCM client is installed and running + Write-Log -Message 'Check to see if SCCM Client service [ccmexec] is installed and running.' -Source ${CmdletName} + If (Test-ServiceExists -Name 'ccmexec') { + If ($(Get-Service -Name 'ccmexec' -ErrorAction 'SilentlyContinue').Status -ne 'Running') { + Throw "SCCM Client Service [ccmexec] exists but it is not in a 'Running' state." + } + } Else { + Throw 'SCCM Client Service [ccmexec] does not exist. The SCCM Client may not be installed.' + } + + ## Determine the SCCM Client Version + Try { + [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' + Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + } + Catch { + Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Throw 'Failed to determine the SCCM client version number.' + } + + ## Create a hashtable of Schedule IDs compatible with SCCM Client 2007 + [hashtable]$ScheduleIds = @{ + HardwareInventory = '{00000000-0000-0000-0000-000000000001}'; # Hardware Inventory Collection Task + SoftwareInventory = '{00000000-0000-0000-0000-000000000002}'; # Software Inventory Collection Task + HeartbeatDiscovery = '{00000000-0000-0000-0000-000000000003}'; # Heartbeat Discovery Cycle + SoftwareInventoryFileCollection = '{00000000-0000-0000-0000-000000000010}'; # Software Inventory File Collection Task + RequestMachinePolicy = '{00000000-0000-0000-0000-000000000021}'; # Request Machine Policy Assignments + EvaluateMachinePolicy = '{00000000-0000-0000-0000-000000000022}'; # Evaluate Machine Policy Assignments + RefreshDefaultMp = '{00000000-0000-0000-0000-000000000023}'; # Refresh Default MP Task + RefreshLocationServices = '{00000000-0000-0000-0000-000000000024}'; # Refresh Location Services Task + LocationServicesCleanup = '{00000000-0000-0000-0000-000000000025}'; # Location Services Cleanup Task + SoftwareMeteringReport = '{00000000-0000-0000-0000-000000000031}'; # Software Metering Report Cycle + SourceUpdate = '{00000000-0000-0000-0000-000000000032}'; # Source Update Manage Update Cycle + PolicyAgentCleanup = '{00000000-0000-0000-0000-000000000040}'; # Policy Agent Cleanup Cycle + RequestMachinePolicy2 = '{00000000-0000-0000-0000-000000000042}'; # Request Machine Policy Assignments + CertificateMaintenance = '{00000000-0000-0000-0000-000000000051}'; # Certificate Maintenance Cycle + PeerDistributionPointStatus = '{00000000-0000-0000-0000-000000000061}'; # Peer Distribution Point Status Task + PeerDistributionPointProvisioning = '{00000000-0000-0000-0000-000000000062}'; # Peer Distribution Point Provisioning Status Task + ComplianceIntervalEnforcement = '{00000000-0000-0000-0000-000000000071}'; # Compliance Interval Enforcement + SoftwareUpdatesAgentAssignmentEvaluation = '{00000000-0000-0000-0000-000000000108}'; # Software Updates Agent Assignment Evaluation Cycle + UploadStateMessage = '{00000000-0000-0000-0000-000000000111}'; # Send Unsent State Messages + StateMessageManager = '{00000000-0000-0000-0000-000000000112}'; # State Message Manager Task + SoftwareUpdatesScan = '{00000000-0000-0000-0000-000000000113}'; # Force Update Scan + AMTProvisionCycle = '{00000000-0000-0000-0000-000000000120}'; # AMT Provision Cycle + } + + ## If SCCM 2012 Client or higher, modify hashtabe containing Schedule IDs so that it only has the ones compatible with this version of the SCCM client + If ($SCCMClientVersion.Major -ge 5) { + $ScheduleIds.Remove('PeerDistributionPointStatus') + $ScheduleIds.Remove('PeerDistributionPointProvisioning') + $ScheduleIds.Remove('ComplianceIntervalEnforcement') + $ScheduleIds.Add('UpdateStorePolicy','{00000000-0000-0000-0000-000000000114}') # Update Store Policy + $ScheduleIds.Add('StateSystemBulkSend','{00000000-0000-0000-0000-000000000116}') # State System Policy Bulk Send Low + $ScheduleIds.Add('ApplicationManagerPolicyAction','{00000000-0000-0000-0000-000000000121}') # Application Manager Policy Action + $ScheduleIds.Add('PowerManagementStartSummarizer','{00000000-0000-0000-0000-000000000131}') # Power Management Start Summarizer + } + + ## Determine if the requested Schedule ID is available on this version of the SCCM Client + If (-not ($ScheduleIds.ContainsKey($ScheduleId))) { + Throw "The requested ScheduleId [$ScheduleId] is not available with this version of the SCCM Client [$SCCMClientVersion]." + } + + ## Trigger SCCM task + Write-Log -Message "Trigger SCCM Task ID [$ScheduleId]." -Source ${CmdletName} + [Management.ManagementClass]$SmsClient = [WMIClass]'ROOT\CCM:SMS_Client' + $null = $SmsClient.TriggerSchedule($ScheduleIds.$ScheduleID) + } + Catch { + Write-Log -Message "Failed to trigger SCCM Schedule Task ID [$($ScheduleIds.$ScheduleId)]. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to trigger SCCM Schedule Task ID [$($ScheduleIds.$ScheduleId)]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9476,112 +9668,112 @@ Function Invoke-SCCMTask { Function Install-SCCMSoftwareUpdates { <# .SYNOPSIS - Scans for outstanding SCCM updates to be installed and installs the pending updates. + Scans for outstanding SCCM updates to be installed and installs the pending updates. .DESCRIPTION - Scans for outstanding SCCM updates to be installed and installs the pending updates. - Only compatible with SCCM 2012 Client or higher. This function can take several minutes to run. + Scans for outstanding SCCM updates to be installed and installs the pending updates. + Only compatible with SCCM 2012 Client or higher. This function can take several minutes to run. .PARAMETER SoftwareUpdatesScanWaitInSeconds - The amount of time to wait in seconds for the software updates scan to complete. Default is: 180 seconds. + The amount of time to wait in seconds for the software updates scan to complete. Default is: 180 seconds. .PARAMETER WaitForPendingUpdatesTimeout - The amount of time to wait for missing and pending updates to install before exiting the function. Default is: 45 minutes. + The amount of time to wait for missing and pending updates to install before exiting the function. Default is: 45 minutes. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Install-SCCMSoftwareUpdates + Install-SCCMSoftwareUpdates .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [int32]$SoftwareUpdatesScanWaitInSeconds = 180, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [timespan]$WaitForPendingUpdatesTimeout = $(New-TimeSpan -Minutes 45), - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Scan for and install pending SCCM software updates.' -Source ${CmdletName} - - ## Make sure SCCM client is installed and running - Write-Log -Message 'Check to see if SCCM Client service [ccmexec] is installed and running.' -Source ${CmdletName} - If (Test-ServiceExists -Name 'ccmexec') { - If ($(Get-Service -Name 'ccmexec' -ErrorAction 'SilentlyContinue').Status -ne 'Running') { - Throw "SCCM Client Service [ccmexec] exists but it is not in a 'Running' state." - } - } Else { - Throw 'SCCM Client Service [ccmexec] does not exist. The SCCM Client may not be installed.' - } - - ## Determine the SCCM Client Version - Try { - [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' - Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} - } - Catch { - Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - Throw 'Failed to determine the SCCM client version number.' - } - # If SCCM 2007 Client or lower, exit function - If ($SCCMClientVersion.Major -le 4) { - Throw 'SCCM 2007 or lower, which is incompatible with this function, was detected on this system.' - } - - $StartTime = Get-Date - ## Trigger SCCM client scan for Software Updates - Write-Log -Message 'Trigger SCCM client scan for Software Updates...' -Source ${CmdletName} - Invoke-SCCMTask -ScheduleId 'SoftwareUpdatesScan' - - Write-Log -Message "The SCCM client scan for Software Updates has been triggered. The script is suspended for [$SoftwareUpdatesScanWaitInSeconds] seconds to let the update scan finish." -Source ${CmdletName} - Start-Sleep -Seconds $SoftwareUpdatesScanWaitInSeconds - - ## Find the number of missing updates - Try { - [Management.ManagementObject[]]$CMMissingUpdates = @(Get-WmiObject -Namespace 'ROOT\CCM\ClientSDK' -Query "SELECT * FROM CCM_SoftwareUpdate WHERE ComplianceState = '0'" -ErrorAction 'Stop') - } - Catch { - Write-Log -Message "Failed to find the number of missing software updates. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} - Throw 'Failed to find the number of missing software updates.' - } - - ## Install missing updates and wait for pending updates to finish installing - If ($CMMissingUpdates.Count) { - # Install missing updates - Write-Log -Message "Install missing updates. The number of missing updates is [$($CMMissingUpdates.Count)]." -Source ${CmdletName} - $CMInstallMissingUpdates = (Get-WmiObject -Namespace 'ROOT\CCM\ClientSDK' -Class 'CCM_SoftwareUpdatesManager' -List).InstallUpdates($CMMissingUpdates) - - # Wait for pending updates to finish installing or the timeout value to expire - Do { - Start-Sleep -Seconds 60 - [array]$CMInstallPendingUpdates = @(Get-WmiObject -Namespace "ROOT\CCM\ClientSDK" -Query "SELECT * FROM CCM_SoftwareUpdate WHERE EvaluationState = 6 or EvaluationState = 7") - Write-Log -Message "The number of updates pending installation is [$($CMInstallPendingUpdates.Count)]." -Source ${CmdletName} - } While (($CMInstallPendingUpdates.Count -ne 0) -and ((New-TimeSpan -Start $StartTime -End $(Get-Date)) -lt $WaitForPendingUpdatesTimeout)) - } - Else { - Write-Log -Message 'There are no missing updates.' -Source ${CmdletName} - } - } - Catch { - Write-Log -Message "Failed to trigger installation of missing software updates. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to trigger installation of missing software updates: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [int32]$SoftwareUpdatesScanWaitInSeconds = 180, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [timespan]$WaitForPendingUpdatesTimeout = $(New-TimeSpan -Minutes 45), + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Scan for and install pending SCCM software updates.' -Source ${CmdletName} + + ## Make sure SCCM client is installed and running + Write-Log -Message 'Check to see if SCCM Client service [ccmexec] is installed and running.' -Source ${CmdletName} + If (Test-ServiceExists -Name 'ccmexec') { + If ($(Get-Service -Name 'ccmexec' -ErrorAction 'SilentlyContinue').Status -ne 'Running') { + Throw "SCCM Client Service [ccmexec] exists but it is not in a 'Running' state." + } + } Else { + Throw 'SCCM Client Service [ccmexec] does not exist. The SCCM Client may not be installed.' + } + + ## Determine the SCCM Client Version + Try { + [version]$SCCMClientVersion = Get-WmiObject -Namespace 'ROOT\CCM' -Class 'CCM_InstalledComponent' -ErrorAction 'Stop' | Where-Object { $_.Name -eq 'SmsClient' } | Select-Object -ExpandProperty 'Version' -ErrorAction 'Stop' + Write-Log -Message "Installed SCCM Client Version Number [$SCCMClientVersion]." -Source ${CmdletName} + } + Catch { + Write-Log -Message "Failed to determine the SCCM client version number. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Throw 'Failed to determine the SCCM client version number.' + } + # If SCCM 2007 Client or lower, exit function + If ($SCCMClientVersion.Major -le 4) { + Throw 'SCCM 2007 or lower, which is incompatible with this function, was detected on this system.' + } + + $StartTime = Get-Date + ## Trigger SCCM client scan for Software Updates + Write-Log -Message 'Trigger SCCM client scan for Software Updates...' -Source ${CmdletName} + Invoke-SCCMTask -ScheduleId 'SoftwareUpdatesScan' + + Write-Log -Message "The SCCM client scan for Software Updates has been triggered. The script is suspended for [$SoftwareUpdatesScanWaitInSeconds] seconds to let the update scan finish." -Source ${CmdletName} + Start-Sleep -Seconds $SoftwareUpdatesScanWaitInSeconds + + ## Find the number of missing updates + Try { + [Management.ManagementObject[]]$CMMissingUpdates = @(Get-WmiObject -Namespace 'ROOT\CCM\ClientSDK' -Query "SELECT * FROM CCM_SoftwareUpdate WHERE ComplianceState = '0'" -ErrorAction 'Stop') + } + Catch { + Write-Log -Message "Failed to find the number of missing software updates. `n$(Resolve-Error)" -Severity 2 -Source ${CmdletName} + Throw 'Failed to find the number of missing software updates.' + } + + ## Install missing updates and wait for pending updates to finish installing + If ($CMMissingUpdates.Count) { + # Install missing updates + Write-Log -Message "Install missing updates. The number of missing updates is [$($CMMissingUpdates.Count)]." -Source ${CmdletName} + $CMInstallMissingUpdates = (Get-WmiObject -Namespace 'ROOT\CCM\ClientSDK' -Class 'CCM_SoftwareUpdatesManager' -List).InstallUpdates($CMMissingUpdates) + + # Wait for pending updates to finish installing or the timeout value to expire + Do { + Start-Sleep -Seconds 60 + [array]$CMInstallPendingUpdates = @(Get-WmiObject -Namespace "ROOT\CCM\ClientSDK" -Query "SELECT * FROM CCM_SoftwareUpdate WHERE EvaluationState = 6 or EvaluationState = 7") + Write-Log -Message "The number of updates pending installation is [$($CMInstallPendingUpdates.Count)]." -Source ${CmdletName} + } While (($CMInstallPendingUpdates.Count -ne 0) -and ((New-TimeSpan -Start $StartTime -End $(Get-Date)) -lt $WaitForPendingUpdatesTimeout)) + } + Else { + Write-Log -Message 'There are no missing updates.' -Source ${CmdletName} + } + } + Catch { + Write-Log -Message "Failed to trigger installation of missing software updates. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to trigger installation of missing software updates: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9590,66 +9782,66 @@ Function Install-SCCMSoftwareUpdates { Function Update-GroupPolicy { <# .SYNOPSIS - Performs a gpupdate command to refresh Group Policies on the local machine. + Performs a gpupdate command to refresh Group Policies on the local machine. .DESCRIPTION - Performs a gpupdate command to refresh Group Policies on the local machine. + Performs a gpupdate command to refresh Group Policies on the local machine. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Update-GroupPolicy + Update-GroupPolicy .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - [string[]]$GPUpdateCmds = '/C echo N | gpupdate.exe /Target:Computer /Force', '/C echo N | gpupdate.exe /Target:User /Force' - [int32]$InstallCount = 0 - ForEach ($GPUpdateCmd in $GPUpdateCmds) { - Try { - If ($InstallCount -eq 0) { - [string]$InstallMsg = 'Update Group Policies for the Machine' - Write-Log -Message "$($InstallMsg)..." -Source ${CmdletName} - } - Else { - [string]$InstallMsg = 'Update Group Policies for the User' - Write-Log -Message "$($InstallMsg)..." -Source ${CmdletName} - } - [psobject]$ExecuteResult = Execute-Process -Path "$envWindir\system32\cmd.exe" -Parameters $GPUpdateCmd -WindowStyle 'Hidden' -PassThru - - If ($ExecuteResult.ExitCode -ne 0) { - If ($ExecuteResult.ExitCode -eq 60002) { - Throw "Execute-Process function failed with exit code [$($ExecuteResult.ExitCode)]." - } - Else { - Throw "gpupdate.exe failed with exit code [$($ExecuteResult.ExitCode)]." - } - } - $InstallCount++ - } - Catch { - Write-Log -Message "Failed to $($InstallMsg). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to $($InstallMsg): $($_.Exception.Message)" - } - Continue - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + [string[]]$GPUpdateCmds = '/C echo N | gpupdate.exe /Target:Computer /Force', '/C echo N | gpupdate.exe /Target:User /Force' + [int32]$InstallCount = 0 + ForEach ($GPUpdateCmd in $GPUpdateCmds) { + Try { + If ($InstallCount -eq 0) { + [string]$InstallMsg = 'Update Group Policies for the Machine' + Write-Log -Message "$($InstallMsg)..." -Source ${CmdletName} + } + Else { + [string]$InstallMsg = 'Update Group Policies for the User' + Write-Log -Message "$($InstallMsg)..." -Source ${CmdletName} + } + [psobject]$ExecuteResult = Execute-Process -Path "$envWinDir\system32\cmd.exe" -Parameters $GPUpdateCmd -WindowStyle 'Hidden' -PassThru -ExitOnProcessFailure $false + + If ($ExecuteResult.ExitCode -ne 0) { + If ($ExecuteResult.ExitCode -eq 60002) { + Throw "Execute-Process function failed with exit code [$($ExecuteResult.ExitCode)]." + } + Else { + Throw "gpupdate.exe failed with exit code [$($ExecuteResult.ExitCode)]." + } + } + $InstallCount++ + } + Catch { + Write-Log -Message "Failed to $($InstallMsg). `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to $($InstallMsg): $($_.Exception.Message)" + } + Continue + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9658,46 +9850,46 @@ Function Update-GroupPolicy { Function Enable-TerminalServerInstallMode { <# .SYNOPSIS - Changes to user install mode for Remote Desktop Session Host/Citrix servers. + Changes to user install mode for Remote Desktop Session Host/Citrix servers. .DESCRIPTION - Changes to user install mode for Remote Desktop Session Host/Citrix servers. + Changes to user install mode for Remote Desktop Session Host/Citrix servers. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Enable-TerminalServerInstallMode + Enable-TerminalServerInstallMode .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Change terminal server into user install mode...' -Source ${CmdletName} - $terminalServerResult = & change.exe User /Install - - If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } - } - Catch { - Write-Log -Message "Failed to change terminal server into user install mode. `n$(Resolve-Error) " -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to change terminal server into user install mode: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Change terminal server into user install mode...' -Source ${CmdletName} + $terminalServerResult = & "$envWinDir\System32\change.exe" User /Install + + If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } + } + Catch { + Write-Log -Message "Failed to change terminal server into user install mode. `n$(Resolve-Error) " -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to change terminal server into user install mode: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9706,46 +9898,46 @@ Function Enable-TerminalServerInstallMode { Function Disable-TerminalServerInstallMode { <# .SYNOPSIS - Changes to user install mode for Remote Desktop Session Host/Citrix servers. + Changes to user install mode for Remote Desktop Session Host/Citrix servers. .DESCRIPTION - Changes to user install mode for Remote Desktop Session Host/Citrix servers. + Changes to user install mode for Remote Desktop Session Host/Citrix servers. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Disable-TerminalServerInstallMode + Disable-TerminalServerInstallMode .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Change terminal server into user execute mode...' -Source ${CmdletName} - $terminalServerResult = & change.exe User /Execute - - If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } - } - Catch { - Write-Log -Message "Failed to change terminal server into user execute mode. `n$(Resolve-Error) " -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to change terminal server into user execute mode: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Change terminal server into user execute mode...' -Source ${CmdletName} + $terminalServerResult = & "$envWinDir\System32\change.exe" User /Execute + + If ($global:LastExitCode -ne 1) { Throw $terminalServerResult } + } + Catch { + Write-Log -Message "Failed to change terminal server into user execute mode. `n$(Resolve-Error) " -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to change terminal server into user execute mode: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9754,225 +9946,242 @@ Function Disable-TerminalServerInstallMode { Function Set-ActiveSetup { <# .SYNOPSIS - Creates an Active Setup entry in the registry to execute a file for each user upon login. + Creates an Active Setup entry in the registry to execute a file for each user upon login. .DESCRIPTION - Active Setup allows handling of per-user changes registry/file changes upon login. - A registry key is created in the HKLM registry hive which gets replicated to the HKCU hive when a user logs in. - If the "Version" value of the Active Setup entry in HKLM is higher than the version value in HKCU, the file referenced in "StubPath" is executed. - This Function: - - Creates the registry entries in HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$installName. - - Creates StubPath value depending on the file extension of the $StubExePath parameter. - - Handles Version value with YYYYMMDDHHMMSS granularity to permit re-installs on the same day and still trigger Active Setup after Version increase. - - Copies/overwrites the StubPath file to $StubExePath destination path if file exists in 'Files' subdirectory of script directory. - - Executes the StubPath file for the current user as long as not in Session 0 (no need to logout/login to trigger Active Setup). + Active Setup allows handling of per-user changes registry/file changes upon login. + A registry key is created in the HKLM registry hive which gets replicated to the HKCU hive when a user logs in. + If the "Version" value of the Active Setup entry in HKLM is higher than the version value in HKCU, the file referenced in "StubPath" is executed. + This Function: + - Creates the registry entries in HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$installName. + - Creates StubPath value depending on the file extension of the $StubExePath parameter. + - Handles Version value with YYYYMMDDHHMMSS granularity to permit re-installs on the same day and still trigger Active Setup after Version increase. + - Copies/overwrites the StubPath file to $StubExePath destination path if file exists in 'Files' subdirectory of script directory. + - Executes the StubPath file for the current user as long as not in Session 0 (no need to logout/login to trigger Active Setup). .PARAMETER StubExePath - Full destination path to the file that will be executed for each user that logs in. - If this file exists in the 'Files' subdirectory of the script directory, it will be copied to the destination path. + Full destination path to the file that will be executed for each user that logs in. + If this file exists in the 'Files' subdirectory of the script directory, it will be copied to the destination path. .PARAMETER Arguments - Arguments to pass to the file being executed. + Arguments to pass to the file being executed. .PARAMETER Description - Description for the Active Setup. Users will see "Setting up personalized settings for: $Description" at logon. Default is: $installName. + Description for the Active Setup. Users will see "Setting up personalized settings for: $Description" at logon. Default is: $installName. .PARAMETER Key - Name of the registry key for the Active Setup entry. Default is: $installName. + Name of the registry key for the Active Setup entry. Default is: $installName. .PARAMETER Version - Optional. Specify version for Active setup entry. Active Setup is not triggered if Version value has more than 8 consecutive digits. Use commas to get around this limitation. + Optional. Specify version for Active setup entry. Active Setup is not triggered if Version value has more than 8 consecutive digits. Use commas to get around this limitation. .PARAMETER Locale - Optional. Arbitrary string used to specify the installation language of the file being executed. Not replicated to HKCU. + Optional. Arbitrary string used to specify the installation language of the file being executed. Not replicated to HKCU. .PARAMETER PurgeActiveSetupKey - Remove Active Setup entry from HKLM registry hive. Will also load each logon user's HKCU registry hive to remove Active Setup entry. + Remove Active Setup entry from HKLM registry hive. Will also load each logon user's HKCU registry hive to remove Active Setup entry. .PARAMETER DisableActiveSetup - Disables the Active Setup entry so that the StubPath file will not be executed. + Disables the Active Setup entry so that the StubPath file will not be executed. +.PARAMETER ExecuteForCurrentUser + Specifies whether the StubExePath should be executed for the current user. Since this user is already logged in, the user won't have the application started without logging out and logging back in. Default: $True .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Set-ActiveSetup -StubExePath 'C:\Users\Public\Company\ProgramUserConfig.vbs' -Arguments '/Silent' -Description 'Program User Config' -Key 'ProgramUserConfig' -Locale 'en' + Set-ActiveSetup -StubExePath 'C:\Users\Public\Company\ProgramUserConfig.vbs' -Arguments '/Silent' -Description 'Program User Config' -Key 'ProgramUserConfig' -Locale 'en' .EXAMPLE - Set-ActiveSetup -StubExePath "$envWinDir\regedit.exe" -Arguments "/S `"%SystemDrive%\Program Files (x86)\PS App Deploy\PSAppDeployHKCUSettings.reg`"" -Description 'PS App Deploy Config' -Key 'PS_App_Deploy_Config' -ContinueOnError $true + Set-ActiveSetup -StubExePath "$envWinDir\regedit.exe" -Arguments "/S `"%SystemDrive%\Program Files (x86)\PS App Deploy\PSAppDeployHKCUSettings.reg`"" -Description 'PS App Deploy Config' -Key 'PS_App_Deploy_Config' -ContinueOnError $true .EXAMPLE - Set-ActiveSetup -Key 'ProgramUserConfig' -PurgeActiveSetupKey - Deletes "ProgramUserConfig" active setup entry from all registry hives. + Set-ActiveSetup -Key 'ProgramUserConfig' -PurgeActiveSetupKey + Deletes "ProgramUserConfig" active setup entry from all registry hives. .NOTES - Original code borrowed from: Denis St-Pierre (Ottawa, Canada), Todd MacNaught (Ottawa, Canada) + Original code borrowed from: Denis St-Pierre (Ottawa, Canada), Todd MacNaught (Ottawa, Canada) .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [string]$StubExePath, - [Parameter(Mandatory=$false,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [string]$Arguments, - [Parameter(Mandatory=$false,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [string]$Description = $installName, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$Key = $installName, - [Parameter(Mandatory=$false,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [string]$Version = ((Get-Date -Format 'yyMM,ddHH,mmss').ToString()), # Ex: 1405,1515,0522 - [Parameter(Mandatory=$false,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [string]$Locale, - [Parameter(Mandatory=$false,ParameterSetName='Create')] - [ValidateNotNullorEmpty()] - [switch]$DisableActiveSetup = $false, - [Parameter(Mandatory=$true,ParameterSetName='Purge')] - [switch]$PurgeActiveSetupKey, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [boolean]$ContinueOnError = $true - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - [string]$ActiveSetupKey = "HKLM:SOFTWARE\Microsoft\Active Setup\Installed Components\$Key" - [string]$HKCUActiveSetupKey = "HKCU:Software\Microsoft\Active Setup\Installed Components\$Key" - - ## Delete Active Setup registry entry from the HKLM hive and for all logon user registry hives on the system - If ($PurgeActiveSetupKey) { - Write-Log -Message "Remove Active Setup entry [$ActiveSetupKey]." -Source ${CmdletName} - Remove-RegistryKey -Key $ActiveSetupKey -Recurse - - Write-Log -Message "Remove Active Setup entry [$HKCUActiveSetupKey] for all log on user registry hives on the system." -Source ${CmdletName} - [scriptblock]$RemoveHKCUActiveSetupKey = { - If (Get-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID) { - Remove-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID -Recurse - } - } - Invoke-HKCURegistrySettingsForAllUsers -RegistrySettings $RemoveHKCUActiveSetupKey -UserProfiles (Get-UserProfiles -ExcludeDefaultUser) - Return - } - - ## Verify a file with a supported file extension was specified in $StubExePath - [string[]]$StubExePathFileExtensions = '.exe', '.vbs', '.cmd', '.ps1', '.js' - [string]$StubExeExt = [IO.Path]::GetExtension($StubExePath) - If ($StubExePathFileExtensions -notcontains $StubExeExt) { - Throw "Unsupported Active Setup StubPath file extension [$StubExeExt]." - } - - ## Copy file to $StubExePath from the 'Files' subdirectory of the script directory (if it exists there) - [string]$StubExePath = [Environment]::ExpandEnvironmentVariables($StubExePath) - [string]$ActiveSetupFileName = [IO.Path]::GetFileName($StubExePath) - [string]$StubExeFile = Join-Path -Path $dirFiles -ChildPath $ActiveSetupFileName - If (Test-Path -LiteralPath $StubExeFile -PathType 'Leaf') { - # This will overwrite the StubPath file if $StubExePath already exists on target - Copy-File -Path $StubExeFile -Destination $StubExePath -ContinueOnError $false - } - - ## Check if the $StubExePath file exists - If (-not (Test-Path -LiteralPath $StubExePath -PathType 'Leaf')) { Throw "Active Setup StubPath file [$ActiveSetupFileName] is missing." } - - ## Define Active Setup StubPath according to file extension of $StubExePath - Switch ($StubExeExt) { - '.exe' { - [string]$CUStubExePath = $StubExePath - [string]$CUArguments = $Arguments - [string]$StubPath = "$CUStubExePath" - } - {'.vbs','.js' -contains $StubExeExt} { - [string]$CUStubExePath = "$envWinDir\system32\cscript.exe" - [string]$CUArguments = "//nologo `"$StubExePath`"" - [string]$StubPath = "$CUStubExePath $CUArguments" - } - '.cmd' { - [string]$CUStubExePath = "$envWinDir\system32\CMD.exe" - [string]$CUArguments = "/C `"$StubExePath`"" - [string]$StubPath = "$CUStubExePath $CUArguments" - } - '.ps1' { - [string]$CUStubExePath = "$PSHOME\powershell.exe" - [string]$CUArguments = "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command `"& { & `\`"$StubExePath`\`"}`"" - [string]$StubPath = "$CUStubExePath $CUArguments" - } - } - If ($Arguments) { - [string]$StubPath = "$StubPath $Arguments" - If ($StubExeExt -ne '.exe') { [string]$CUArguments = "$CUArguments $Arguments" } - } - - ## Create the Active Setup entry in the registry - [scriptblock]$SetActiveSetupRegKeys = { - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullorEmpty()] - [string]$ActiveSetupRegKey, - [Parameter(Mandatory=$false)] - [ValidateNotNullorEmpty()] - [string]$SID - ) - If ($SID) { - Set-RegistryKey -Key $ActiveSetupRegKey -Name '(Default)' -Value $Description -SID $SID -ContinueOnError $false - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'StubPath' -Value $StubPath -Type 'String' -SID $SID -ContinueOnError $false - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Version' -Value $Version -SID $SID -ContinueOnError $false - If ($Locale) { Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Locale' -Value $Locale -SID $SID -ContinueOnError $false } - If ($DisableActiveSetup) { - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 0 -Type 'DWord' -SID $SID -ContinueOnError $false - } - Else { - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 1 -Type 'DWord' -SID $SID -ContinueOnError $false - } - } - Else { - Set-RegistryKey -Key $ActiveSetupRegKey -Name '(Default)' -Value $Description -ContinueOnError $false - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'StubPath' -Value $StubPath -Type 'String' -ContinueOnError $false - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Version' -Value $Version -ContinueOnError $false - If ($Locale) { Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Locale' -Value $Locale -ContinueOnError $false } - If ($DisableActiveSetup) { - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 0 -Type 'DWord' -ContinueOnError $false - } - Else { - Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 1 -Type 'DWord' -ContinueOnError $false - } - } - - } - & $SetActiveSetupRegKeys -ActiveSetupRegKey $ActiveSetupKey - - ## Execute the StubPath file for the current user as long as not in Session 0 - If ($SessionZero) { - If ($RunAsActiveUser) { - Write-Log -Message "Session 0 detected: Execute Active Setup StubPath file for currently logged in user [$($RunAsActiveUser.NTAccount)]." -Source ${CmdletName} - If ($CUArguments) { - Execute-ProcessAsUser -Path $CUStubExePath -Parameters $CUArguments -Wait -ContinueOnError $true - } - Else { - Execute-ProcessAsUser -Path $CUStubExePath -Wait -ContinueOnError $true - } - & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey -SID $RunAsActiveUser.SID - } - Else { - Write-Log -Message 'Session 0 detected: No logged in users detected. Active Setup StubPath file will execute when users first log into their account.' -Source ${CmdletName} - } - } - Else { - Write-Log -Message 'Execute Active Setup StubPath file for the current user.' -Source ${CmdletName} - If ($CUArguments) { - $ExecuteResults = Execute-Process -FilePath $CUStubExePath -Parameters $CUArguments -PassThru - } - Else { - $ExecuteResults = Execute-Process -FilePath $CUStubExePath -PassThru - } - & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey - } - } - Catch { - Write-Log -Message "Failed to set Active Setup registry entry. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed to set Active Setup registry entry: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [string]$StubExePath, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [string]$Arguments, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [string]$Description = $installName, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$Key = $installName, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [string]$Version = ((Get-Date -Format 'yyMM,ddHH,mmss').ToString()), # Ex: 1405,1515,0522 + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [string]$Locale, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [switch]$DisableActiveSetup = $false, + [Parameter(Mandatory=$true,ParameterSetName='Purge')] + [switch]$PurgeActiveSetupKey, + [Parameter(Mandatory=$false,ParameterSetName='Create')] + [ValidateNotNullorEmpty()] + [boolean]$ExecuteForCurrentUser = $true, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [boolean]$ContinueOnError = $true + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + [string]$ActiveSetupKey = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\$Key" + [string]$HKCUActiveSetupKey = "Registry::HKEY_CURRENT_USER\Software\Microsoft\Active Setup\Installed Components\$Key" + + ## Delete Active Setup registry entry from the HKLM hive and for all logon user registry hives on the system + If ($PurgeActiveSetupKey) { + Write-Log -Message "Removing Active Setup entry [$ActiveSetupKey]." -Source ${CmdletName} + Remove-RegistryKey -Key $ActiveSetupKey -Recurse + + Write-Log -Message "Removing Active Setup entry [$HKCUActiveSetupKey] for all log on user registry hives on the system." -Source ${CmdletName} + [scriptblock]$RemoveHKCUActiveSetupKey = { + If (Get-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID) { + Remove-RegistryKey -Key $HKCUActiveSetupKey -SID $UserProfile.SID -Recurse + } + } + Invoke-HKCURegistrySettingsForAllUsers -RegistrySettings $RemoveHKCUActiveSetupKey -UserProfiles (Get-UserProfiles -ExcludeDefaultUser) + Return + } + + ## Verify a file with a supported file extension was specified in $StubExePath + [string[]]$StubExePathFileExtensions = '.exe', '.vbs', '.cmd', '.ps1', '.js' + [string]$StubExeExt = [IO.Path]::GetExtension($StubExePath) + If ($StubExePathFileExtensions -notcontains $StubExeExt) { + Throw "Unsupported Active Setup StubPath file extension [$StubExeExt]." + } + + ## Copy file to $StubExePath from the 'Files' subdirectory of the script directory (if it exists there) + [string]$StubExePath = [Environment]::ExpandEnvironmentVariables($StubExePath) + [string]$ActiveSetupFileName = [IO.Path]::GetFileName($StubExePath) + [string]$StubExeFile = Join-Path -Path $dirFiles -ChildPath $ActiveSetupFileName + If (Test-Path -LiteralPath $StubExeFile -PathType 'Leaf') { + # This will overwrite the StubPath file if $StubExePath already exists on target + Copy-File -Path $StubExeFile -Destination $StubExePath -ContinueOnError $false + } + + ## Check if the $StubExePath file exists + If (-not (Test-Path -LiteralPath $StubExePath -PathType 'Leaf')) { Throw "Active Setup StubPath file [$ActiveSetupFileName] is missing." } + + ## Define Active Setup StubPath according to file extension of $StubExePath + Switch ($StubExeExt) { + '.exe' { + [string]$CUStubExePath = $StubExePath + [string]$CUArguments = $Arguments + [string]$StubPath = "$CUStubExePath" + } + {'.vbs','.js' -contains $StubExeExt} { + [string]$CUStubExePath = "$envWinDir\system32\cscript.exe" + [string]$CUArguments = "//nologo `"$StubExePath`"" + [string]$StubPath = "$CUStubExePath $CUArguments" + } + '.cmd' { + [string]$CUStubExePath = "$envWinDir\system32\CMD.exe" + [string]$CUArguments = "/C `"$StubExePath`"" + [string]$StubPath = "$CUStubExePath $CUArguments" + } + '.ps1' { + [string]$CUStubExePath = "$PSHOME\powershell.exe" + [string]$CUArguments = "-ExecutionPolicy Bypass -NoProfile -NoLogo -WindowStyle Hidden -Command `"& { & `\`"$StubExePath`\`"}`"" + [string]$StubPath = "$CUStubExePath $CUArguments" + } + } + If ($Arguments) { + [string]$StubPath = "$StubPath $Arguments" + If ($StubExeExt -ne '.exe') { [string]$CUArguments = "$CUArguments $Arguments" } + } + + ## Create the Active Setup entry in the registry + [scriptblock]$SetActiveSetupRegKeys = { + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullorEmpty()] + [string]$ActiveSetupRegKey, + [Parameter(Mandatory=$false)] + [ValidateNotNullorEmpty()] + [string]$SID + ) + If ($SID) { + Set-RegistryKey -Key $ActiveSetupRegKey -Name '(Default)' -Value $Description -SID $SID -ContinueOnError $false + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'StubPath' -Value $StubPath -Type 'String' -SID $SID -ContinueOnError $false + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Version' -Value $Version -SID $SID -ContinueOnError $false + If ($Locale) { Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Locale' -Value $Locale -SID $SID -ContinueOnError $false } + If ($DisableActiveSetup) { + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 0 -Type 'DWord' -SID $SID -ContinueOnError $false + } + Else { + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 1 -Type 'DWord' -SID $SID -ContinueOnError $false + } + } + Else { + Set-RegistryKey -Key $ActiveSetupRegKey -Name '(Default)' -Value $Description -ContinueOnError $false + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'StubPath' -Value $StubPath -Type 'String' -ContinueOnError $false + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Version' -Value $Version -ContinueOnError $false + If ($Locale) { Set-RegistryKey -Key $ActiveSetupRegKey -Name 'Locale' -Value $Locale -ContinueOnError $false } + If ($DisableActiveSetup) { + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 0 -Type 'DWord' -ContinueOnError $false + } + Else { + Set-RegistryKey -Key $ActiveSetupRegKey -Name 'IsInstalled' -Value 1 -Type 'DWord' -ContinueOnError $false + } + } + + } + + Write-Log -Message "Adding Active Setup Key for local machine: [$ActiveSetupKey]." -Source ${CmdletName} + & $SetActiveSetupRegKeys -ActiveSetupRegKey $ActiveSetupKey + + ## Execute the StubPath file for the current user as long as not in Session 0 + If ($SessionZero) { + If ($RunAsActiveUser) { + If ($ExecuteForCurrentUser) { + Write-Log -Message "Session 0 detected: Executing Active Setup StubPath file for currently logged in user [$($RunAsActiveUser.NTAccount)]." -Source ${CmdletName} + If ($CUArguments) { + Execute-ProcessAsUser -Path $CUStubExePath -Parameters $CUArguments -Wait -ContinueOnError $true + } + Else { + Execute-ProcessAsUser -Path $CUStubExePath -Wait -ContinueOnError $true + } + } + + Write-Log -Message "Adding Active Setup Key for the current user: [$HKCUActiveSetupKey]." -Source ${CmdletName} + & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey -SID $RunAsActiveUser.SID + } + Else { + If ($ExecuteForCurrentUser) { + Write-Log -Message 'Session 0 detected: No logged in users detected. Active Setup StubPath file will execute when users first log into their account.' -Source ${CmdletName} + } + } + } + Else { + If ($ExecuteForCurrentUser) { + Write-Log -Message 'Executing Active Setup StubPath file for the current user.' -Source ${CmdletName} + If ($CUArguments) { + $ExecuteResults = Execute-Process -FilePath $CUStubExePath -Parameters $CUArguments -PassThru -ExitOnProcessFailure $false + } + Else { + $ExecuteResults = Execute-Process -FilePath $CUStubExePath -PassThru -ExitOnProcessFailure $false + } + } + + Write-Log -Message "Adding Active Setup Key for the current user: [$HKCUActiveSetupKey]." -Source ${CmdletName} + & $SetActiveSetupRegKeys -ActiveSetupRegKey $HKCUActiveSetupKey + } + } + Catch { + Write-Log -Message "Failed to set Active Setup registry entry. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed to set Active Setup registry entry: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -9981,73 +10190,73 @@ Function Set-ActiveSetup { Function Test-ServiceExists { <# .SYNOPSIS - Check to see if a service exists. + Check to see if a service exists. .DESCRIPTION - Check to see if a service exists (using WMI method because Get-Service will generate ErrorRecord if service doesn't exist). + Check to see if a service exists (using WMI method because Get-Service will generate ErrorRecord if service doesn't exist). .PARAMETER Name - Specify the name of the service. - Note: Service name can be found by executing "Get-Service | Format-Table -AutoSize -Wrap" or by using the properties screen of a service in services.msc. + Specify the name of the service. + Note: Service name can be found by executing "Get-Service | Format-Table -AutoSize -Wrap" or by using the properties screen of a service in services.msc. .PARAMETER ComputerName - Specify the name of the computer. Default is: the local computer. + Specify the name of the computer. Default is: the local computer. .PARAMETER PassThru - Return the WMI service object. + Return the WMI service object. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Test-ServiceExists -Name 'wuauserv' + Test-ServiceExists -Name 'wuauserv' .EXAMPLE - Test-ServiceExists -Name 'testservice' -PassThru | Where-Object { $_ } | ForEach-Object { $_.Delete() } - Check if a service exists and then delete it by using the -PassThru parameter. + Test-ServiceExists -Name 'testservice' -PassThru | Where-Object { $_ } | ForEach-Object { $_.Delete() } + Check if a service exists and then delete it by using the -PassThru parameter. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$ComputerName = $env:ComputerName, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$PassThru, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - Begin { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - $ServiceObject = Get-WmiObject -ComputerName $ComputerName -Class 'Win32_Service' -Filter "Name='$Name'" -ErrorAction 'Stop' - # If nothing is returned from Win32_Service, check Win32_BaseService - If (-not ($ServiceObject) ) { - $ServiceObject = Get-WmiObject -ComputerName $ComputerName -Class 'Win32_BaseService' -Filter "Name='$Name'" -ErrorAction 'Stop' - } - - If ($ServiceObject) { - Write-Log -Message "Service [$Name] exists." -Source ${CmdletName} - If ($PassThru) { Write-Output -InputObject $ServiceObject } Else { Write-Output -InputObject $true } - } - Else { - Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} - If ($PassThru) { Write-Output -InputObject $ServiceObject } Else { Write-Output -InputObject $false } - } - } - Catch { - Write-Log -Message "Failed check to see if service [$Name] exists." -Severity 3 -Source ${CmdletName} - If (-not $ContinueOnError) { - Throw "Failed check to see if service [$Name] exists: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = $env:ComputerName, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$PassThru, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + Begin { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + $ServiceObject = Get-WmiObject -ComputerName $ComputerName -Class 'Win32_Service' -Filter "Name='$Name'" -ErrorAction 'Stop' + # If nothing is returned from Win32_Service, check Win32_BaseService + If (-not ($ServiceObject) ) { + $ServiceObject = Get-WmiObject -ComputerName $ComputerName -Class 'Win32_BaseService' -Filter "Name='$Name'" -ErrorAction 'Stop' + } + + If ($ServiceObject) { + Write-Log -Message "Service [$Name] exists." -Source ${CmdletName} + If ($PassThru) { Write-Output -InputObject $ServiceObject } Else { Write-Output -InputObject $true } + } + Else { + Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} + If ($PassThru) { Write-Output -InputObject $ServiceObject } Else { Write-Output -InputObject $false } + } + } + Catch { + Write-Log -Message "Failed check to see if service [$Name] exists." -Severity 3 -Source ${CmdletName} + If (-not $ContinueOnError) { + Throw "Failed check to see if service [$Name] exists: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10056,123 +10265,123 @@ Function Test-ServiceExists { Function Stop-ServiceAndDependencies { <# .SYNOPSIS - Stop Windows service and its dependencies. + Stop Windows service and its dependencies. .DESCRIPTION - Stop Windows service and its dependencies. + Stop Windows service and its dependencies. .PARAMETER Name - Specify the name of the service. + Specify the name of the service. .PARAMETER ComputerName - Specify the name of the computer. Default is: the local computer. + Specify the name of the computer. Default is: the local computer. .PARAMETER SkipServiceExistsTest - Choose to skip the test to check whether or not the service exists if it was already done outside of this function. + Choose to skip the test to check whether or not the service exists if it was already done outside of this function. .PARAMETER SkipDependentServices - Choose to skip checking for and stopping dependent services. Default is: $false. + Choose to skip checking for and stopping dependent services. Default is: $false. .PARAMETER PendingStatusWait - The amount of time to wait for a service to get out of a pending state before continuing. Default is 60 seconds. + The amount of time to wait for a service to get out of a pending state before continuing. Default is 60 seconds. .PARAMETER PassThru - Return the System.ServiceProcess.ServiceController service object. + Return the System.ServiceProcess.ServiceController service object. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Stop-ServiceAndDependencies -Name 'wuauserv' + Stop-ServiceAndDependencies -Name 'wuauserv' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$ComputerName = $env:ComputerName, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$SkipServiceExistsTest, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$SkipDependentServices, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [timespan]$PendingStatusWait = (New-TimeSpan -Seconds 60), - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$PassThru, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - Begin { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## Check to see if the service exists - If ((-not $SkipServiceExistsTest) -and (-not (Test-ServiceExists -ComputerName $ComputerName -Name $Name -ContinueOnError $false))) { - Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} -Severity 2 - Throw "Service [$Name] does not exist." - } - - ## Get the service object - Write-Log -Message "Get the service object for service [$Name]." -Source ${CmdletName} - [ServiceProcess.ServiceController]$Service = Get-Service -ComputerName $ComputerName -Name $Name -ErrorAction 'Stop' - ## Wait up to 60 seconds if service is in a pending state - [string[]]$PendingStatus = 'ContinuePending', 'PausePending', 'StartPending', 'StopPending' - If ($PendingStatus -contains $Service.Status) { - Switch ($Service.Status) { - 'ContinuePending' { $DesiredStatus = 'Running' } - 'PausePending' { $DesiredStatus = 'Paused' } - 'StartPending' { $DesiredStatus = 'Running' } - 'StopPending' { $DesiredStatus = 'Stopped' } - } - Write-Log -Message "Waiting for up to [$($PendingStatusWait.TotalSeconds)] seconds to allow service pending status [$($Service.Status)] to reach desired status [$DesiredStatus]." -Source ${CmdletName} - $Service.WaitForStatus([ServiceProcess.ServiceControllerStatus]$DesiredStatus, $PendingStatusWait) - $Service.Refresh() - } - ## Discover if the service is currently running - Write-Log -Message "Service [$($Service.ServiceName)] with display name [$($Service.DisplayName)] has a status of [$($Service.Status)]." -Source ${CmdletName} - If ($Service.Status -ne 'Stopped') { - # Discover all dependent services that are running and stop them - If (-not $SkipDependentServices) { - Write-Log -Message "Discover all dependent service(s) for service [$Name] which are not 'Stopped'." -Source ${CmdletName} - [ServiceProcess.ServiceController[]]$DependentServices = Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -DependentServices -ErrorAction 'Stop' | Where-Object { $_.Status -ne 'Stopped' } - If ($DependentServices) { - ForEach ($DependentService in $DependentServices) { - Write-Log -Message "Stop dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]." -Source ${CmdletName} - Try { - Stop-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $DependentService.ServiceName -ErrorAction 'Stop') -Force -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} - Continue - } - } - } - Else { - Write-Log -Message "Dependent service(s) were not discovered for service [$Name]." -Source ${CmdletName} - } - } - # Stop the parent service - Write-Log -Message "Stop parent service [$($Service.ServiceName)] with display name [$($Service.DisplayName)]." -Source ${CmdletName} - [ServiceProcess.ServiceController]$Service = Stop-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -ErrorAction 'Stop') -Force -PassThru -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' - } - } - Catch { - Write-Log -Message "Failed to stop the service [$Name]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 - If (-not $ContinueOnError) { - Throw "Failed to stop the service [$Name]: $($_.Exception.Message)" - } - } - Finally { - # Return the service object if option selected - If ($PassThru -and $Service) { Write-Output -InputObject $Service } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = $env:ComputerName, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$SkipServiceExistsTest, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$SkipDependentServices, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [timespan]$PendingStatusWait = (New-TimeSpan -Seconds 60), + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$PassThru, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + Begin { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## Check to see if the service exists + If ((-not $SkipServiceExistsTest) -and (-not (Test-ServiceExists -ComputerName $ComputerName -Name $Name -ContinueOnError $false))) { + Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} -Severity 2 + Throw "Service [$Name] does not exist." + } + + ## Get the service object + Write-Log -Message "Get the service object for service [$Name]." -Source ${CmdletName} + [ServiceProcess.ServiceController]$Service = Get-Service -ComputerName $ComputerName -Name $Name -ErrorAction 'Stop' + ## Wait up to 60 seconds if service is in a pending state + [string[]]$PendingStatus = 'ContinuePending', 'PausePending', 'StartPending', 'StopPending' + If ($PendingStatus -contains $Service.Status) { + Switch ($Service.Status) { + 'ContinuePending' { $DesiredStatus = 'Running' } + 'PausePending' { $DesiredStatus = 'Paused' } + 'StartPending' { $DesiredStatus = 'Running' } + 'StopPending' { $DesiredStatus = 'Stopped' } + } + Write-Log -Message "Waiting for up to [$($PendingStatusWait.TotalSeconds)] seconds to allow service pending status [$($Service.Status)] to reach desired status [$DesiredStatus]." -Source ${CmdletName} + $Service.WaitForStatus([ServiceProcess.ServiceControllerStatus]$DesiredStatus, $PendingStatusWait) + $Service.Refresh() + } + ## Discover if the service is currently running + Write-Log -Message "Service [$($Service.ServiceName)] with display name [$($Service.DisplayName)] has a status of [$($Service.Status)]." -Source ${CmdletName} + If ($Service.Status -ne 'Stopped') { + # Discover all dependent services that are running and stop them + If (-not $SkipDependentServices) { + Write-Log -Message "Discover all dependent service(s) for service [$Name] which are not 'Stopped'." -Source ${CmdletName} + [ServiceProcess.ServiceController[]]$DependentServices = Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -DependentServices -ErrorAction 'Stop' | Where-Object { $_.Status -ne 'Stopped' } + If ($DependentServices) { + ForEach ($DependentService in $DependentServices) { + Write-Log -Message "Stop dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]." -Source ${CmdletName} + Try { + Stop-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $DependentService.ServiceName -ErrorAction 'Stop') -Force -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} + Continue + } + } + } + Else { + Write-Log -Message "Dependent service(s) were not discovered for service [$Name]." -Source ${CmdletName} + } + } + # Stop the parent service + Write-Log -Message "Stop parent service [$($Service.ServiceName)] with display name [$($Service.DisplayName)]." -Source ${CmdletName} + [ServiceProcess.ServiceController]$Service = Stop-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -ErrorAction 'Stop') -Force -PassThru -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' + } + } + Catch { + Write-Log -Message "Failed to stop the service [$Name]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 + If (-not $ContinueOnError) { + Throw "Failed to stop the service [$Name]: $($_.Exception.Message)" + } + } + Finally { + # Return the service object if option selected + If ($PassThru -and $Service) { Write-Output -InputObject $Service } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10181,124 +10390,124 @@ Function Stop-ServiceAndDependencies { Function Start-ServiceAndDependencies { <# .SYNOPSIS - Start Windows service and its dependencies. + Start Windows service and its dependencies. .DESCRIPTION - Start Windows service and its dependencies. + Start Windows service and its dependencies. .PARAMETER Name - Specify the name of the service. + Specify the name of the service. .PARAMETER ComputerName - Specify the name of the computer. Default is: the local computer. + Specify the name of the computer. Default is: the local computer. .PARAMETER SkipServiceExistsTest - Choose to skip the test to check whether or not the service exists if it was already done outside of this function. + Choose to skip the test to check whether or not the service exists if it was already done outside of this function. .PARAMETER SkipDependentServices - Choose to skip checking for and starting dependent services. Default is: $false. + Choose to skip checking for and starting dependent services. Default is: $false. .PARAMETER PendingStatusWait - The amount of time to wait for a service to get out of a pending state before continuing. Default is 60 seconds. + The amount of time to wait for a service to get out of a pending state before continuing. Default is 60 seconds. .PARAMETER PassThru - Return the System.ServiceProcess.ServiceController service object. + Return the System.ServiceProcess.ServiceController service object. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Start-ServiceAndDependencies -Name 'wuauserv' + Start-ServiceAndDependencies -Name 'wuauserv' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$ComputerName = $env:ComputerName, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$SkipServiceExistsTest, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$SkipDependentServices, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [timespan]$PendingStatusWait = (New-TimeSpan -Seconds 60), - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [switch]$PassThru, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - Begin { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## Check to see if the service exists - If ((-not $SkipServiceExistsTest) -and (-not (Test-ServiceExists -ComputerName $ComputerName -Name $Name -ContinueOnError $false))) { - Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} -Severity 2 - Throw "Service [$Name] does not exist." - } - - ## Get the service object - Write-Log -Message "Get the service object for service [$Name]." -Source ${CmdletName} - [ServiceProcess.ServiceController]$Service = Get-Service -ComputerName $ComputerName -Name $Name -ErrorAction 'Stop' - ## Wait up to 60 seconds if service is in a pending state - [string[]]$PendingStatus = 'ContinuePending', 'PausePending', 'StartPending', 'StopPending' - If ($PendingStatus -contains $Service.Status) { - Switch ($Service.Status) { - 'ContinuePending' { $DesiredStatus = 'Running' } - 'PausePending' { $DesiredStatus = 'Paused' } - 'StartPending' { $DesiredStatus = 'Running' } - 'StopPending' { $DesiredStatus = 'Stopped' } - } - Write-Log -Message "Waiting for up to [$($PendingStatusWait.TotalSeconds)] seconds to allow service pending status [$($Service.Status)] to reach desired status [$DesiredStatus]." -Source ${CmdletName} - $Service.WaitForStatus([ServiceProcess.ServiceControllerStatus]$DesiredStatus, $PendingStatusWait) - $Service.Refresh() - } - ## Discover if the service is currently stopped - Write-Log -Message "Service [$($Service.ServiceName)] with display name [$($Service.DisplayName)] has a status of [$($Service.Status)]." -Source ${CmdletName} - If ($Service.Status -ne 'Running') { - # Start the parent service - Write-Log -Message "Start parent service [$($Service.ServiceName)] with display name [$($Service.DisplayName)]." -Source ${CmdletName} - [ServiceProcess.ServiceController]$Service = Start-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -ErrorAction 'Stop') -PassThru -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' - - # Discover all dependent services that are stopped and start them - If (-not $SkipDependentServices) { - Write-Log -Message "Discover all dependent service(s) for service [$Name] which are not 'Running'." -Source ${CmdletName} - [ServiceProcess.ServiceController[]]$DependentServices = Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -DependentServices -ErrorAction 'Stop' | Where-Object { $_.Status -ne 'Running' } - If ($DependentServices) { - ForEach ($DependentService in $DependentServices) { - Write-Log -Message "Start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]." -Source ${CmdletName} - Try { - Start-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $DependentService.ServiceName -ErrorAction 'Stop') -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' - } - Catch { - Write-Log -Message "Failed to start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} - Continue - } - } - } - Else { - Write-Log -Message "Dependent service(s) were not discovered for service [$Name]." -Source ${CmdletName} - } - } - } - } - Catch { - Write-Log -Message "Failed to start the service [$Name]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 - If (-not $ContinueOnError) { - Throw "Failed to start the service [$Name]: $($_.Exception.Message)" - } - } - Finally { - # Return the service object if option selected - If ($PassThru -and $Service) { Write-Output -InputObject $Service } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = $env:ComputerName, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$SkipServiceExistsTest, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$SkipDependentServices, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [timespan]$PendingStatusWait = (New-TimeSpan -Seconds 60), + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [switch]$PassThru, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + Begin { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## Check to see if the service exists + If ((-not $SkipServiceExistsTest) -and (-not (Test-ServiceExists -ComputerName $ComputerName -Name $Name -ContinueOnError $false))) { + Write-Log -Message "Service [$Name] does not exist." -Source ${CmdletName} -Severity 2 + Throw "Service [$Name] does not exist." + } + + ## Get the service object + Write-Log -Message "Get the service object for service [$Name]." -Source ${CmdletName} + [ServiceProcess.ServiceController]$Service = Get-Service -ComputerName $ComputerName -Name $Name -ErrorAction 'Stop' + ## Wait up to 60 seconds if service is in a pending state + [string[]]$PendingStatus = 'ContinuePending', 'PausePending', 'StartPending', 'StopPending' + If ($PendingStatus -contains $Service.Status) { + Switch ($Service.Status) { + 'ContinuePending' { $DesiredStatus = 'Running' } + 'PausePending' { $DesiredStatus = 'Paused' } + 'StartPending' { $DesiredStatus = 'Running' } + 'StopPending' { $DesiredStatus = 'Stopped' } + } + Write-Log -Message "Waiting for up to [$($PendingStatusWait.TotalSeconds)] seconds to allow service pending status [$($Service.Status)] to reach desired status [$DesiredStatus]." -Source ${CmdletName} + $Service.WaitForStatus([ServiceProcess.ServiceControllerStatus]$DesiredStatus, $PendingStatusWait) + $Service.Refresh() + } + ## Discover if the service is currently stopped + Write-Log -Message "Service [$($Service.ServiceName)] with display name [$($Service.DisplayName)] has a status of [$($Service.Status)]." -Source ${CmdletName} + If ($Service.Status -ne 'Running') { + # Start the parent service + Write-Log -Message "Start parent service [$($Service.ServiceName)] with display name [$($Service.DisplayName)]." -Source ${CmdletName} + [ServiceProcess.ServiceController]$Service = Start-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -ErrorAction 'Stop') -PassThru -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' + + # Discover all dependent services that are stopped and start them + If (-not $SkipDependentServices) { + Write-Log -Message "Discover all dependent service(s) for service [$Name] which are not 'Running'." -Source ${CmdletName} + [ServiceProcess.ServiceController[]]$DependentServices = Get-Service -ComputerName $ComputerName -Name $Service.ServiceName -DependentServices -ErrorAction 'Stop' | Where-Object { $_.Status -ne 'Running' } + If ($DependentServices) { + ForEach ($DependentService in $DependentServices) { + Write-Log -Message "Start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]." -Source ${CmdletName} + Try { + Start-Service -InputObject (Get-Service -ComputerName $ComputerName -Name $DependentService.ServiceName -ErrorAction 'Stop') -WarningAction 'SilentlyContinue' -ErrorAction 'Stop' + } + Catch { + Write-Log -Message "Failed to start dependent service [$($DependentService.ServiceName)] with display name [$($DependentService.DisplayName)] and a status of [$($DependentService.Status)]. Continue..." -Severity 2 -Source ${CmdletName} + Continue + } + } + } + Else { + Write-Log -Message "Dependent service(s) were not discovered for service [$Name]." -Source ${CmdletName} + } + } + } + } + Catch { + Write-Log -Message "Failed to start the service [$Name]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 + If (-not $ContinueOnError) { + Throw "Failed to start the service [$Name]: $($_.Exception.Message)" + } + } + Finally { + # Return the service object if option selected + If ($PassThru -and $Service) { Write-Output -InputObject $Service } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10308,67 +10517,67 @@ Function Get-ServiceStartMode { <# .SYNOPSIS - Get the service startup mode. + Get the service startup mode. .DESCRIPTION - Get the service startup mode. + Get the service startup mode. .PARAMETER Name - Specify the name of the service. + Specify the name of the service. .PARAMETER ComputerName - Specify the name of the computer. Default is: the local computer. + Specify the name of the computer. Default is: the local computer. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Get-ServiceStartMode -Name 'wuauserv' + Get-ServiceStartMode -Name 'wuauserv' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdLetBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$ComputerName = $env:ComputerName, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - Begin { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message "Get the service [$Name] startup mode." -Source ${CmdletName} - [string]$ServiceStartMode = (Get-WmiObject -ComputerName $ComputerName -Class 'Win32_Service' -Filter "Name='$Name'" -Property 'StartMode' -ErrorAction 'Stop').StartMode - ## If service start mode is set to 'Auto', change value to 'Automatic' to be consistent with 'Set-ServiceStartMode' function - If ($ServiceStartMode -eq 'Auto') { $ServiceStartMode = 'Automatic'} - - ## If on Windows Vista or higher, check to see if service is set to Automatic (Delayed Start) - If (($ServiceStartMode -eq 'Automatic') -and (([version]$envOSVersion).Major -gt 5)) { - Try { - [string]$ServiceRegistryPath = "HKLM:SYSTEM\CurrentControlSet\Services\$Name" - [int32]$DelayedAutoStart = Get-ItemProperty -LiteralPath $ServiceRegistryPath -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DelayedAutoStart' -ErrorAction 'Stop' - If ($DelayedAutoStart -eq 1) { $ServiceStartMode = 'Automatic (Delayed Start)' } - } - Catch { } - } - - Write-Log -Message "Service [$Name] startup mode is set to [$ServiceStartMode]." -Source ${CmdletName} - Write-Output -InputObject $ServiceStartMode - } - Catch { - Write-Log -Message "Failed to get the service [$Name] startup mode. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 - If (-not $ContinueOnError) { - Throw "Failed to get the service [$Name] startup mode: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdLetBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = $env:ComputerName, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + Begin { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message "Get the service [$Name] startup mode." -Source ${CmdletName} + [string]$ServiceStartMode = (Get-WmiObject -ComputerName $ComputerName -Class 'Win32_Service' -Filter "Name='$Name'" -Property 'StartMode' -ErrorAction 'Stop').StartMode + ## If service start mode is set to 'Auto', change value to 'Automatic' to be consistent with 'Set-ServiceStartMode' function + If ($ServiceStartMode -eq 'Auto') { $ServiceStartMode = 'Automatic'} + + ## If on Windows Vista or higher, check to see if service is set to Automatic (Delayed Start) + If (($ServiceStartMode -eq 'Automatic') -and (([version]$envOSVersion).Major -gt 5)) { + Try { + [string]$ServiceRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\$Name" + [int32]$DelayedAutoStart = Get-ItemProperty -LiteralPath $ServiceRegistryPath -ErrorAction 'Stop' | Select-Object -ExpandProperty 'DelayedAutoStart' -ErrorAction 'Stop' + If ($DelayedAutoStart -eq 1) { $ServiceStartMode = 'Automatic (Delayed Start)' } + } + Catch { } + } + + Write-Log -Message "Service [$Name] startup mode is set to [$ServiceStartMode]." -Source ${CmdletName} + Write-Output -InputObject $ServiceStartMode + } + Catch { + Write-Log -Message "Failed to get the service [$Name] startup mode. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 + If (-not $ContinueOnError) { + Throw "Failed to get the service [$Name] startup mode: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10378,74 +10587,74 @@ Function Set-ServiceStartMode { <# .SYNOPSIS - Set the service startup mode. + Set the service startup mode. .DESCRIPTION - Set the service startup mode. + Set the service startup mode. .PARAMETER Name - Specify the name of the service. + Specify the name of the service. .PARAMETER ComputerName - Specify the name of the computer. Default is: the local computer. + Specify the name of the computer. Default is: the local computer. .PARAMETER StartMode - Specify startup mode for the service. Options: Automatic, Automatic (Delayed Start), Manual, Disabled, Boot, System. + Specify startup mode for the service. Options: Automatic, Automatic (Delayed Start), Manual, Disabled, Boot, System. .PARAMETER ContinueOnError - Continue if an error is encountered. Default is: $true. + Continue if an error is encountered. Default is: $true. .EXAMPLE - Set-ServiceStartMode -Name 'wuauserv' -StartMode 'Automatic (Delayed Start)' + Set-ServiceStartMode -Name 'wuauserv' -StartMode 'Automatic (Delayed Start)' .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdLetBinding()] - Param ( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$ComputerName = $env:ComputerName, - [Parameter(Mandatory=$true)] - [ValidateSet('Automatic','Automatic (Delayed Start)','Manual','Disabled','Boot','System')] - [string]$StartMode, - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [boolean]$ContinueOnError = $true - ) - Begin { - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - ## If on lower than Windows Vista and 'Automatic (Delayed Start)' selected, then change to 'Automatic' because 'Delayed Start' is not supported. - If (($StartMode -eq 'Automatic (Delayed Start)') -and (([version]$envOSVersion).Major -lt 6)) { $StartMode = 'Automatic' } - - Write-Log -Message "Set service [$Name] startup mode to [$StartMode]." -Source ${CmdletName} - - ## Set the name of the start up mode that will be passed to sc.exe - [string]$ScExeStartMode = $StartMode - If ($StartMode -eq 'Automatic') { $ScExeStartMode = 'Auto' } - If ($StartMode -eq 'Automatic (Delayed Start)') { $ScExeStartMode = 'Delayed-Auto' } - If ($StartMode -eq 'Manual') { $ScExeStartMode = 'Demand' } - - ## Set the start up mode using sc.exe. Note: we found that the ChangeStartMode method in the Win32_Service WMI class set services to 'Automatic (Delayed Start)' even when you specified 'Automatic' on Win7, Win8, and Win10. - $ChangeStartMode = & sc.exe config $Name start= $ScExeStartMode - - If ($global:LastExitCode -ne 0) { - Throw "sc.exe failed with exit code [$($global:LastExitCode)] and message [$ChangeStartMode]." - } - - Write-Log -Message "Successfully set service [$Name] startup mode to [$StartMode]." -Source ${CmdletName} - } - Catch { - Write-Log -Message "Failed to set service [$Name] startup mode to [$StartMode]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 - If (-not $ContinueOnError) { - Throw "Failed to set service [$Name] startup mode to [$StartMode]: $($_.Exception.Message)" - } - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdLetBinding()] + Param ( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$ComputerName = $env:ComputerName, + [Parameter(Mandatory=$true)] + [ValidateSet('Automatic','Automatic (Delayed Start)','Manual','Disabled','Boot','System')] + [string]$StartMode, + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [boolean]$ContinueOnError = $true + ) + Begin { + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + ## If on lower than Windows Vista and 'Automatic (Delayed Start)' selected, then change to 'Automatic' because 'Delayed Start' is not supported. + If (($StartMode -eq 'Automatic (Delayed Start)') -and (([version]$envOSVersion).Major -lt 6)) { $StartMode = 'Automatic' } + + Write-Log -Message "Set service [$Name] startup mode to [$StartMode]." -Source ${CmdletName} + + ## Set the name of the start up mode that will be passed to sc.exe + [string]$ScExeStartMode = $StartMode + If ($StartMode -eq 'Automatic') { $ScExeStartMode = 'Auto' } + If ($StartMode -eq 'Automatic (Delayed Start)') { $ScExeStartMode = 'Delayed-Auto' } + If ($StartMode -eq 'Manual') { $ScExeStartMode = 'Demand' } + + ## Set the start up mode using sc.exe. Note: we found that the ChangeStartMode method in the Win32_Service WMI class set services to 'Automatic (Delayed Start)' even when you specified 'Automatic' on Win7, Win8, and Win10. + $ChangeStartMode = & "$envWinDir\System32\sc.exe" config $Name start= $ScExeStartMode + + If ($global:LastExitCode -ne 0) { + Throw "sc.exe failed with exit code [$($global:LastExitCode)] and message [$ChangeStartMode]." + } + + Write-Log -Message "Successfully set service [$Name] startup mode to [$StartMode]." -Source ${CmdletName} + } + Catch { + Write-Log -Message "Failed to set service [$Name] startup mode to [$StartMode]. `n$(Resolve-Error)" -Source ${CmdletName} -Severity 3 + If (-not $ContinueOnError) { + Throw "Failed to set service [$Name] startup mode to [$StartMode]: $($_.Exception.Message)" + } + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10454,58 +10663,58 @@ Function Set-ServiceStartMode Function Get-LoggedOnUser { <# .SYNOPSIS - Get session details for all local and RDP logged on users. + Get session details for all local and RDP logged on users. .DESCRIPTION - Get session details for all local and RDP logged on users using Win32 APIs. Get the following session details: - NTAccount, SID, UserName, DomainName, SessionId, SessionName, ConnectState, IsCurrentSession, IsConsoleSession, IsUserSession, IsActiveUserSession - IsRdpSession, IsLocalAdmin, LogonTime, IdleTime, DisconnectTime, ClientName, ClientProtocolType, ClientDirectory, ClientBuildNumber + Get session details for all local and RDP logged on users using Win32 APIs. Get the following session details: + NTAccount, SID, UserName, DomainName, SessionId, SessionName, ConnectState, IsCurrentSession, IsConsoleSession, IsUserSession, IsActiveUserSession + IsRdpSession, IsLocalAdmin, LogonTime, IdleTime, DisconnectTime, ClientName, ClientProtocolType, ClientDirectory, ClientBuildNumber .EXAMPLE - Get-LoggedOnUser + Get-LoggedOnUser .NOTES - Description of ConnectState property: - Value Description - ----- ----------- - Active A user is logged on to the session. - ConnectQuery The session is in the process of connecting to a client. - Connected A client is connected to the session. - Disconnected The session is active, but the client has disconnected from it. - Down The session is down due to an error. - Idle The session is waiting for a client to connect. - Initializing The session is initializing. - Listening The session is listening for connections. - Reset The session is being reset. - Shadowing This session is shadowing another session. - - Description of IsActiveUserSession property: - If a console user exists, then that will be the active user session. - If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that is either 'Active' or 'Connected' is the active user. - - Description of IsRdpSession property: - Gets a value indicating whether the user is associated with an RDP client session. + Description of ConnectState property: + Value Description + ----- ----------- + Active A user is logged on to the session. + ConnectQuery The session is in the process of connecting to a client. + Connected A client is connected to the session. + Disconnected The session is active, but the client has disconnected from it. + Down The session is down due to an error. + Idle The session is waiting for a client to connect. + Initializing The session is initializing. + Listening The session is listening for connections. + Reset The session is being reset. + Shadowing This session is shadowing another session. + + Description of IsActiveUserSession property: + If a console user exists, then that will be the active user session. + If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that is either 'Active' or 'Connected' is the active user. + + Description of IsRdpSession property: + Gets a value indicating whether the user is associated with an RDP client session. .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - } - Process { - Try { - Write-Log -Message 'Get session information for all logged on users.' -Source ${CmdletName} - Write-Output -InputObject ([PSADT.QueryUser]::GetUserSessionInfo("$env:ComputerName")) - } - Catch { - Write-Log -Message "Failed to get session information for all logged on users. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - End { - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + Process { + Try { + Write-Log -Message 'Get session information for all logged on users.' -Source ${CmdletName} + Write-Output -InputObject ([PSADT.QueryUser]::GetUserSessionInfo("$env:ComputerName")) + } + Catch { + Write-Log -Message "Failed to get session information for all logged on users. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion @@ -10514,178 +10723,393 @@ Function Get-LoggedOnUser { Function Get-PendingReboot { <# .SYNOPSIS - Get the pending reboot status on a local computer. + Get the pending reboot status on a local computer. .DESCRIPTION - Check WMI and the registry to determine if the system has a pending reboot operation from any of the following: - a) Component Based Servicing (Vista, Windows 2008) - b) Windows Update / Auto Update (XP, Windows 2003 / 2008) - c) SCCM 2012 Clients (DetermineIfRebootPending WMI method) - d) App-V Pending Tasks (global based Appv 5.0 SP2) - e) Pending File Rename Operations (XP, Windows 2003 / 2008) + Check WMI and the registry to determine if the system has a pending reboot operation from any of the following: + a) Component Based Servicing (Vista, Windows 2008) + b) Windows Update / Auto Update (XP, Windows 2003 / 2008) + c) SCCM 2012 Clients (DetermineIfRebootPending WMI method) + d) App-V Pending Tasks (global based Appv 5.0 SP2) + e) Pending File Rename Operations (XP, Windows 2003 / 2008) .EXAMPLE - Get-PendingReboot + Get-PendingReboot - Returns custom object with following properties: - ComputerName, LastBootUpTime, IsSystemRebootPending, IsCBServicingRebootPending, IsWindowsUpdateRebootPending, IsSCCMClientRebootPending, IsFileRenameRebootPending, PendingFileRenameOperations, ErrorMsg + Returns custom object with following properties: + ComputerName, LastBootUpTime, IsSystemRebootPending, IsCBServicingRebootPending, IsWindowsUpdateRebootPending, IsSCCMClientRebootPending, IsFileRenameRebootPending, PendingFileRenameOperations, ErrorMsg - *Notes: ErrorMsg only contains something if an error occurred + *Notes: ErrorMsg only contains something if an error occurred .EXAMPLE - (Get-PendingReboot).IsSystemRebootPending - Returns boolean value determining whether or not there is a pending reboot operation. + (Get-PendingReboot).IsSystemRebootPending + Returns boolean value determining whether or not there is a pending reboot operation. .NOTES .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> - [CmdletBinding()] - Param ( - ) - - Begin { - ## Get the name of this function and write header - [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header - - ## Initialize variables - [string]$private:ComputerName = ([Net.Dns]::GetHostEntry('')).HostName - $PendRebootErrorMsg = $null - } - Process { - Write-Log -Message "Get the pending reboot status on the local computer [$ComputerName]." -Source ${CmdletName} - - ## Get the date/time that the system last booted up - Try { - [nullable[datetime]]$LastBootUpTime = (Get-Date -ErrorAction 'Stop') - ([timespan]::FromMilliseconds([math]::Abs([Environment]::TickCount))) - } - Catch { - [nullable[datetime]]$LastBootUpTime = $null - [string[]]$PendRebootErrorMsg += "Failed to get LastBootUpTime: $($_.Exception.Message)" - Write-Log -Message "Failed to get LastBootUpTime. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - ## Determine if a Windows Vista/Server 2008 and above machine has a pending reboot from a Component Based Servicing (CBS) operation - Try { - If (([version]$envOSVersion).Major -ge 5) { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' -ErrorAction 'Stop') { - [nullable[boolean]]$IsCBServicingRebootPending = $true - } - Else { - [nullable[boolean]]$IsCBServicingRebootPending = $false - } - } - } - Catch { - [nullable[boolean]]$IsCBServicingRebootPending = $null - [string[]]$PendRebootErrorMsg += "Failed to get IsCBServicingRebootPending: $($_.Exception.Message)" - Write-Log -Message "Failed to get IsCBServicingRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - ## Determine if there is a pending reboot from a Windows Update - Try { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' -ErrorAction 'Stop') { - [nullable[boolean]]$IsWindowsUpdateRebootPending = $true - } - Else { - [nullable[boolean]]$IsWindowsUpdateRebootPending = $false - } - } - Catch { - [nullable[boolean]]$IsWindowsUpdateRebootPending = $null - [string[]]$PendRebootErrorMsg += "Failed to get IsWindowsUpdateRebootPending: $($_.Exception.Message)" - Write-Log -Message "Failed to get IsWindowsUpdateRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - ## Determine if there is a pending reboot from a pending file rename operation - [boolean]$IsFileRenameRebootPending = $false - $PendingFileRenameOperations = $null - If (Test-RegistryValue -Key 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations') { - # If PendingFileRenameOperations value exists, set $IsFileRenameRebootPending variable to $true - [boolean]$IsFileRenameRebootPending = $true - # Get the value of PendingFileRenameOperations - Try { - [string[]]$PendingFileRenameOperations = Get-ItemProperty -LiteralPath 'HKLM:SYSTEM\CurrentControlSet\Control\Session Manager' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'PendingFileRenameOperations' -ErrorAction 'Stop' - } - Catch { - [string[]]$PendRebootErrorMsg += "Failed to get PendingFileRenameOperations: $($_.Exception.Message)" - Write-Log -Message "Failed to get PendingFileRenameOperations. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - } - - ## Determine SCCM 2012 Client reboot pending status - Try { - [boolean]$IsSccmClientNamespaceExists = $false - [psobject]$SCCMClientRebootStatus = Invoke-WmiMethod -ComputerName $ComputerName -NameSpace 'ROOT\CCM\ClientSDK' -Class 'CCM_ClientUtilities' -Name 'DetermineIfRebootPending' -ErrorAction 'Stop' - [boolean]$IsSccmClientNamespaceExists = $true - If ($SCCMClientRebootStatus.ReturnValue -ne 0) { - Throw "'DetermineIfRebootPending' method of 'ROOT\CCM\ClientSDK\CCM_ClientUtilities' class returned error code [$($SCCMClientRebootStatus.ReturnValue)]" - } - Else { - Write-Log -Message 'Successfully queried SCCM client for reboot status.' -Source ${CmdletName} - [nullable[boolean]]$IsSCCMClientRebootPending = $false - If ($SCCMClientRebootStatus.IsHardRebootPending -or $SCCMClientRebootStatus.RebootPending) { - [nullable[boolean]]$IsSCCMClientRebootPending = $true - Write-Log -Message 'Pending SCCM reboot detected.' -Source ${CmdletName} - } - Else { - Write-Log -Message 'Pending SCCM reboot not detected.' -Source ${CmdletName} - } - } - } - Catch [System.Management.ManagementException] { - [nullable[boolean]]$IsSCCMClientRebootPending = $null - [boolean]$IsSccmClientNamespaceExists = $false - Write-Log -Message "Failed to get IsSCCMClientRebootPending. Failed to detect the SCCM client WMI class." -Severity 3 -Source ${CmdletName} - } - Catch { - [nullable[boolean]]$IsSCCMClientRebootPending = $null - [string[]]$PendRebootErrorMsg += "Failed to get IsSCCMClientRebootPending: $($_.Exception.Message)" - Write-Log -Message "Failed to get IsSCCMClientRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - ## Determine if there is a pending reboot from an App-V global Pending Task. (User profile based tasks will complete on logoff/logon) - Try { - If (Test-Path -LiteralPath 'HKLM:SOFTWARE\Software\Microsoft\AppV\Client\PendingTasks' -ErrorAction 'Stop') { - - [nullable[boolean]]$IsAppVRebootPending = $true - } - Else { - [nullable[boolean]]$IsAppVRebootPending = $false - } - } - Catch { - [nullable[boolean]]$IsAppVRebootPending = $null - [string[]]$PendRebootErrorMsg += "Failed to get IsAppVRebootPending: $($_.Exception.Message)" - Write-Log -Message "Failed to get IsAppVRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} - } - - ## Determine if there is a pending reboot for the system - [boolean]$IsSystemRebootPending = $false - If ($IsCBServicingRebootPending -or $IsWindowsUpdateRebootPending -or $IsSCCMClientRebootPending -or $IsFileRenameRebootPending) { - [boolean]$IsSystemRebootPending = $true - } - - ## Create a custom object containing pending reboot information for the system - [psobject]$PendingRebootInfo = New-Object -TypeName 'PSObject' -Property @{ - ComputerName = $ComputerName - LastBootUpTime = $LastBootUpTime - IsSystemRebootPending = $IsSystemRebootPending - IsCBServicingRebootPending = $IsCBServicingRebootPending - IsWindowsUpdateRebootPending = $IsWindowsUpdateRebootPending - IsSCCMClientRebootPending = $IsSCCMClientRebootPending - IsAppVRebootPending = $IsAppVRebootPending - IsFileRenameRebootPending = $IsFileRenameRebootPending - PendingFileRenameOperations = $PendingFileRenameOperations - ErrorMsg = $PendRebootErrorMsg - } - Write-Log -Message "Pending reboot status on the local computer [$ComputerName]: `n$($PendingRebootInfo | Format-List | Out-String)" -Source ${CmdletName} - } - End { - Write-Output -InputObject ($PendingRebootInfo | Select-Object -Property 'ComputerName','LastBootUpTime','IsSystemRebootPending','IsCBServicingRebootPending','IsWindowsUpdateRebootPending','IsSCCMClientRebootPending','IsAppVRebootPending','IsFileRenameRebootPending','PendingFileRenameOperations','ErrorMsg') - - Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer - } + [CmdletBinding()] + Param ( + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + + ## Initialize variables + [string]$private:ComputerName = $envComputerNameFQDN + $PendRebootErrorMsg = $null + } + Process { + Write-Log -Message "Get the pending reboot status on the local computer [$ComputerName]." -Source ${CmdletName} + + ## Get the date/time that the system last booted up + Try { + [nullable[datetime]]$LastBootUpTime = (Get-Date -ErrorAction 'Stop') - ([timespan]::FromMilliseconds([math]::Abs([Environment]::TickCount))) + } + Catch { + [nullable[datetime]]$LastBootUpTime = $null + [string[]]$PendRebootErrorMsg += "Failed to get LastBootUpTime: $($_.Exception.Message)" + Write-Log -Message "Failed to get LastBootUpTime. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Determine if a Windows Vista/Server 2008 and above machine has a pending reboot from a Component Based Servicing (CBS) operation + Try { + If (([version]$envOSVersion).Major -ge 5) { + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' -ErrorAction 'Stop') { + [nullable[boolean]]$IsCBServicingRebootPending = $true + } + Else { + [nullable[boolean]]$IsCBServicingRebootPending = $false + } + } + } + Catch { + [nullable[boolean]]$IsCBServicingRebootPending = $null + [string[]]$PendRebootErrorMsg += "Failed to get IsCBServicingRebootPending: $($_.Exception.Message)" + Write-Log -Message "Failed to get IsCBServicingRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Determine if there is a pending reboot from a Windows Update + Try { + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' -ErrorAction 'Stop') { + [nullable[boolean]]$IsWindowsUpdateRebootPending = $true + } + Else { + [nullable[boolean]]$IsWindowsUpdateRebootPending = $false + } + } + Catch { + [nullable[boolean]]$IsWindowsUpdateRebootPending = $null + [string[]]$PendRebootErrorMsg += "Failed to get IsWindowsUpdateRebootPending: $($_.Exception.Message)" + Write-Log -Message "Failed to get IsWindowsUpdateRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Determine if there is a pending reboot from a pending file rename operation + [boolean]$IsFileRenameRebootPending = $false + $PendingFileRenameOperations = $null + If (Test-RegistryValue -Key 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager' -Value 'PendingFileRenameOperations') { + # If PendingFileRenameOperations value exists, set $IsFileRenameRebootPending variable to $true + [boolean]$IsFileRenameRebootPending = $true + # Get the value of PendingFileRenameOperations + Try { + [string[]]$PendingFileRenameOperations = Get-ItemProperty -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager' -ErrorAction 'Stop' | Select-Object -ExpandProperty 'PendingFileRenameOperations' -ErrorAction 'Stop' + } + Catch { + [string[]]$PendRebootErrorMsg += "Failed to get PendingFileRenameOperations: $($_.Exception.Message)" + Write-Log -Message "Failed to get PendingFileRenameOperations. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + } + + ## Determine SCCM 2012 Client reboot pending status + Try { + [boolean]$IsSccmClientNamespaceExists = $false + [psobject]$SCCMClientRebootStatus = Invoke-WmiMethod -ComputerName $ComputerName -NameSpace 'ROOT\CCM\ClientSDK' -Class 'CCM_ClientUtilities' -Name 'DetermineIfRebootPending' -ErrorAction 'Stop' + [boolean]$IsSccmClientNamespaceExists = $true + If ($SCCMClientRebootStatus.ReturnValue -ne 0) { + Throw "'DetermineIfRebootPending' method of 'ROOT\CCM\ClientSDK\CCM_ClientUtilities' class returned error code [$($SCCMClientRebootStatus.ReturnValue)]" + } + Else { + Write-Log -Message 'Successfully queried SCCM client for reboot status.' -Source ${CmdletName} + [nullable[boolean]]$IsSCCMClientRebootPending = $false + If ($SCCMClientRebootStatus.IsHardRebootPending -or $SCCMClientRebootStatus.RebootPending) { + [nullable[boolean]]$IsSCCMClientRebootPending = $true + Write-Log -Message 'Pending SCCM reboot detected.' -Source ${CmdletName} + } + Else { + Write-Log -Message 'Pending SCCM reboot not detected.' -Source ${CmdletName} + } + } + } + Catch [System.Management.ManagementException] { + [nullable[boolean]]$IsSCCMClientRebootPending = $null + [boolean]$IsSccmClientNamespaceExists = $false + Write-Log -Message "Failed to get IsSCCMClientRebootPending. Failed to detect the SCCM client WMI class." -Severity 3 -Source ${CmdletName} + } + Catch { + [nullable[boolean]]$IsSCCMClientRebootPending = $null + [string[]]$PendRebootErrorMsg += "Failed to get IsSCCMClientRebootPending: $($_.Exception.Message)" + Write-Log -Message "Failed to get IsSCCMClientRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Determine if there is a pending reboot from an App-V global Pending Task. (User profile based tasks will complete on logoff/logon) + Try { + If (Test-Path -LiteralPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Software\Microsoft\AppV\Client\PendingTasks' -ErrorAction 'Stop') { + + [nullable[boolean]]$IsAppVRebootPending = $true + } + Else { + [nullable[boolean]]$IsAppVRebootPending = $false + } + } + Catch { + [nullable[boolean]]$IsAppVRebootPending = $null + [string[]]$PendRebootErrorMsg += "Failed to get IsAppVRebootPending: $($_.Exception.Message)" + Write-Log -Message "Failed to get IsAppVRebootPending. `n$(Resolve-Error)" -Severity 3 -Source ${CmdletName} + } + + ## Determine if there is a pending reboot for the system + [boolean]$IsSystemRebootPending = $false + If ($IsCBServicingRebootPending -or $IsWindowsUpdateRebootPending -or $IsSCCMClientRebootPending -or $IsFileRenameRebootPending) { + [boolean]$IsSystemRebootPending = $true + } + + ## Create a custom object containing pending reboot information for the system + [psobject]$PendingRebootInfo = New-Object -TypeName 'PSObject' -Property @{ + ComputerName = $ComputerName + LastBootUpTime = $LastBootUpTime + IsSystemRebootPending = $IsSystemRebootPending + IsCBServicingRebootPending = $IsCBServicingRebootPending + IsWindowsUpdateRebootPending = $IsWindowsUpdateRebootPending + IsSCCMClientRebootPending = $IsSCCMClientRebootPending + IsAppVRebootPending = $IsAppVRebootPending + IsFileRenameRebootPending = $IsFileRenameRebootPending + PendingFileRenameOperations = $PendingFileRenameOperations + ErrorMsg = $PendRebootErrorMsg + } + Write-Log -Message "Pending reboot status on the local computer [$ComputerName]: `n$($PendingRebootInfo | Format-List | Out-String)" -Source ${CmdletName} + } + End { + Write-Output -InputObject ($PendingRebootInfo | Select-Object -Property 'ComputerName','LastBootUpTime','IsSystemRebootPending','IsCBServicingRebootPending','IsWindowsUpdateRebootPending','IsSCCMClientRebootPending','IsAppVRebootPending','IsFileRenameRebootPending','PendingFileRenameOperations','ErrorMsg') + + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } } #endregion +#region Function Set-ItemPermission +Function Set-ItemPermission { + <# + .SYNOPSIS + Allow you to easily change permissions on files or folders + .PARAMETER Path + Path to the folder or file you want to modify (ex: C:\Temp) + .PARAMETER User + One or more user names (ex: BUILTIN\Users, DOMAIN\Admin) to give the permissions to. If you want to use SID, prefix it with an asterisk * (ex: *S-1-5-18) + .PARAMETER Permission + Permission or list of permissions to be set/added/removed/replaced. To see all the possible permissions go to 'http://technet.microsoft.com/fr-fr/library/ff730951.aspx'. + Permission DeleteSubdirectoriesAndFiles does not apply to files. + .PARAMETER PermissionType + Sets Access Control Type of the permissions. Allowed options: Allow, Deny Default: Allow + .PARAMETER Inheritance + Sets permission inheritance. Does not apply to files. Multiple options can be specified. Allowed options: ObjectInherit, ContainerInherit, None Default: None + None - The permission entry is not inherited by child objects, ObjectInherit - The permission entry is inherited by child leaf objects. ContainerInherit - The permission entry is inherited by child container objects. + .PARAMETER Propagation + Sets how to propagate inheritance. Does not apply to files. Allowed options: None, InheritOnly, NoPropagateInherit Default: None + None - Specifies that no inheritance flags are set. NoPropagateInherit - Specifies that the permission entry is not propagated to child objects. InheritOnly - Specifies that the permission entry is propagated only to child objects. This includes both container and leaf child objects. + .PARAMETER Method + Specifies which method will be used to apply the permissions. Allowed options: Add, Set, Reset. + Add - adds permissions rules but it does not remove previous permissions, Set - overwrites matching permission rules with new ones, Reset - removes matching permissions rules and then adds permission rules, Remove - Removes matching permission rules, RemoveSpecific - Removes specific permissions, RemoveAll - Removes all permission rules for specified user/s + Default: Add + .PARAMETER EnableInheritance + Enables inheritance on the files/folders. + .EXAMPLE + Will grant FullControl permissions to 'John' and 'Users' on 'C:\Temp' and its files and folders children. + PS C:\>Set-ItemPermission -Path "C:\Temp" -User "DOMAIN\John", "BUILTIN\Utilisateurs" -Permission FullControl -Inheritance ObjectInherit,ContainerInherit + .EXAMPLE + Will grant Read permissions to 'John' on 'C:\Temp\pic.png' + PS C:\>Set-ItemPermission -Path "C:\Temp\pic.png" -User "DOMAIN\John" -Permission Read + .EXAMPLE + Will remove all permissions to 'John' on 'C:\Temp\Private' + PS C:\>Set-ItemPermission -Path "C:\Temp\Private" -User "DOMAIN\John" -Permission None -Method RemoveAll + .NOTES + Original Author : Julian DA CUNHA - dacunha.julian@gmail.com, used with permission + .LINK + http://psappdeploytoolkit.com + #> + + [CmdletBinding()] + Param ( + [Parameter( Mandatory=$True, Position=0, HelpMessage = "Path to the folder or file you want to modify (ex: C:\Temp)",ParameterSetName="DisableInheritance" )] + [Parameter( Mandatory=$True, Position=0, HelpMessage = "Path to the folder or file you want to modify (ex: C:\Temp)",ParameterSetName="EnableInheritance" )] + [ValidateNotNullOrEmpty()] + [Alias('File', 'Folder')] + [String]$Path, + + [Parameter( Mandatory=$True, Position=1, HelpMessage = "One or more user names (ex: BUILTIN\Users, DOMAIN\Admin). If you want to use SID, prefix it with an asterisk * (ex: *S-1-5-18)", ParameterSetName="DisableInheritance")] + [Alias('Username', 'Users', 'SID', 'Usernames')] + [String[]]$User, + + [Parameter( Mandatory=$True, Position=2, HelpMessage = "Permission or list of permissions to be set/added/removed/replaced. To see all the possible permissions go to 'http://technet.microsoft.com/fr-fr/library/ff730951.aspx'", ParameterSetName="DisableInheritance")] + [Alias('Acl', 'Grant', 'Permissions', 'Deny')] + [ValidateSet("AppendData", "ChangePermissions", "CreateDirectories", "CreateFiles", "Delete", ` + "DeleteSubdirectoriesAndFiles", "ExecuteFile", "FullControl", "ListDirectory", "Modify",` + "Read", "ReadAndExecute", "ReadAttributes", "ReadData", "ReadExtendedAttributes", "ReadPermissions",` + "Synchronize", "TakeOwnership", "Traverse", "Write", "WriteAttributes", "WriteData", "WriteExtendedAttributes", "None")] + [String[]]$Permission, + + [Parameter( Mandatory=$False, Position=3, HelpMessage = "Whether you want to set Allow or Deny permissions", ParameterSetName="DisableInheritance")] + [Alias('AccessControlType')] + [ValidateSet("Allow", "Deny")] + [String]$PermissionType = "Allow", + + [Parameter( Mandatory=$False, Position=4, HelpMessage = "Sets how permissions are inherited", ParameterSetName="DisableInheritance")] + [ValidateSet("ContainerInherit", "None", "ObjectInherit")] + [String[]]$Inheritance = "None", + + [Parameter( Mandatory=$False, Position=5, HelpMessage = "Sets how to propage inheritance flags", ParameterSetName="DisableInheritance")] + [ValidateSet("None", "InheritOnly", "NoPropagateInherit")] + [String]$Propagation = "None", + + [Parameter( Mandatory=$False, Position=6, HelpMessage = "Specifies which method will be used to add/remove/replace permissions.", ParameterSetName="DisableInheritance")] + [ValidateSet("Add", "Set", "Reset", "Remove", "RemoveSpecific", "RemoveAll")] + [Alias("ApplyMethod", "ApplicationMethod")] + [String]$Method = "Add", + + [Parameter( Mandatory=$True, Position=1, HelpMessage = "Enables inheritance, which removes explicit permissions.", ParameterSetName="EnableInheritance")] + [switch]$EnableInheritance + ) + + Begin { + ## Get the name of this function and write header + [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header + } + + Process { + # Test elevated perms + If (-not $IsAdmin){ + Write-Log -Message "Unable to use the function [Set-ItemPermission] without elevated permissions." -Source ${CmdletName} + Throw "Unable to use the function [Set-ItemPermission] without elevated permissions." + } + + # Check path existence + If (-not (Test-Path -Path $Path -ErrorAction Stop)) { + Write-Log -Message "Specified path does not exist [$Path]." -Source ${CmdletName} + Throw "Specified path does not exist [$Path]." + } + + If ($EnableInheritance) { + # Get object acls + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Enable inherance + $Acl.SetAccessRuleProtection($False, $True) + Write-Log -Message "Enabling Inheritance on path [$Path]." -Source ${CmdletName} + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + return + } + # Permissions + [System.Security.AccessControl.FileSystemRights]$FileSystemRights = New-Object System.Security.AccessControl.FileSystemRights + If ($Permission -ne "None") { + foreach ($Entry in $Permission) { + $FileSystemRights = $FileSystemRights -bor [System.Security.AccessControl.FileSystemRights]$Entry + } + } + + # InheritanceFlags + $InheritanceFlag = New-Object System.Security.AccessControl.InheritanceFlags + foreach ($IFlag in $Inheritance) { + $InheritanceFlag = $InheritanceFlag -bor [System.Security.AccessControl.InheritanceFlags]$IFlag + } + + # PropagationFlags + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$Propagation + + # Access Control Type + $Allow = [System.Security.AccessControl.AccessControlType]$PermissionType + + # Modify variables to remove file incompatible flags if this is a file + If (Test-Path -Path $Path -ErrorAction Stop -PathType Leaf) { + $FileSystemRights = $FileSystemRights -band (-bnot [System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles) + $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]::None + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]::None + } + + # Get object acls + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Disable inherance, Preserve inherited permissions + $Acl.SetAccessRuleProtection($True, $True) + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + # Get updated acls - without inheritance + $Acl = $null + $Acl = (get-item -Path $Path -ErrorAction Stop).GetAccessControl('Access') + # Apply permissions on Users + Foreach ($U in $User) { + # Trim whitespace and skip if empty + $U = $U.Trim() + If ($U.Length -eq 0) { + continue + } + # Set Username + If ($U.StartsWith('*')) { + # This is a SID, remove the * + $U = $U.remove(0,1) + try { + # Translate the SID + $Username = ConvertTo-NTAccountOrSID -SID $U + } + catch { + Write-Log "Failed to translate SID [$U]. Skipping..." -Source ${CmdletName} -Severity 2 + continue + } + + $Username = New-Object System.Security.Principal.NTAccount($UsersAccountName) + } else { + $Username = New-Object System.Security.Principal.NTAccount($U) + } + + # Set/Add/Remove/Replace permissions and log the changes + $Rule = New-Object System.Security.AccessControl.FileSystemAccessRule($Username, $FileSystemRights, $InheritanceFlag, $PropagationFlag, $Allow) + switch ($Method) { + "Add" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.AddAccessRule($Rule) + break + } + "Set" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.SetAccessRule($Rule) + break + } + "Reset" { + Write-Log -Message "Setting permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.ResetAccessRule($Rule) + break + } + "Remove" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRule($Rule) + break + } + "RemoveSpecific" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRuleSpecific($Rule) + break + } + "RemoveAll" { + Write-Log -Message "Removing permissions [Permissions:$FileSystemRights, InheritanceFlags:$InheritanceFlag, PropagationFlags:$PropagationFlag, AccessControlType:$Allow, Method:$Method] on path [$Path] for user [$Username]." -Source ${CmdletName} + $Acl.RemoveAccessRuleAll($Rule) + break + } + } + } + # Use the prepared ACL + $null = Set-Acl -Path $Path -AclObject $Acl -ErrorAction Stop + } + + End { + Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer + } +} +#endregion #endregion ##*============================================= @@ -10699,13 +11123,13 @@ Function Get-PendingReboot { ## If the script was invoked by the Help Console, exit the script now If ($invokingScript) { - If ((Split-Path -Path $invokingScript -Leaf) -eq 'AppDeployToolkitHelp.ps1') { Return } + If ((Split-Path -Path $invokingScript -Leaf) -eq 'AppDeployToolkitHelp.ps1') { Return } } ## Add the custom types required for the toolkit If (-not ([Management.Automation.PSTypeName]'PSADT.UiAutomation').Type) { - [string[]]$ReferencedAssemblies = 'System.Drawing', 'System.Windows.Forms', 'System.DirectoryServices' - Add-Type -Path $appDeployCustomTypesSourceCode -ReferencedAssemblies $ReferencedAssemblies -IgnoreWarnings -ErrorAction 'Stop' + [string[]]$ReferencedAssemblies = 'System.Drawing', 'System.Windows.Forms', 'System.DirectoryServices' + Add-Type -Path $appDeployCustomTypesSourceCode -ReferencedAssemblies $ReferencedAssemblies -IgnoreWarnings -ErrorAction 'Stop' } ## Define ScriptBlocks to disable/revert script logging @@ -10714,45 +11138,45 @@ If (-not ([Management.Automation.PSTypeName]'PSADT.UiAutomation').Type) { ## Define ScriptBlock for getting details for all logged on users [scriptblock]$GetLoggedOnUserDetails = { - [psobject[]]$LoggedOnUserSessions = Get-LoggedOnUser - [string[]]$usersLoggedOn = $LoggedOnUserSessions | ForEach-Object { $_.NTAccount } + [psobject[]]$LoggedOnUserSessions = Get-LoggedOnUser + [string[]]$usersLoggedOn = $LoggedOnUserSessions | ForEach-Object { $_.NTAccount } - If ($usersLoggedOn) { - # Get account and session details for the logged on user session that the current process is running under. Note that the account used to execute the current process may be different than the account that is logged into the session (i.e. you can use "RunAs" to launch with different credentials when logged into an account). - [psobject]$CurrentLoggedOnUserSession = $LoggedOnUserSessions | Where-Object { $_.IsCurrentSession } + If ($usersLoggedOn) { + # Get account and session details for the logged on user session that the current process is running under. Note that the account used to execute the current process may be different than the account that is logged into the session (i.e. you can use "RunAs" to launch with different credentials when logged into an account). + [psobject]$CurrentLoggedOnUserSession = $LoggedOnUserSessions | Where-Object { $_.IsCurrentSession } - # Get account and session details for the account running as the console user (user with control of the physical monitor, keyboard, and mouse) - [psobject]$CurrentConsoleUserSession = $LoggedOnUserSessions | Where-Object { $_.IsConsoleSession } + # Get account and session details for the account running as the console user (user with control of the physical monitor, keyboard, and mouse) + [psobject]$CurrentConsoleUserSession = $LoggedOnUserSessions | Where-Object { $_.IsConsoleSession } - ## Determine the account that will be used to execute commands in the user session when toolkit is running under the SYSTEM account - # If a console user exists, then that will be the active user session. - # If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that is either 'Active' or 'Connected' is the active user. - [psobject]$RunAsActiveUser = $LoggedOnUserSessions | Where-Object { $_.IsActiveUserSession } - } + ## Determine the account that will be used to execute commands in the user session when toolkit is running under the SYSTEM account + # If a console user exists, then that will be the active user session. + # If no console user exists but users are logged in, such as on terminal servers, then the first logged-in non-console user that is either 'Active' or 'Connected' is the active user. + [psobject]$RunAsActiveUser = $LoggedOnUserSessions | Where-Object { $_.IsActiveUserSession } + } } ## Define ScriptBlock to test for and attempt to make a service healthy by checking if it exists, is currently running, and has the specified start mode. [scriptblock]$TestServiceHealth = { - Param ( - [string]$ServiceName, - [string]$ServiceStartMode = 'Automatic' - ) - Try { - [boolean]$IsServiceHealthy = $true - If (Test-ServiceExists -Name $ServiceName -ContinueOnError $false) { - If ((Get-ServiceStartMode -Name $ServiceName -ContinueOnError $false) -ne $ServiceStartMode) { - Set-ServiceStartMode -Name $ServiceName -StartMode $ServiceStartMode -ContinueOnError $false - } - Start-ServiceAndDependencies -Name $ServiceName -SkipServiceExistsTest -ContinueOnError $false - } - Else { - [boolean]$IsServiceHealthy = $false - } - } - Catch { - [boolean]$IsServiceHealthy = $false - } - Write-Output -InputObject $IsServiceHealthy + Param ( + [string]$ServiceName, + [string]$ServiceStartMode = 'Automatic' + ) + Try { + [boolean]$IsServiceHealthy = $true + If (Test-ServiceExists -Name $ServiceName -ContinueOnError $false) { + If ((Get-ServiceStartMode -Name $ServiceName -ContinueOnError $false) -ne $ServiceStartMode) { + Set-ServiceStartMode -Name $ServiceName -StartMode $ServiceStartMode -ContinueOnError $false + } + Start-ServiceAndDependencies -Name $ServiceName -SkipServiceExistsTest -ContinueOnError $false + } + Else { + [boolean]$IsServiceHealthy = $false + } + } + Catch { + [boolean]$IsServiceHealthy = $false + } + Write-Output -InputObject $IsServiceHealthy } ## Disable logging until log file details are available @@ -10760,67 +11184,67 @@ If (-not ([Management.Automation.PSTypeName]'PSADT.UiAutomation').Type) { ## If the default Deploy-Application.ps1 hasn't been modified, and the main script was not called by a referring script, check for MSI / MST and modify the install accordingly If ((-not $appName) -and (-not $ReferredInstallName)){ - # Build properly formatted Architecture String - switch ($Is64Bit) { - $false { $formattedOSArch = "x86" } - $true { $formattedOSArch = "x64" } - } - # Find the first MSI file in the Files folder and use that as our install - if ([string]$defaultMsiFile = (Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq ".msi") -and ($_.Name.EndsWith(".$formattedOSArch.msi")) } | Select-Object -ExpandProperty 'FullName' -First 1)) { - Write-Log -Message "Discovered $formattedOSArch Zerotouch MSI under $defaultMSIFile" -Source $appDeployToolkitName - } - elseif ([string]$defaultMsiFile = (Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq ".msi") } | Select-Object -ExpandProperty 'FullName' -First 1)) { - Write-Log -Message "Discovered Arch-Independent Zerotouch MSI under $defaultMSIFile" -Source $appDeployToolkitName - } - If ($defaultMsiFile) { - Try { - [boolean]$useDefaultMsi = $true - Write-Log -Message "Discovered Zero-Config MSI installation file [$defaultMsiFile]." -Source $appDeployToolkitName - # Discover if there is a zero-config MST file - [string]$defaultMstFile = [IO.Path]::ChangeExtension($defaultMsiFile, 'mst') - If (Test-Path -LiteralPath $defaultMstFile -PathType 'Leaf') { - Write-Log -Message "Discovered Zero-Config MST installation file [$defaultMstFile]." -Source $appDeployToolkitName - } - Else { - [string]$defaultMstFile = '' - } - # Discover if there are zero-config MSP files. Name multiple MSP files in alphabetical order to control order in which they are installed. - [string[]]$defaultMspFiles = Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq '.msp') } | Select-Object -ExpandProperty 'FullName' - If ($defaultMspFiles) { - Write-Log -Message "Discovered Zero-Config MSP installation file(s) [$($defaultMspFiles -join ',')]." -Source $appDeployToolkitName - } - - ## Read the MSI and get the installation details - [hashtable]$GetDefaultMsiTablePropertySplat = @{ Path = $defaultMsiFile; Table = 'Property'; ContinueOnError = $false; ErrorAction = 'Stop' } - If ($defaultMstFile) { $GetDefaultMsiTablePropertySplat.Add('TransformPath', $defaultMstFile) } - [psobject]$defaultMsiPropertyList = Get-MsiTableProperty @GetDefaultMsiTablePropertySplat - [string]$appVendor = $defaultMsiPropertyList.Manufacturer - [string]$appName = $defaultMsiPropertyList.ProductName - [string]$appVersion = $defaultMsiPropertyList.ProductVersion - $GetDefaultMsiTablePropertySplat.Set_Item('Table', 'File') - [psobject]$defaultMsiFileList = Get-MsiTableProperty @GetDefaultMsiTablePropertySplat - [string[]]$defaultMsiExecutables = Get-Member -InputObject $defaultMsiFileList -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Name' -ErrorAction 'Stop' | Where-Object { [IO.Path]::GetExtension($_) -eq '.exe' } | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_) } - [string]$defaultMsiExecutablesList = $defaultMsiExecutables -join ',' - Write-Log -Message "App Vendor [$appVendor]." -Source $appDeployToolkitName - Write-Log -Message "App Name [$appName]." -Source $appDeployToolkitName - Write-Log -Message "App Version [$appVersion]." -Source $appDeployToolkitName - Write-Log -Message "MSI Executable List [$defaultMsiExecutablesList]." -Source $appDeployToolkitName - } - Catch { - Write-Log -Message "Failed to process Zero-Config MSI Deployment. `n$(Resolve-Error)" -Source $appDeployToolkitName - $useDefaultMsi = $false ; $appVendor = '' ; $appName = '' ; $appVersion = '' - } - } + # Build properly formatted Architecture String + switch ($Is64Bit) { + $false { $formattedOSArch = "x86" } + $true { $formattedOSArch = "x64" } + } + # Find the first MSI file in the Files folder and use that as our install + if ([string]$defaultMsiFile = (Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq ".msi") -and ($_.Name.EndsWith(".$formattedOSArch.msi")) } | Select-Object -ExpandProperty 'FullName' -First 1)) { + Write-Log -Message "Discovered $formattedOSArch Zerotouch MSI under $defaultMSIFile" -Source $appDeployToolkitName + } + elseif ([string]$defaultMsiFile = (Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq ".msi") } | Select-Object -ExpandProperty 'FullName' -First 1)) { + Write-Log -Message "Discovered Arch-Independent Zerotouch MSI under $defaultMSIFile" -Source $appDeployToolkitName + } + If ($defaultMsiFile) { + Try { + [boolean]$useDefaultMsi = $true + Write-Log -Message "Discovered Zero-Config MSI installation file [$defaultMsiFile]." -Source $appDeployToolkitName + # Discover if there is a zero-config MST file + [string]$defaultMstFile = [IO.Path]::ChangeExtension($defaultMsiFile, 'mst') + If (Test-Path -LiteralPath $defaultMstFile -PathType 'Leaf') { + Write-Log -Message "Discovered Zero-Config MST installation file [$defaultMstFile]." -Source $appDeployToolkitName + } + Else { + [string]$defaultMstFile = '' + } + # Discover if there are zero-config MSP files. Name multiple MSP files in alphabetical order to control order in which they are installed. + [string[]]$defaultMspFiles = Get-ChildItem -LiteralPath $dirFiles -ErrorAction 'SilentlyContinue' | Where-Object { (-not $_.PsIsContainer) -and ([IO.Path]::GetExtension($_.Name) -eq '.msp') } | Select-Object -ExpandProperty 'FullName' + If ($defaultMspFiles) { + Write-Log -Message "Discovered Zero-Config MSP installation file(s) [$($defaultMspFiles -join ',')]." -Source $appDeployToolkitName + } + + ## Read the MSI and get the installation details + [hashtable]$GetDefaultMsiTablePropertySplat = @{ Path = $defaultMsiFile; Table = 'Property'; ContinueOnError = $false; ErrorAction = 'Stop' } + If ($defaultMstFile) { $GetDefaultMsiTablePropertySplat.Add('TransformPath', $defaultMstFile) } + [psobject]$defaultMsiPropertyList = Get-MsiTableProperty @GetDefaultMsiTablePropertySplat + [string]$appVendor = $defaultMsiPropertyList.Manufacturer + [string]$appName = $defaultMsiPropertyList.ProductName + [string]$appVersion = $defaultMsiPropertyList.ProductVersion + $GetDefaultMsiTablePropertySplat.Set_Item('Table', 'File') + [psobject]$defaultMsiFileList = Get-MsiTableProperty @GetDefaultMsiTablePropertySplat + [string[]]$defaultMsiExecutables = Get-Member -InputObject $defaultMsiFileList -ErrorAction 'Stop' | Select-Object -ExpandProperty 'Name' -ErrorAction 'Stop' | Where-Object { [IO.Path]::GetExtension($_) -eq '.exe' } | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($_) } + [string]$defaultMsiExecutablesList = $defaultMsiExecutables -join ',' + Write-Log -Message "App Vendor [$appVendor]." -Source $appDeployToolkitName + Write-Log -Message "App Name [$appName]." -Source $appDeployToolkitName + Write-Log -Message "App Version [$appVersion]." -Source $appDeployToolkitName + Write-Log -Message "MSI Executable List [$defaultMsiExecutablesList]." -Source $appDeployToolkitName + } + Catch { + Write-Log -Message "Failed to process Zero-Config MSI Deployment. `n$(Resolve-Error)" -Source $appDeployToolkitName + $useDefaultMsi = $false ; $appVendor = '' ; $appName = '' ; $appVersion = '' + } + } } ## Set up sample variables if Dot Sourcing the script, app details have not been specified If (-not $appName) { - [string]$appName = $appDeployMainScriptFriendlyName - If (-not $appVendor) { [string]$appVendor = 'PS' } - If (-not $appVersion) { [string]$appVersion = $appDeployMainScriptVersion } - If (-not $appLang) { [string]$appLang = $currentLanguage } - If (-not $appRevision) { [string]$appRevision = '01' } - If (-not $appArch) { [string]$appArch = '' } + [string]$appName = $appDeployMainScriptFriendlyName + If (-not $appVendor) { [string]$appVendor = 'PS' } + If (-not $appVersion) { [string]$appVersion = $appDeployMainScriptVersion } + If (-not $appLang) { [string]$appLang = $currentLanguage } + If (-not $appRevision) { [string]$appRevision = '01' } + If (-not $appArch) { [string]$appArch = '' } } ## Sanitize the application details, as they can cause issues in the script @@ -10834,7 +11258,7 @@ If (-not $appName) { ## Build the Installation Title If ($ReferredInstallTitle) { [string]$installTitle = (Remove-InvalidFileNameChars -Name ($ReferredInstallTitle.Trim())) } If (-not $installTitle) { - [string]$installTitle = "$appVendor $appName $appVersion" + [string]$installTitle = "$appVendor $appName $appVersion" } ## Set Powershell window title, in case the window is visible @@ -10844,12 +11268,12 @@ $Host.UI.RawUI.WindowTitle = "$installTitle - $DeploymentType" ## Build the Installation Name If ($ReferredInstallName) { [string]$installName = (Remove-InvalidFileNameChars -Name $ReferredInstallName) } If (-not $installName) { - If ($appArch) { - [string]$installName = $appVendor + '_' + $appName + '_' + $appVersion + '_' + $appArch + '_' + $appLang + '_' + $appRevision - } - Else { - [string]$installName = $appVendor + '_' + $appName + '_' + $appVersion + '_' + $appLang + '_' + $appRevision - } + If ($appArch) { + [string]$installName = $appVendor + '_' + $appName + '_' + $appVersion + '_' + $appArch + '_' + $appLang + '_' + $appRevision + } + Else { + [string]$installName = $appVendor + '_' + $appName + '_' + $appVersion + '_' + $appLang + '_' + $appRevision + } } [string]$installName = (($installName -replace ' ','').Trim('_') -replace '[_]+','_') @@ -10862,10 +11286,10 @@ If (-not $logName) { [string]$logName = $installName + '_' + $appDeployToolkitNa # If option to compress logs is selected, then log will be created in temp log folder ($logTempFolder) and then copied to actual log folder ($configToolkitLogDir) after being zipped. [string]$logTempFolder = Join-Path -Path $envTemp -ChildPath "${installName}_$deploymentType" If ($configToolkitCompressLogs) { - # If the temp log folder already exists from a previous ZIP operation, then delete all files in it to avoid issues - If (Test-Path -LiteralPath $logTempFolder -PathType 'Container' -ErrorAction 'SilentlyContinue') { - $null = Remove-Item -LiteralPath $logTempFolder -Recurse -Force -ErrorAction 'SilentlyContinue' - } + # If the temp log folder already exists from a previous ZIP operation, then delete all files in it to avoid issues + If (Test-Path -LiteralPath $logTempFolder -PathType 'Container' -ErrorAction 'SilentlyContinue') { + $null = Remove-Item -LiteralPath $logTempFolder -Recurse -Force -ErrorAction 'SilentlyContinue' + } } ## Revert script logging to original setting @@ -10879,34 +11303,34 @@ Write-Log -Message "[$installName] setup started." -Source $appDeployToolkitName ## Assemblies: Load Try { - Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' - Add-Type -AssemblyName 'PresentationFramework' -ErrorAction 'Stop' - Add-Type -AssemblyName 'Microsoft.VisualBasic' -ErrorAction 'Stop' - Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' - Add-Type -AssemblyName 'PresentationCore' -ErrorAction 'Stop' - Add-Type -AssemblyName 'WindowsBase' -ErrorAction 'Stop' + Add-Type -AssemblyName 'System.Windows.Forms' -ErrorAction 'Stop' + Add-Type -AssemblyName 'PresentationFramework' -ErrorAction 'Stop' + Add-Type -AssemblyName 'Microsoft.VisualBasic' -ErrorAction 'Stop' + Add-Type -AssemblyName 'System.Drawing' -ErrorAction 'Stop' + Add-Type -AssemblyName 'PresentationCore' -ErrorAction 'Stop' + Add-Type -AssemblyName 'WindowsBase' -ErrorAction 'Stop' } Catch { - Write-Log -Message "Failed to load assembly. `n$(Resolve-Error)" -Severity 3 -Source $appDeployToolkitName - If ($deployModeNonInteractive) { - Write-Log -Message "Continue despite assembly load error since deployment mode is [$deployMode]." -Source $appDeployToolkitName - } - Else { - Exit-Script -ExitCode 60004 - } + Write-Log -Message "Failed to load assembly. `n$(Resolve-Error)" -Severity 3 -Source $appDeployToolkitName + If ($deployModeNonInteractive) { + Write-Log -Message "Continue despite assembly load error since deployment mode is [$deployMode]." -Source $appDeployToolkitName + } + Else { + Exit-Script -ExitCode 60004 + } } ## Check how the script was invoked If ($invokingScript) { - Write-Log -Message "Script [$scriptPath] dot-source invoked by [$invokingScript]" -Source $appDeployToolkitName + Write-Log -Message "Script [$scriptPath] dot-source invoked by [$invokingScript]" -Source $appDeployToolkitName } Else { - Write-Log -Message "Script [$scriptPath] invoked directly" -Source $appDeployToolkitName + Write-Log -Message "Script [$scriptPath] invoked directly" -Source $appDeployToolkitName } ## Dot Source script extensions If (Test-Path -LiteralPath "$scriptRoot\$appDeployToolkitDotSourceExtensions" -PathType 'Leaf') { - . "$scriptRoot\$appDeployToolkitDotSourceExtensions" + . "$scriptRoot\$appDeployToolkitDotSourceExtensions" } ## Evaluate non-default parameters passed to the scripts @@ -10918,13 +11342,15 @@ If ($appDeployExtScriptParameters) { [string]$appDeployExtScriptParameters = ($a ## Check the XML config file version If ($configConfigVersion -lt $appDeployMainScriptMinimumConfigVersion) { - [string]$XMLConfigVersionErr = "The XML configuration file version [$configConfigVersion] is lower than the supported version required by the Toolkit [$appDeployMainScriptMinimumConfigVersion]. Please upgrade the configuration file." - Write-Log -Message $XMLConfigVersionErr -Severity 3 -Source $appDeployToolkitName - Throw $XMLConfigVersionErr + [string]$XMLConfigVersionErr = "The XML configuration file version [$configConfigVersion] is lower than the supported version required by the Toolkit [$appDeployMainScriptMinimumConfigVersion]. Please upgrade the configuration file." + Write-Log -Message $XMLConfigVersionErr -Severity 3 -Source $appDeployToolkitName + Throw $XMLConfigVersionErr } ## Log system/script information If ($appScriptVersion) { Write-Log -Message "[$installName] script version is [$appScriptVersion]" -Source $appDeployToolkitName } +If ($appScriptDate) { Write-Log -Message "[$installName] script date is [$appScriptDate]" -Source $appDeployToolkitName } +If ($appScriptAuthor) { Write-Log -Message "[$installName] script author is [$appScriptAuthor]" -Source $appDeployToolkitName } If ($deployAppScriptFriendlyName) { Write-Log -Message "[$deployAppScriptFriendlyName] script version is [$deployAppScriptVersion]" -Source $appDeployToolkitName } If ($deployAppScriptParameters) { Write-Log -Message "The following non-default parameters were passed to [$deployAppScriptFriendlyName]: [$deployAppScriptParameters]" -Source $appDeployToolkitName } If ($appDeployMainScriptFriendlyName) { Write-Log -Message "[$appDeployMainScriptFriendlyName] script version is [$appDeployMainScriptVersion]" -Source $appDeployToolkitName } @@ -10934,10 +11360,10 @@ If ($appDeployExtScriptParameters) { Write-Log -Message "The following non-defau Write-Log -Message "Computer Name is [$envComputerNameFQDN]" -Source $appDeployToolkitName Write-Log -Message "Current User is [$ProcessNTAccount]" -Source $appDeployToolkitName If ($envOSServicePack) { - Write-Log -Message "OS Version is [$envOSName $envOSServicePack $envOSArchitecture $envOSVersion]" -Source $appDeployToolkitName + Write-Log -Message "OS Version is [$envOSName $envOSServicePack $envOSArchitecture $envOSVersion]" -Source $appDeployToolkitName } Else { - Write-Log -Message "OS Version is [$envOSName $envOSArchitecture $envOSVersion]" -Source $appDeployToolkitName + Write-Log -Message "OS Version is [$envOSName $envOSArchitecture $envOSVersion]" -Source $appDeployToolkitName } Write-Log -Message "OS Type is [$envOSProductTypeName]" -Source $appDeployToolkitName Write-Log -Message "Current Culture is [$($culture.Name)], language is [$currentLanguage] and UI language is [$currentUILanguage]" -Source $appDeployToolkitName @@ -10964,130 +11390,130 @@ Write-Log -Message $scriptSeparator -Source $appDeployToolkitName ## Set the install phase to asynchronous if the script was not dot sourced, i.e. called with parameters If ($AsyncToolkitLaunch) { - $installPhase = 'Asynchronous' + $installPhase = 'Asynchronous' } ## If the ShowInstallationPrompt Parameter is specified, only call that function. If ($showInstallationPrompt) { - Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowInstallationPrompt]." -Source $appDeployToolkitName - $appDeployMainScriptAsyncParameters.Remove('ShowInstallationPrompt') - $appDeployMainScriptAsyncParameters.Remove('AsyncToolkitLaunch') - $appDeployMainScriptAsyncParameters.Remove('ReferredInstallName') - $appDeployMainScriptAsyncParameters.Remove('ReferredInstallTitle') - $appDeployMainScriptAsyncParameters.Remove('ReferredLogName') - Show-InstallationPrompt @appDeployMainScriptAsyncParameters - Exit 0 + Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowInstallationPrompt]." -Source $appDeployToolkitName + $appDeployMainScriptAsyncParameters.Remove('ShowInstallationPrompt') + $appDeployMainScriptAsyncParameters.Remove('AsyncToolkitLaunch') + $appDeployMainScriptAsyncParameters.Remove('ReferredInstallName') + $appDeployMainScriptAsyncParameters.Remove('ReferredInstallTitle') + $appDeployMainScriptAsyncParameters.Remove('ReferredLogName') + Show-InstallationPrompt @appDeployMainScriptAsyncParameters + Exit 0 } ## If the ShowInstallationRestartPrompt Parameter is specified, only call that function. If ($showInstallationRestartPrompt) { - Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowInstallationRestartPrompt]." -Source $appDeployToolkitName - $appDeployMainScriptAsyncParameters.Remove('ShowInstallationRestartPrompt') - $appDeployMainScriptAsyncParameters.Remove('AsyncToolkitLaunch') - $appDeployMainScriptAsyncParameters.Remove('ReferredInstallName') - $appDeployMainScriptAsyncParameters.Remove('ReferredInstallTitle') - $appDeployMainScriptAsyncParameters.Remove('ReferredLogName') - Show-InstallationRestartPrompt @appDeployMainScriptAsyncParameters - Exit 0 + Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowInstallationRestartPrompt]." -Source $appDeployToolkitName + $appDeployMainScriptAsyncParameters.Remove('ShowInstallationRestartPrompt') + $appDeployMainScriptAsyncParameters.Remove('AsyncToolkitLaunch') + $appDeployMainScriptAsyncParameters.Remove('ReferredInstallName') + $appDeployMainScriptAsyncParameters.Remove('ReferredInstallTitle') + $appDeployMainScriptAsyncParameters.Remove('ReferredLogName') + Show-InstallationRestartPrompt @appDeployMainScriptAsyncParameters + Exit 0 } ## If the CleanupBlockedApps Parameter is specified, only call that function. If ($cleanupBlockedApps) { - $deployModeSilent = $true - Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-CleanupBlockedApps]." -Source $appDeployToolkitName - Unblock-AppExecution - Exit 0 + $deployModeSilent = $true + Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-CleanupBlockedApps]." -Source $appDeployToolkitName + Unblock-AppExecution + Exit 0 } ## If the ShowBlockedAppDialog Parameter is specified, only call that function. If ($showBlockedAppDialog) { - Try { - . $DisableScriptLogging - Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowBlockedAppDialog]." -Source $appDeployToolkitName - # Create a mutex and specify a name without acquiring a lock on the mutex - [boolean]$showBlockedAppDialogMutexLocked = $false - [string]$showBlockedAppDialogMutexName = 'Global\PSADT_ShowBlockedAppDialog_Message' - [Threading.Mutex]$showBlockedAppDialogMutex = New-Object -TypeName 'System.Threading.Mutex' -ArgumentList ($false, $showBlockedAppDialogMutexName) - # Attempt to acquire an exclusive lock on the mutex, attempt will fail after 1 millisecond if unable to acquire exclusive lock - If ((Test-IsMutexAvailable -MutexName $showBlockedAppDialogMutexName -MutexWaitTimeInMilliseconds 1) -and ($showBlockedAppDialogMutex.WaitOne(1))) { - [boolean]$showBlockedAppDialogMutexLocked = $true - Show-InstallationPrompt -Title $installTitle -Message $configBlockExecutionMessage -Icon 'Warning' -ButtonRightText 'OK' - Exit 0 - } - Else { - # If attempt to acquire an exclusive lock on the mutex failed, then exit script as another blocked app dialog window is already open - Write-Log -Message "Unable to acquire an exclusive lock on mutex [$showBlockedAppDialogMutexName] because another blocked application dialog window is already open. Exiting script..." -Severity 2 -Source $appDeployToolkitName - Exit 0 - } - } - Catch { - Write-Log -Message "There was an error in displaying the Installation Prompt. `n$(Resolve-Error)" -Severity 3 -Source $appDeployToolkitName - Exit 60005 - } - Finally { - If ($showBlockedAppDialogMutexLocked) { $null = $showBlockedAppDialogMutex.ReleaseMutex() } - If ($showBlockedAppDialogMutex) { $showBlockedAppDialogMutex.Close() } - } + Try { + . $DisableScriptLogging + Write-Log -Message "[$appDeployMainScriptFriendlyName] called with switch [-ShowBlockedAppDialog]." -Source $appDeployToolkitName + # Create a mutex and specify a name without acquiring a lock on the mutex + [boolean]$showBlockedAppDialogMutexLocked = $false + [string]$showBlockedAppDialogMutexName = 'Global\PSADT_ShowBlockedAppDialog_Message' + [Threading.Mutex]$showBlockedAppDialogMutex = New-Object -TypeName 'System.Threading.Mutex' -ArgumentList ($false, $showBlockedAppDialogMutexName) + # Attempt to acquire an exclusive lock on the mutex, attempt will fail after 1 millisecond if unable to acquire exclusive lock + If ((Test-IsMutexAvailable -MutexName $showBlockedAppDialogMutexName -MutexWaitTimeInMilliseconds 1) -and ($showBlockedAppDialogMutex.WaitOne(1))) { + [boolean]$showBlockedAppDialogMutexLocked = $true + Show-InstallationPrompt -Title $installTitle -Message $configBlockExecutionMessage -Icon 'Warning' -ButtonRightText 'OK' + Exit 0 + } + Else { + # If attempt to acquire an exclusive lock on the mutex failed, then exit script as another blocked app dialog window is already open + Write-Log -Message "Unable to acquire an exclusive lock on mutex [$showBlockedAppDialogMutexName] because another blocked application dialog window is already open. Exiting script..." -Severity 2 -Source $appDeployToolkitName + Exit 0 + } + } + Catch { + Write-Log -Message "There was an error in displaying the Installation Prompt. `n$(Resolve-Error)" -Severity 3 -Source $appDeployToolkitName + Exit 60005 + } + Finally { + If ($showBlockedAppDialogMutexLocked) { $null = $showBlockedAppDialogMutex.ReleaseMutex() } + If ($showBlockedAppDialogMutex) { $showBlockedAppDialogMutex.Close() } + } } ## Log details for all currently logged in users Write-Log -Message "Display session information for all logged on users: `n$($LoggedOnUserSessions | Format-List | Out-String)" -Source $appDeployToolkitName If ($usersLoggedOn) { - Write-Log -Message "The following users are logged on to the system: [$($usersLoggedOn -join ', ')]." -Source $appDeployToolkitName - - # Check if the current process is running in the context of one of the logged in users - If ($CurrentLoggedOnUserSession) { - Write-Log -Message "Current process is running with user account [$ProcessNTAccount] under logged in user session for [$($CurrentLoggedOnUserSession.NTAccount)]." -Source $appDeployToolkitName - } - Else { - Write-Log -Message "Current process is running under a system account [$ProcessNTAccount]." -Source $appDeployToolkitName - } - - # Display account and session details for the account running as the console user (user with control of the physical monitor, keyboard, and mouse) - If ($CurrentConsoleUserSession) { - Write-Log -Message "The following user is the console user [$($CurrentConsoleUserSession.NTAccount)] (user with control of physical monitor, keyboard, and mouse)." -Source $appDeployToolkitName - } - Else { - Write-Log -Message 'There is no console user logged in (user with control of physical monitor, keyboard, and mouse).' -Source $appDeployToolkitName - } - - # Display the account that will be used to execute commands in the user session when toolkit is running under the SYSTEM account - If ($RunAsActiveUser) { - Write-Log -Message "The active logged on user is [$($RunAsActiveUser.NTAccount)]." -Source $appDeployToolkitName - } + Write-Log -Message "The following users are logged on to the system: [$($usersLoggedOn -join ', ')]." -Source $appDeployToolkitName + + # Check if the current process is running in the context of one of the logged in users + If ($CurrentLoggedOnUserSession) { + Write-Log -Message "Current process is running with user account [$ProcessNTAccount] under logged in user session for [$($CurrentLoggedOnUserSession.NTAccount)]." -Source $appDeployToolkitName + } + Else { + Write-Log -Message "Current process is running under a system account [$ProcessNTAccount]." -Source $appDeployToolkitName + } + + # Display account and session details for the account running as the console user (user with control of the physical monitor, keyboard, and mouse) + If ($CurrentConsoleUserSession) { + Write-Log -Message "The following user is the console user [$($CurrentConsoleUserSession.NTAccount)] (user with control of physical monitor, keyboard, and mouse)." -Source $appDeployToolkitName + } + Else { + Write-Log -Message 'There is no console user logged in (user with control of physical monitor, keyboard, and mouse).' -Source $appDeployToolkitName + } + + # Display the account that will be used to execute commands in the user session when toolkit is running under the SYSTEM account + If ($RunAsActiveUser) { + Write-Log -Message "The active logged on user is [$($RunAsActiveUser.NTAccount)]." -Source $appDeployToolkitName + } } Else { - Write-Log -Message 'No users are logged on to the system.' -Source $appDeployToolkitName + Write-Log -Message 'No users are logged on to the system.' -Source $appDeployToolkitName } ## Log which language's UI messages are loaded from the config XML file If ($HKUPrimaryLanguageShort) { - Write-Log -Message "The active logged on user [$($RunAsActiveUser.NTAccount)] has a primary UI language of [$HKUPrimaryLanguageShort]." -Source $appDeployToolkitName + Write-Log -Message "The active logged on user [$($RunAsActiveUser.NTAccount)] has a primary UI language of [$HKUPrimaryLanguageShort]." -Source $appDeployToolkitName } Else { - Write-Log -Message "The current system account [$ProcessNTAccount] has a primary UI language of [$currentLanguage]." -Source $appDeployToolkitName + Write-Log -Message "The current system account [$ProcessNTAccount] has a primary UI language of [$currentLanguage]." -Source $appDeployToolkitName } If ($configInstallationUILanguageOverride) { Write-Log -Message "The config XML file was configured to override the detected primary UI language with the following UI language: [$configInstallationUILanguageOverride]." -Source $appDeployToolkitName } Write-Log -Message "The following UI messages were imported from the config XML file: [$xmlUIMessageLanguage]." -Source $appDeployToolkitName ## Log system DPI scale factor of active logged on user If ($UserDisplayScaleFactor) { - Write-Log -Message "The active logged on user [$($RunAsActiveUser.NTAccount)] has a DPI scale factor of [$dpiScale] with DPI pixels [$dpiPixels]." -Source $appDeployToolkitName + Write-Log -Message "The active logged on user [$($RunAsActiveUser.NTAccount)] has a DPI scale factor of [$dpiScale] with DPI pixels [$dpiPixels]." -Source $appDeployToolkitName } Else { - Write-Log -Message "The system has a DPI scale factor of [$dpiScale] with DPI pixels [$dpiPixels]." -Source $appDeployToolkitName + Write-Log -Message "The system has a DPI scale factor of [$dpiScale] with DPI pixels [$dpiPixels]." -Source $appDeployToolkitName } ## Check if script is running from a SCCM Task Sequence Try { - [__comobject]$SMSTSEnvironment = New-Object -ComObject 'Microsoft.SMS.TSEnvironment' -ErrorAction 'Stop' - Write-Log -Message 'Successfully loaded COM Object [Microsoft.SMS.TSEnvironment]. Therefore, script is currently running from a SCCM Task Sequence.' -Source $appDeployToolkitName - $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SMSTSEnvironment) - $runningTaskSequence = $true + [__comobject]$SMSTSEnvironment = New-Object -ComObject 'Microsoft.SMS.TSEnvironment' -ErrorAction 'Stop' + Write-Log -Message 'Successfully loaded COM Object [Microsoft.SMS.TSEnvironment]. Therefore, script is currently running from a SCCM Task Sequence.' -Source $appDeployToolkitName + $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($SMSTSEnvironment) + $runningTaskSequence = $true } Catch { - Write-Log -Message 'Unable to load COM Object [Microsoft.SMS.TSEnvironment]. Therefore, script is not currently running from a SCCM Task Sequence.' -Source $appDeployToolkitName - $runningTaskSequence = $false + Write-Log -Message 'Unable to load COM Object [Microsoft.SMS.TSEnvironment]. Therefore, script is not currently running from a SCCM Task Sequence.' -Source $appDeployToolkitName + $runningTaskSequence = $false } @@ -11095,64 +11521,64 @@ Catch { ## The task scheduler service and the services it is dependent on can/should only be started/stopped/modified when running in the SYSTEM context. [boolean]$IsTaskSchedulerHealthy = $true If ($IsLocalSystemAccount) { - # Check the health of the 'COM+ Event System' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventSystem' - # Check the health of the 'Remote Procedure Call (RPC)' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'RpcSs' - # Check the health of the 'Windows Event Log' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventLog' - # Check the health of the 'Task Scheduler' service - [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'Schedule' - - Write-Log -Message "The task scheduler service is in a healthy state: $IsTaskSchedulerHealthy." -Source $appDeployToolkitName + # Check the health of the 'COM+ Event System' service + [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventSystem' + # Check the health of the 'Remote Procedure Call (RPC)' service + [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'RpcSs' + # Check the health of the 'Windows Event Log' service + [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'EventLog' + # Check the health of the 'Task Scheduler' service + [boolean]$IsTaskSchedulerHealthy = & $TestServiceHealth -ServiceName 'Schedule' + + Write-Log -Message "The task scheduler service is in a healthy state: $IsTaskSchedulerHealthy." -Source $appDeployToolkitName } Else { - Write-Log -Message "Skipping attempt to check for and make the task scheduler services healthy because the App Deployment Toolkit is not running under the [$LocalSystemNTAccount] account." -Source $appDeployToolkitName + Write-Log -Message "Skipping attempt to check for and make the task scheduler services healthy because the App Deployment Toolkit is not running under the [$LocalSystemNTAccount] account." -Source $appDeployToolkitName } ## If script is running in session zero If ($SessionZero) { - ## If the script was launched with deployment mode set to NonInteractive, then continue - If ($deployMode -eq 'NonInteractive') { - Write-Log -Message "Session 0 detected but deployment mode was manually set to [$deployMode]." -Source $appDeployToolkitName - } - Else { - ## If the process is not able to display a UI, enable NonInteractive mode - If (-not $IsProcessUserInteractive) { - $deployMode = 'NonInteractive' - Write-Log -Message "Session 0 detected, process not running in user interactive mode; deployment mode set to [$deployMode]." -Source $appDeployToolkitName - } - Else { - If (-not $usersLoggedOn) { - $deployMode = 'NonInteractive' - Write-Log -Message "Session 0 detected, process running in user interactive mode, no users logged in; deployment mode set to [$deployMode]." -Source $appDeployToolkitName - } - Else { - Write-Log -Message 'Session 0 detected, process running in user interactive mode, user(s) logged in.' -Source $appDeployToolkitName - } - } - } + ## If the script was launched with deployment mode set to NonInteractive, then continue + If ($deployMode -eq 'NonInteractive') { + Write-Log -Message "Session 0 detected but deployment mode was manually set to [$deployMode]." -Source $appDeployToolkitName + } + Else { + ## If the process is not able to display a UI, enable NonInteractive mode + If (-not $IsProcessUserInteractive) { + $deployMode = 'NonInteractive' + Write-Log -Message "Session 0 detected, process not running in user interactive mode; deployment mode set to [$deployMode]." -Source $appDeployToolkitName + } + Else { + If (-not $usersLoggedOn) { + $deployMode = 'NonInteractive' + Write-Log -Message "Session 0 detected, process running in user interactive mode, no users logged in; deployment mode set to [$deployMode]." -Source $appDeployToolkitName + } + Else { + Write-Log -Message 'Session 0 detected, process running in user interactive mode, user(s) logged in.' -Source $appDeployToolkitName + } + } + } } Else { - Write-Log -Message 'Session 0 not detected.' -Source $appDeployToolkitName + Write-Log -Message 'Session 0 not detected.' -Source $appDeployToolkitName } ## Set Deploy Mode switches If ($deployMode) { - Write-Log -Message "Installation is running in [$deployMode] mode." -Source $appDeployToolkitName + Write-Log -Message "Installation is running in [$deployMode] mode." -Source $appDeployToolkitName } Switch ($deployMode) { - 'Silent' { $deployModeSilent = $true } - 'NonInteractive' { $deployModeNonInteractive = $true; $deployModeSilent = $true } - Default { $deployModeNonInteractive = $false; $deployModeSilent = $false } + 'Silent' { $deployModeSilent = $true } + 'NonInteractive' { $deployModeNonInteractive = $true; $deployModeSilent = $true } + Default { $deployModeNonInteractive = $false; $deployModeSilent = $false } } ## Check deployment type (install/uninstall) Switch ($deploymentType) { - 'Install' { $deploymentTypeName = $configDeploymentTypeInstall } - 'Uninstall' { $deploymentTypeName = $configDeploymentTypeUnInstall } - 'Repair' { $deploymentTypeName = $configDeploymentTypeRepair } - Default { $deploymentTypeName = $configDeploymentTypeInstall } + 'Install' { $deploymentTypeName = $configDeploymentTypeInstall } + 'Uninstall' { $deploymentTypeName = $configDeploymentTypeUnInstall } + 'Repair' { $deploymentTypeName = $configDeploymentTypeRepair } + Default { $deploymentTypeName = $configDeploymentTypeInstall } } If ($deploymentTypeName) { Write-Log -Message "Deployment type is [$deploymentTypeName]." -Source $appDeployToolkitName } @@ -11160,13 +11586,13 @@ If ($useDefaultMsi) { Write-Log -Message "Discovered Zero-Config MSI installatio ## Check current permissions and exit if not running with Administrator rights If ($configToolkitRequireAdmin) { - # Check if the current process is running with elevated administrator permissions - If ((-not $IsAdmin) -and (-not $ShowBlockedAppDialog)) { - [string]$AdminPermissionErr = "[$appDeployToolkitName] has an XML config file option [Toolkit_RequireAdmin] set to [True] so as to require Administrator rights for the toolkit to function. Please re-run the deployment script as an Administrator or change the option in the XML config file to not require Administrator rights." - Write-Log -Message $AdminPermissionErr -Severity 3 -Source $appDeployToolkitName - Show-DialogBox -Text $AdminPermissionErr -Icon 'Stop' - Throw $AdminPermissionErr - } + # Check if the current process is running with elevated administrator permissions + If ((-not $IsAdmin) -and (-not $ShowBlockedAppDialog)) { + [string]$AdminPermissionErr = "[$appDeployToolkitName] has an XML config file option [Toolkit_RequireAdmin] set to [True] so as to require Administrator rights for the toolkit to function. Please re-run the deployment script as an Administrator or change the option in the XML config file to not require Administrator rights." + Write-Log -Message $AdminPermissionErr -Severity 3 -Source $appDeployToolkitName + Show-DialogBox -Text $AdminPermissionErr -Icon 'Stop' + Throw $AdminPermissionErr + } } ## If terminal server mode was specified, change the installation mode to support it diff --git a/CHANGELOG b/CHANGELOG index a0cf709..de2c831 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1 +1,4 @@ # CHANGELOG + +2021-01-17 - Update to new repository. + Update scripts to PowerShell Application Deployment Toolkit 3.8.3 from 3.8.2. diff --git a/Deploy-Application.ps1 b/Deploy-Application.ps1 new file mode 100644 index 0000000..f223a16 --- /dev/null +++ b/Deploy-Application.ps1 @@ -0,0 +1,237 @@ +<# +.SYNOPSIS + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + 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 . +.DESCRIPTION + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. +.PARAMETER DeploymentType + The type of deployment to perform. Default is: Install. +.PARAMETER DeployMode + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. +.PARAMETER AllowRebootPassThru + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. +.PARAMETER TerminalServerMode + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. +.PARAMETER DisableLogging + Disables logging to file for the script. Default is: $false. +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -AllowRebootPassThru; Exit $LastExitCode }" +.EXAMPLE + powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeploymentType 'Uninstall'; Exit $LastExitCode }" +.EXAMPLE + Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" +.NOTES + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 +.LINK + http://psappdeploytoolkit.com +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory=$false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory=$false)] + [switch]$DisableLogging = $false +) + +Try { + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = '365 Apps for Enterprise' + [string]$appVersion = '' # No need to display this! + [string]$appArch = 'x64' + [string]$appLang = 'EN' + [string]$appRevision = '01' + [string]$appScriptVersion = '1.2.0' + [string]$appScriptDate = '2021-01-17' + [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.3' + [string]$deployAppScriptDate = '30/09/2020' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' + + ## Show Welcome Message, close required applications, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' + + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } + + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Install.xml`"" + + ## Force update group policy + Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose + + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' + + ## + + ## Display a message at the end of the install + If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' + + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' + + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + + # + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Uninstall.xml`"" + + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' + + ## + + } + ElseIf ($deploymentType -ieq 'Repair') + { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are repairing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' + + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # + + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + + ## + + } + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== + + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode +} +Catch { + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode +} diff --git a/Deploy-M365Apps.ps1 b/Deploy-M365Apps.ps1 index 7a14935..8009c0e 100644 --- a/Deploy-M365Apps.ps1 +++ b/Deploy-M365Apps.ps1 @@ -1,7 +1,4 @@ <# - https://github.com/cameronkollwitz/DeployM365Apps/ -#> -<# .SYNOPSIS This script performs the installation or uninstallation of an application(s). # LICENSE # @@ -19,7 +16,7 @@ .PARAMETER DeployMode Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. .PARAMETER AllowRebootPassThru - Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. ConfigMgr) if detected from an installation. If 3010 is passed back to ConfigMgr, a reboot prompt will be triggered. + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. .PARAMETER TerminalServerMode Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. .PARAMETER DisableLogging @@ -42,17 +39,17 @@ #> [CmdletBinding()] Param ( - [Parameter(Mandatory = $false)] - [ValidateSet('Install', 'Uninstall', 'Repair')] + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] [string]$DeploymentType = 'Install', - [Parameter(Mandatory = $false)] - [ValidateSet('Interactive', 'Silent', 'NonInteractive')] + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] [string]$DeployMode = 'Interactive', - [Parameter(Mandatory = $false)] + [Parameter(Mandatory=$false)] [switch]$AllowRebootPassThru = $false, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory=$false)] [switch]$TerminalServerMode = $false, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory=$false)] [switch]$DisableLogging = $false ) @@ -64,14 +61,14 @@ Try { ##* VARIABLE DECLARATION ##*=============================================== ## Variables: Application - [string]$appVendor = 'Microsoft' + [string]$appVendor = 'Microsoft Corporation' [string]$appName = '365 Apps for Enterprise' [string]$appVersion = '' # No need to display this! [string]$appArch = 'x64' [string]$appLang = 'EN' [string]$appRevision = '01' - [string]$appScriptVersion = '1.1.0' - [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptVersion = '1.2.0' + [string]$appScriptDate = '2021-01-17' [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' ##*=============================================== ## Variables: Install Titles (Only set here to override defaults set by the toolkit) @@ -86,8 +83,8 @@ Try { ## Variables: Script [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' + [version]$deployAppScriptVersion = [version]'3.8.3' + [string]$deployAppScriptDate = '30/09/2020' [hashtable]$deployAppScriptParameters = $psBoundParameters ## Variables: Environment @@ -99,10 +96,11 @@ Try { [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } - } Catch { - If ($mainExitCode -eq 0) { [int32]$mainExitCode = 60008 } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' - ## Exit the script, returning the exit code to ConfigMgr + ## Exit the script, returning the exit code to SCCM If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } } @@ -122,10 +120,11 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false ## + ##*=============================================== ##* INSTALLATION ##*=============================================== @@ -133,7 +132,7 @@ Try { ## Handle Zero-Config MSI Installations If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } } @@ -149,12 +148,12 @@ Try { [string]$installPhase = 'Post-Installation' ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 ## Display a message at the end of the install - #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } - } ElseIf ($deploymentType -ieq 'Uninstall') { + If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { ##*=============================================== ##* PRE-UNINSTALLATION ##*=============================================== @@ -164,7 +163,7 @@ Try { Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false ## @@ -175,7 +174,7 @@ Try { ## Handle Zero-Config MSI Uninstallations If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } Execute-MSI @ExecuteDefaultMSISplat } @@ -189,14 +188,16 @@ Try { ## - } ElseIf ($deploymentType -ieq 'Repair') { + } + ElseIf ($deploymentType -ieq 'Repair') + { ##*=============================================== ##* PRE-REPAIR ##*=============================================== [string]$installPhase = 'Pre-Repair' ## Show Progress Message (with the default message) - Show-InstallationProgress + Show-InstallationProgress -StatusMessage "We are repairing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false ## @@ -207,7 +208,7 @@ Try { ## Handle Zero-Config MSI Repairs If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } Execute-MSI @ExecuteDefaultMSISplat } # @@ -219,14 +220,15 @@ Try { ## - } + } ##*=============================================== ##* END SCRIPT BODY ##*=============================================== ## Call the Exit-Script function to perform final cleanup operations Exit-Script -ExitCode $mainExitCode -} Catch { +} +Catch { [int32]$mainExitCode = 60001 [string]$mainErrorMessage = "$(Resolve-Error)" Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName diff --git a/Deploy-Project.ps1 b/Deploy-Project.ps1 index 73353ff..a6b2c0d 100644 --- a/Deploy-Project.ps1 +++ b/Deploy-Project.ps1 @@ -1,29 +1,26 @@ <# - https://github.com/cameronkollwitz/DeployM365Apps/ -#> -<# .SYNOPSIS - This script performs the installation or uninstallation of an application(s). - # LICENSE # - PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. - Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. - 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 . + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + 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 . .DESCRIPTION - The script is provided as a template to perform an install or uninstall of an application(s). - The script either performs an "Install" deployment type or an "Uninstall" deployment type. - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. - The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. .PARAMETER DeploymentType - The type of deployment to perform. Default is: Install. + The type of deployment to perform. Default is: Install. .PARAMETER DeployMode - Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. .PARAMETER AllowRebootPassThru - Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. .PARAMETER TerminalServerMode - Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. .PARAMETER DisableLogging - Disables logging to file for the script. Default is: $false. + Disables logging to file for the script. Default is: $false. .EXAMPLE powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE @@ -33,213 +30,208 @@ .EXAMPLE Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES - Toolkit Exit Code Ranges: - 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 - 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 - 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> [CmdletBinding()] Param ( - [Parameter(Mandatory=$false)] - [ValidateSet('Install','Uninstall','Repair')] - [string]$DeploymentType = 'Install', - [Parameter(Mandatory=$false)] - [ValidateSet('Interactive','Silent','NonInteractive')] - [string]$DeployMode = 'Interactive', - [Parameter(Mandatory=$false)] - [switch]$AllowRebootPassThru = $false, - [Parameter(Mandatory=$false)] - [switch]$TerminalServerMode = $false, - [Parameter(Mandatory=$false)] - [switch]$DisableLogging = $false + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory=$false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory=$false)] + [switch]$DisableLogging = $false ) Try { - ## Set the script execution policy for this process - Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} - - ##*=============================================== - ##* VARIABLE DECLARATION - ##*=============================================== - ## Variables: Application - [string]$appVendor = 'Microsoft' + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = 'Microsoft Corporation' [string]$appName = 'Project' [string]$appVersion = '' # No need to display this! [string]$appArch = 'x64' [string]$appLang = 'EN' [string]$appRevision = '01' - [string]$appScriptVersion = '1.1.0' - [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptVersion = '1.2.0' + [string]$appScriptDate = '2021-01-17' [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' - ##*=============================================== - ## Variables: Install Titles (Only set here to override defaults set by the toolkit) - [string]$installName = '' - [string]$installTitle = '' - - ##* Do not modify section below - #region DoNotModify - - ## Variables: Exit Code - [int32]$mainExitCode = 0 - - ## Variables: Script - [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' - [hashtable]$deployAppScriptParameters = $psBoundParameters - - ## Variables: Environment - If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } - [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent - - ## Dot source the required App Deploy Toolkit Functions - Try { - [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" - If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } - If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } - } - Catch { - If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } - Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' - ## Exit the script, returning the exit code to SCCM - If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } - } - - #endregion - ##* Do not modify section above - ##*=============================================== - ##* END VARIABLE DECLARATION - ##*=============================================== - - If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { - ##*=============================================== - ##* PRE-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Installation' + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.3' + [string]$deployAppScriptDate = '30/09/2020' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' ## Show Welcome Message, close required applications, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ## + ## - ##*=============================================== - ##* INSTALLATION - ##*=============================================== - [string]$installPhase = 'Installation' + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' - ## Handle Zero-Config MSI Installations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } - } + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } - ## - Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Install.xml`"" + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Install.xml`"" - ## Force update group policy + ## Force update group policy Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose - ##*=============================================== - ##* POST-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Installation' - - ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 - - ## Display a message at the end of the install - #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } - } - ElseIf ($deploymentType -ieq 'Uninstall') - { - ##*=============================================== - ##* PRE-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Uninstallation' - - ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## - ## + ## Display a message at the end of the install + If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - ##*=============================================== - ##* UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Uninstallation' + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ## Handle Zero-Config MSI Uninstallations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } + ## - # - Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' - ##*=============================================== - ##* POST-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Uninstallation' + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } - ## + # + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Project_Uninstall.xml`"" + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' - } - ElseIf ($deploymentType -ieq 'Repair') - { - ##*=============================================== - ##* PRE-REPAIR - ##*=============================================== - [string]$installPhase = 'Pre-Repair' + ## - ## Show Progress Message (with the default message) - Show-InstallationProgress + } + ElseIf ($deploymentType -ieq 'Repair') + { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' - ## + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are repairing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ##*=============================================== - ##* REPAIR - ##*=============================================== - [string]$installPhase = 'Repair' + ## - ## Handle Zero-Config MSI Repairs - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } - # + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' - ##*=============================================== - ##* POST-REPAIR - ##*=============================================== - [string]$installPhase = 'Post-Repair' + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # - ## + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + ## } - ##*=============================================== - ##* END SCRIPT BODY - ##*=============================================== + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== - ## Call the Exit-Script function to perform final cleanup operations - Exit-Script -ExitCode $mainExitCode + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode } Catch { - [int32]$mainExitCode = 60001 - [string]$mainErrorMessage = "$(Resolve-Error)" - Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName - Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' - Exit-Script -ExitCode $mainExitCode + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode } diff --git a/Deploy-Visio.ps1 b/Deploy-Visio.ps1 index 5bd4015..d60541a 100644 --- a/Deploy-Visio.ps1 +++ b/Deploy-Visio.ps1 @@ -1,29 +1,26 @@ <# - https://github.com/cameronkollwitz/DeployM365Apps/ -#> -<# .SYNOPSIS - This script performs the installation or uninstallation of an application(s). - # LICENSE # - PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. - Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. - 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 . + This script performs the installation or uninstallation of an application(s). + # LICENSE # + PowerShell App Deployment Toolkit - Provides a set of functions to perform common application deployment tasks on Windows. + Copyright (C) 2017 - Sean Lillis, Dan Cunningham, Muhammad Mashwani, Aman Motazedian. + 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 . .DESCRIPTION - The script is provided as a template to perform an install or uninstall of an application(s). - The script either performs an "Install" deployment type or an "Uninstall" deployment type. - The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. - The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. + The script is provided as a template to perform an install or uninstall of an application(s). + The script either performs an "Install" deployment type or an "Uninstall" deployment type. + The install deployment type is broken down into 3 main sections/phases: Pre-Install, Install, and Post-Install. + The script dot-sources the AppDeployToolkitMain.ps1 script which contains the logic and functions required to install or uninstall an application. .PARAMETER DeploymentType - The type of deployment to perform. Default is: Install. + The type of deployment to perform. Default is: Install. .PARAMETER DeployMode - Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. + Specifies whether the installation should be run in Interactive, Silent, or NonInteractive mode. Default is: Interactive. Options: Interactive = Shows dialogs, Silent = No dialogs, NonInteractive = Very silent, i.e. no blocking apps. NonInteractive mode is automatically set if it is detected that the process is not user interactive. .PARAMETER AllowRebootPassThru - Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. + Allows the 3010 return code (requires restart) to be passed back to the parent process (e.g. SCCM) if detected from an installation. If 3010 is passed back to SCCM, a reboot prompt will be triggered. .PARAMETER TerminalServerMode - Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. + Changes to "user install mode" and back to "user execute mode" for installing/uninstalling applications for Remote Destkop Session Hosts/Citrix servers. .PARAMETER DisableLogging - Disables logging to file for the script. Default is: $false. + Disables logging to file for the script. Default is: $false. .EXAMPLE powershell.exe -Command "& { & '.\Deploy-Application.ps1' -DeployMode 'Silent'; Exit $LastExitCode }" .EXAMPLE @@ -33,211 +30,208 @@ .EXAMPLE Deploy-Application.exe -DeploymentType "Install" -DeployMode "Silent" .NOTES - Toolkit Exit Code Ranges: - 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 - 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 - 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 + Toolkit Exit Code Ranges: + 60000 - 68999: Reserved for built-in exit codes in Deploy-Application.ps1, Deploy-Application.exe, and AppDeployToolkitMain.ps1 + 69000 - 69999: Recommended for user customized exit codes in Deploy-Application.ps1 + 70000 - 79999: Recommended for user customized exit codes in AppDeployToolkitExtensions.ps1 .LINK - http://psappdeploytoolkit.com + http://psappdeploytoolkit.com #> [CmdletBinding()] Param ( - [Parameter(Mandatory=$false)] - [ValidateSet('Install','Uninstall','Repair')] - [string]$DeploymentType = 'Install', - [Parameter(Mandatory=$false)] - [ValidateSet('Interactive','Silent','NonInteractive')] - [string]$DeployMode = 'Interactive', - [Parameter(Mandatory=$false)] - [switch]$AllowRebootPassThru = $false, - [Parameter(Mandatory=$false)] - [switch]$TerminalServerMode = $false, - [Parameter(Mandatory=$false)] - [switch]$DisableLogging = $false + [Parameter(Mandatory=$false)] + [ValidateSet('Install','Uninstall','Repair')] + [string]$DeploymentType = 'Install', + [Parameter(Mandatory=$false)] + [ValidateSet('Interactive','Silent','NonInteractive')] + [string]$DeployMode = 'Interactive', + [Parameter(Mandatory=$false)] + [switch]$AllowRebootPassThru = $false, + [Parameter(Mandatory=$false)] + [switch]$TerminalServerMode = $false, + [Parameter(Mandatory=$false)] + [switch]$DisableLogging = $false ) Try { - ## Set the script execution policy for this process - Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} - - ##*=============================================== - ##* VARIABLE DECLARATION - ##*=============================================== - ## Variables: Application - [string]$appVendor = 'Microsoft' - [string]$appName = 'Visio' + ## Set the script execution policy for this process + Try { Set-ExecutionPolicy -ExecutionPolicy 'ByPass' -Scope 'Process' -Force -ErrorAction 'Stop' } Catch {} + + ##*=============================================== + ##* VARIABLE DECLARATION + ##*=============================================== + ## Variables: Application + [string]$appVendor = 'Microsoft Corporation' + [string]$appName = 'Visio' [string]$appVersion = '' # No need to display this! [string]$appArch = 'x64' [string]$appLang = 'EN' [string]$appRevision = '01' - [string]$appScriptVersion = '1.1.0' - [string]$appScriptDate = '2020/10/08' # YYYY/MM/DD + [string]$appScriptVersion = '1.2.0' + [string]$appScriptDate = '2021-01-17' [string]$appScriptAuthor = 'Cameron Kollwitz (Original by Sandy Zeng)' - ##*=============================================== - ## Variables: Install Titles (Only set here to override defaults set by the toolkit) - [string]$installName = '' - [string]$installTitle = '' - - ##* Do not modify section below - #region DoNotModify - - ## Variables: Exit Code - [int32]$mainExitCode = 0 - - ## Variables: Script - [string]$deployAppScriptFriendlyName = 'Deploy Application' - [version]$deployAppScriptVersion = [version]'3.8.2' - [string]$deployAppScriptDate = '08/05/2020' - [hashtable]$deployAppScriptParameters = $psBoundParameters - - ## Variables: Environment - If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } - [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent - - ## Dot source the required App Deploy Toolkit Functions - Try { - [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" - If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } - If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } - } - Catch { - If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } - Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' - ## Exit the script, returning the exit code to SCCM - If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } - } - - #endregion - ##* Do not modify section above - ##*=============================================== - ##* END VARIABLE DECLARATION - ##*=============================================== - - If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { - ##*=============================================== - ##* PRE-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Installation' - - ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution - - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are insalling $installTitle. Please wait!" -WindowLocation 'BottomRight' -TopMost $false - - ## - - - ##*=============================================== - ##* INSTALLATION - ##*=============================================== - [string]$installPhase = 'Installation' - - ## Handle Zero-Config MSI Installations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } - } - - ## - Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Install.xml`"" - - ## Force update group policy + ##*=============================================== + ## Variables: Install Titles (Only set here to override defaults set by the toolkit) + [string]$installName = '' + [string]$installTitle = '' + + ##* Do not modify section below + #region DoNotModify + + ## Variables: Exit Code + [int32]$mainExitCode = 0 + + ## Variables: Script + [string]$deployAppScriptFriendlyName = 'Deploy Application' + [version]$deployAppScriptVersion = [version]'3.8.3' + [string]$deployAppScriptDate = '30/09/2020' + [hashtable]$deployAppScriptParameters = $psBoundParameters + + ## Variables: Environment + If (Test-Path -LiteralPath 'variable:HostInvocation') { $InvocationInfo = $HostInvocation } Else { $InvocationInfo = $MyInvocation } + [string]$scriptDirectory = Split-Path -Path $InvocationInfo.MyCommand.Definition -Parent + + ## Dot source the required App Deploy Toolkit Functions + Try { + [string]$moduleAppDeployToolkitMain = "$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1" + If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType 'Leaf')) { Throw "Module does not exist at the specified location [$moduleAppDeployToolkitMain]." } + If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain } + } + Catch { + If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 } + Write-Error -Message "Module [$moduleAppDeployToolkitMain] failed to load: `n$($_.Exception.Message)`n `n$($_.InvocationInfo.PositionMessage)" -ErrorAction 'Continue' + ## Exit the script, returning the exit code to SCCM + If (Test-Path -LiteralPath 'variable:HostInvocation') { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode } + } + + #endregion + ##* Do not modify section above + ##*=============================================== + ##* END VARIABLE DECLARATION + ##*=============================================== + + If ($deploymentType -ine 'Uninstall' -and $deploymentType -ine 'Repair') { + ##*=============================================== + ##* PRE-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Installation' + + ## Show Welcome Message, close required applications, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are installing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + + ## + + + ##*=============================================== + ##* INSTALLATION + ##*=============================================== + [string]$installPhase = 'Installation' + + ## Handle Zero-Config MSI Installations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) { $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ } } + } + + ## + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Install.xml`"" + + ## Force update group policy Invoke-GPUpdate -RandomDelayInMinutes 0 -Force -Verbose - ##*=============================================== - ##* POST-INSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Installation' - - ## - ## Popup notification for restart - Show-InstallationRestartPrompt -Countdownseconds 4500 -CountdownNoHideSeconds 600 - - ## Display a message at the end of the install - #If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } - } - ElseIf ($deploymentType -ieq 'Uninstall') - { - ##*=============================================== - ##* PRE-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Pre-Uninstallation' + ##*=============================================== + ##* POST-INSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Installation' - ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing - Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + ## - ## Show Progress Message (with the default message) - Show-InstallationProgress -StatusMessage "We are remvoing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false + ## Display a message at the end of the install + If (-not $useDefaultMsi) { Show-InstallationPrompt -Message 'You can customize text to appear at the end of an install or remove it completely for unattended installations.' -ButtonRightText 'OK' -Icon Information -NoWait } + } + ElseIf ($deploymentType -ieq 'Uninstall') + { + ##*=============================================== + ##* PRE-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Pre-Uninstallation' - ## + ## Show Welcome Message, close required applications with a 60 second countdown before automatically closing + Show-InstallationWelcome -CloseApps "MSACCESS,EXCEL,INFOPATH,ONENOTEM,GROOVE,ONENOTE,OUTLOOK,POWERPNT,WINPROJ,MSPUB,SPDESIGN,lync,VISIO,WINWORD,Teams,Onedrive" -AllowDeferCloseApps -AllowDefer -DeferDays "1" -CloseAppsCountdown "5400" -PersistPrompt -BlockExecution + + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are removing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ##*=============================================== - ##* UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Uninstallation' + ## - ## Handle Zero-Config MSI Uninstallations - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } + ##*=============================================== + ##* UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Uninstallation' - # - Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" + ## Handle Zero-Config MSI Uninstallations + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Uninstall'; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } - ##*=============================================== - ##* POST-UNINSTALLATION - ##*=============================================== - [string]$installPhase = 'Post-Uninstallation' + # + Execute-Process -Path "$dirFiles\setup.exe" -WaitForMsiExec "/configure `"$dirSupportFiles\Visio_Uninstall.xml`"" - ## + ##*=============================================== + ##* POST-UNINSTALLATION + ##*=============================================== + [string]$installPhase = 'Post-Uninstallation' - } - ElseIf ($deploymentType -ieq 'Repair') - { - ##*=============================================== - ##* PRE-REPAIR - ##*=============================================== - [string]$installPhase = 'Pre-Repair' + ## - ## Show Progress Message (with the default message) - Show-InstallationProgress + } + ElseIf ($deploymentType -ieq 'Repair') + { + ##*=============================================== + ##* PRE-REPAIR + ##*=============================================== + [string]$installPhase = 'Pre-Repair' - ## + ## Show Progress Message (with the default message) + Show-InstallationProgress -StatusMessage "We are repairing $installTitle. Please wait." -WindowLocation 'BottomRight' -TopMost $false - ##*=============================================== - ##* REPAIR - ##*=============================================== - [string]$installPhase = 'Repair' + ## - ## Handle Zero-Config MSI Repairs - If ($useDefaultMsi) { - [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } - Execute-MSI @ExecuteDefaultMSISplat - } - # + ##*=============================================== + ##* REPAIR + ##*=============================================== + [string]$installPhase = 'Repair' - ##*=============================================== - ##* POST-REPAIR - ##*=============================================== - [string]$installPhase = 'Post-Repair' + ## Handle Zero-Config MSI Repairs + If ($useDefaultMsi) { + [hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Repair'; Path = $defaultMsiFile; }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile) } + Execute-MSI @ExecuteDefaultMSISplat + } + # - ## + ##*=============================================== + ##* POST-REPAIR + ##*=============================================== + [string]$installPhase = 'Post-Repair' + ## } - ##*=============================================== - ##* END SCRIPT BODY - ##*=============================================== + ##*=============================================== + ##* END SCRIPT BODY + ##*=============================================== - ## Call the Exit-Script function to perform final cleanup operations - Exit-Script -ExitCode $mainExitCode + ## Call the Exit-Script function to perform final cleanup operations + Exit-Script -ExitCode $mainExitCode } Catch { - [int32]$mainExitCode = 60001 - [string]$mainErrorMessage = "$(Resolve-Error)" - Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName - Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' - Exit-Script -ExitCode $mainExitCode + [int32]$mainExitCode = 60001 + [string]$mainErrorMessage = "$(Resolve-Error)" + Write-Log -Message $mainErrorMessage -Severity 3 -Source $deployAppScriptFriendlyName + Show-DialogBox -Text $mainErrorMessage -Icon 'Stop' + Exit-Script -ExitCode $mainExitCode } diff --git a/README.md b/README.md index 5695642..ae21aee 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # DeployM365Apps + +Deploy the Microsoft 365 Apps for Enterpirse via Microsoft CDN with PowerShell Application Deployment Toolkit 3.8.3. diff --git a/SupportFiles/M365Apps_Install.xml b/SupportFiles/M365Apps_Install.xml index 5f8f15f..ca3c9a6 100644 --- a/SupportFiles/M365Apps_Install.xml +++ b/SupportFiles/M365Apps_Install.xml @@ -1,5 +1,5 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/SupportFiles/Project_Install.xml b/SupportFiles/Project_Install.xml index 043e2e2..cb0aa6e 100644 --- a/SupportFiles/Project_Install.xml +++ b/SupportFiles/Project_Install.xml @@ -1,5 +1,5 @@ - + @@ -18,7 +18,7 @@ - + diff --git a/SupportFiles/Visio_Install.xml b/SupportFiles/Visio_Install.xml index d36d32a..e86387c 100644 --- a/SupportFiles/Visio_Install.xml +++ b/SupportFiles/Visio_Install.xml @@ -20,7 +20,7 @@ - + From c6e51e7f95d1c567487f0e46a70f5658fdd63662 Mon Sep 17 00:00:00 2001 From: cameronkollwitz Date: Sun, 17 Jan 2021 21:18:49 -0800 Subject: [PATCH 13/15] Add LICENSE --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) 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 General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 95ce42187ecfed50f29273575bee9d7e6fa3fdbc Mon Sep 17 00:00:00 2001 From: cameronkollwitz Date: Sun, 6 Mar 2022 11:42:58 -0800 Subject: [PATCH 14/15] Update setup.exe --- Files/setup.exe | Bin 5641016 -> 8187224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Files/setup.exe b/Files/setup.exe index f91bbe652f57d8881349c7ccdef0e3228928b148..fdb53ae42a80eb5b433a98cdace1670e1d10e807 100644 GIT binary patch literal 8187224 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

IjobyC;= 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