Modified function Show-InstallationWelcome to enable custom message on deferral

Modified function Show-InstallationWelcome to enable custom message on deferral

I have tried to share this post on the toolkit extensions section at multiple times, but my post never get published, so I’m trying to post it here in General instead.

I’ve been experimenting a bit with the function Show-InstallationWelcome in order to get a custom message with a button to the SCCM Client’s Software Center in case a user defers an installation.

Here’s an example of hos to call the modified function

<code>
Show-InstallationWelcome -AllowDefer -DeferTimes 3 -ShowDeferMessage $true -DeferMessage &#039;&lt;Custom message to show when installation is defered.&gt;&#039;
</code>

and here’s the function modified:

<code>
#region Function Show-InstallationWelcome
Function Show-InstallationWelcome {
&lt;#
.SYNOPSIS
	Show a welcome dialog prompting the user with information about the installation and actions to be performed before the installation can begin.
    THIS VERSION HAS BEEN SLIGHTLY MODIFIED BY Pelle Zandler.
.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 &#039;=&#039; 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: &quot;winword=Microsoft Office Word,excel=Microsoft Office Excel&quot;
.PARAMETER Silent
	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.
.PARAMETER ForceCloseAppsCountdown
	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 &quot;Close Programs&quot; button. Option does not work in SYSTEM context unless toolkit launched with &quot;psexec.exe -s -i&quot; 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 10 seconds. 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.
.PARAMETER AllowDefer
	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.
.PARAMETER DeferTimes
	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.
.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: &quot;08/25/2013&quot; or &quot;08-25-2013&quot; or &quot;08-25-2013 18:00:00&quot;
	If the script is intended for multiple cultures, specify the date in the universal sortable date/time format: &quot;2013-08-22 11:51:52Z&quot;
	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.
.PARAMETER RequiredDiskSpace
	Specify required disk space in MB, used in combination with CheckDiskSpace.
.PARAMETER MinimizeWindows
	Specifies whether to minimize other windows when displaying prompt. Default: $true.
.PARAMETER TopMost
	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 deferal 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.
.PARAMETER ShowDeferMessage
    Specify Whether or not to show a message if the user defers an installation. Default: $false.
.PARAMETER DeferMessage
    Specify a custom message to display when application defered. Only valid if ShowDeferMessage is $true. If not specified, a default message is displayed.
.EXAMPLE
	Show-InstallationWelcome -CloseApps &#039;iexplore,winword,excel&#039;
	Prompt the user to close Internet Explorer, Word and Excel.
.EXAMPLE
	Show-InstallationWelcome -CloseApps &#039;winword,excel&#039; -Silent
	Close Word and Excel without prompting the user.
.EXAMPLE
	Show-InstallationWelcome -CloseApps &#039;winword,excel&#039; -BlockExecution
	Close Word and Excel and prevent the user from launching the applications while the installation is in progress.
.EXAMPLE
	Show-InstallationWelcome -CloseApps &#039;winword=Microsoft Office Word,excel=Microsoft Office Excel&#039; -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 &#039;winword,msaccess,excel&#039; -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 10 seconds so the user cannot ignore it by dragging it aside.
.EXAMPLE
	Show-InstallationWelcome -AllowDefer -DeferDeadline &#039;25/08/2013&#039;
	Allow the user to defer the installation until the deadline is reached.
.EXAMPLE
	Show-InstallationWelcome -CloseApps &#039;winword,excel&#039; -BlockExecution -AllowDefer -DeferTimes 10 -DeferDeadline &#039;25/08/2013&#039; -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
#&gt;
	[CmdletBinding()]
	Param (
		## Specify process names separated by commas. Optionally specify a process description with an equals symbol, e.g. &quot;winword=Microsoft Office Word&quot;
		[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 &quot;Close Programs&quot; button
		[Parameter(Mandatory=$false)]
		[switch]$PromptToSave = $false,
		## Specify whether to make the prompt persist in the center of the screen every 10 seconds.
		[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 = &#039;&#039;,
		## 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(Mandatory=$false)]
		[switch]$CheckDiskSpace = $false,
		## Specify required disk space in MB, used in combination with $CheckDiskSpace.
		[Parameter(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 deferal 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,
		## Specify whether to display a custom message if installation defered by user.
		[Parameter(Mandatory=$false)]
		[switch]$ShowDeferMessage = $false,
		## Specify the deadline (in format dd/mm/yyyy) for which deferral will expire as an option
		[Parameter(Mandatory=$false)]
		[string]$DeferMessage = &#039;&#039;
        
	)
	
	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 = &quot;$CloseApps,$defaultMsiExecutablesList&quot; }
		
		## Check disk space requirements if specified
		If ($CheckDiskSpace) {
			Write-Log -Message &#039;Evaluate disk space requirements.&#039; -Source ${CmdletName}
			[double]$freeDiskSpace = Get-FreeDiskSpace
			If ($RequiredDiskSpace -eq 0) {
				Try {
					#  Determine the size of the Files folder
					$fso = New-Object -ComObject &#039;Scripting.FileSystemObject&#039; -ErrorAction &#039;Stop&#039;
					$RequiredDiskSpace = [math]::Round((($fso.GetFolder($scriptParentPath).Size) / 1MB))
				}
				Catch {
					Write-Log -Message &quot;Failed to calculate disk space requirement from source files. </code>

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.FullDateTimePattern)
			}
			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) -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()
                
                if($ShowDeferMessage -eq $true){
                    
                    # Se default message if custom defer message not been set in deploy-application
                    if($DeferMessage.Length -eq 0)
                    {
                        $DeferMessage = "You have chosen to defer the installation. If you  would like to start it manually at a later point, please open the Software Center from your start menu, or press the button below to the left."
                    }
                    
                    $DeferPrompt = Show-InstallationPrompt -ButtonRightText 'OK' -ButtonLeftText 'Software Center' -Message $DeferMessage -Timeout 150
                    if($DeferPrompt -eq "Programvaresentret"){Execute-ProcessAsUser -Path "$envWinDir\CCM\SCClient.exe"}
                }
				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 '&lt;=') -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
`

I’m sure this modification requires enhancements and it comes as is and at own risk. But would love to hear some feedback and see if anyone else fancy this kind of feature?

Cheers
//Pelle

Trying to add a link to the modified kit as a zip file on my OneDrive… doesn’t seem to work… might also be the reason to my struggles publishing this thing…

(HTTPS COLON SLASH SLASH) 1drv (DOT) ms (SLASH) 1GlPjAs

Tested the link and realize it should be http, not https…

I totally forgot to mention the main reason for my modification… Which is that the experience I’ve had with the toolkit and defferals is that some (most) of my users tend to press defer without even reading what’s going on (based on timestamps in the log file of a PSAppDeploy installation). This combined with one week cycles of application deployment evaluation (using the Application Model in SCCM 2012 R2 SP1) results in deployments taking up to three weeks (or longer depending on the computer’s connectivity during this period and number of allowed defferals of course).

So my idea is to inform the users that “manual” triggering of application deployments is available in the Software Center and also present a button that executes the function Execute-ProcessAsUser to open the Software Center and exit the installer as deferred.

It is of course inconvenient for someone in a meeting or presentation to be bothered by application deployments (within my organization we are forced to deploy applications during work hours btw). So with this modification I’m hoping that some of my users might open up the Software Center at one point and choose to install required applications when it’s more suitable for them…

Could you send this too me directly? Also, have you tried changing the Show-InstallationWelcome so that it shows a countdown when there is a deferral and then when it times out, do a deferral instead of continuing with the installation?