Questions about error handling and exit codes

Hello all,

PSADT is new to me but I’m loving the power of it already. I’m getting my hands on it after being used to deploying all my packages with a VBS wrapper script using my own functions. The use of Powershell is appealing, yet hard to get used to coming from VBS exclusively. I’ve read the documentation and made a first script to deploy Firefox, see code below.

So far so good, but I’m having issues with eror handling. In my previous scripts I started with a main exit code (which seems to be present in deploy-application.psi as well) and I raise this every time an essential action /step of the installation process fails. However, when something goes wrong, I usually let my scripts continue to at least execute the rest of the actions. In PSADT however, when something goes wrong the script stops and doesn’t execute further functions.

For example, to install firefox I check installed versions first, then I use a welcome prompt when installation should continue. When all Firefox windows are closed, I start executing the setup, copy some extensions and then prompt the user when the script is done.

BUT:

  • When the function Execute-Process encounters an error (like ‘Path not found’) it throws the error and the script stops executing.
    - Rather I would want it to continue with the rest of the script, copy the files and foremost give the user the message that the installation failed!

I haven’t found a way to do this so far, but is there a way to achieve this?
With the function Copy-Item I could catch errors like this and raise the main exit code just like I used to. I had to specify -ContinueOnError $false for it to work, bu this doesn’t work with Execute-Process.

I guess the question comes down to whether I’m seeing it all wrong and I’m not using a correct way to handle errors, or am I missing some functionality that the toolkit provides and I’m simple not aware of? Fact is that I prefer to have total control of what happens in my script, so I want to decide when the script should continue and when not. I can have the script exit with a simple line of commands myself, so why would some functions bail out and others continue?

Additionally I would like to suggest to let the Show-InstallationProgress scale to the custom message text that is provided. When I use this block, it gets cut off and looks ugly:
Show-InstallationProgress -StatusMessage "Installation of $appName $appVersion in progress. Please wait…nThis window will close automatically when the installation is finished.nn Do not attempt to start new instances of $appName during this process.nnThank you,nICT Client Services Team"

EXAMPLE SCRIPT:

<code>[CmdletBinding()]
Param (
	[Parameter(Mandatory=$false)]
	[ValidateSet(&#039;Install&#039;,&#039;Uninstall&#039;)]
	[string]$DeploymentType = &#039;Install&#039;,
	[Parameter(Mandatory=$false)]
	[ValidateSet(&#039;Interactive&#039;,&#039;Silent&#039;,&#039;NonInteractive&#039;)]
	[string]$DeployMode = &#039;Interactive&#039;,
	[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 &#039;ByPass&#039; -Scope &#039;Process&#039; -Force -ErrorAction &#039;Stop&#039; } Catch {}
	
	##*===============================================
	##* VARIABLE DECLARATION
	##*===============================================
	## Variables: Application
	[string]$appVendor = &#039;Mozilla&#039;
	[string]$appName = &#039;Firefox&#039;
	[string]$appVersion = &#039;50.1.0&#039;
	[string]$appArch = &#039;x86&#039;
	[string]$appLang = &#039;EN&#039;
	[string]$appRevision = &#039;01&#039;
	[string]$appScriptVersion = &#039;1.0.0&#039;
	[string]$appScriptDate = &#039;12/23/2016&#039;
	[string]$appScriptAuthor = &#039;Jonathan De Nil&#039;
	##*===============================================
	## Variables: Install Titles (Only set here to override defaults set by the toolkit)
	[string]$installName = &#039;&#039;
	[string]$installTitle = &#039;&#039;
	
	##* Do not modify section below
	#region DoNotModify
	
	## Variables: Exit Code
	[int32]$mainExitCode = 0
	
	## Variables: Script
	[string]$deployAppScriptFriendlyName = &#039;Deploy Application&#039;
	[version]$deployAppScriptVersion = [version]&#039;3.6.8&#039;
	[string]$deployAppScriptDate = &#039;02/06/2016&#039;
	[hashtable]$deployAppScriptParameters = $psBoundParameters
	
	## Variables: Environment
	If (Test-Path -LiteralPath &#039;variable:HostInvocation&#039;) { $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 = &quot;$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1&quot;
		If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType &#039;Leaf&#039;)) { Throw &quot;Module does not exist at the specified location [$moduleAppDeployToolkitMain].&quot; }
		If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain }
	}
	Catch {
		If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 }
		Write-Error -Message &quot;Module [$moduleAppDeployToolkitMain] failed to load: &lt;code&gt;n$($_.Exception.Message)&lt;/code&gt;n </code>

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') {	
	##*===============================================
	##* PRE-INSTALLATION
	##*===============================================
	[string]$installPhase = 'Pre-Installation'
	
	## Detection check
	[string]$ProgramPath = "$envProgramFilesX86\Example\Example.exe"
	[boolean]$VersionOK = $false

	If (Test-Path -Path $ProgramPath) {
		[string]$ProgramVersion = Get-FileVersion -File $ProgramPath
		Write-Log -Message "--&gt; ProgramPath = $ProgramPath"
		Write-Log -Message "--&gt; ProgramVersion = $ProgramVersion"
		
		If ([System.Version]$ProgramVersion -ge [System.Version]$appVersion) {
			$VersionOK = $true
			Write-Log -Message "--&gt; Version OK."
		}
		Else {
			Write-Log -Message "--&gt; Version not OK."
		}
	}
	
	If (!($VersionOK)) {
		## Show Welcome Message
		Show-InstallationWelcome -CloseApps "firefox=Mozilla Firefox" -ForceCloseAppsCountdown 1800 -Minimizewindows $false -BlockExecution -PersistPrompt
		
		## Show Progress Message (with the default message)
		Show-InstallationProgress
		
		##*===============================================
		##* INSTALLATION 
		##*===============================================
		[string]$installPhase = 'Installation'
		
		## Setup
		Try { Execute-Process -Path "$dirFiles\Install\Firefox_Setup.exe" -Parameters "-ms /ini=<code>&quot;$dirSupportFiles\Config\Config.ini</code>"" -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		
		##*===============================================
		##* POST-INSTALLATION
		##*===============================================
		[string]$installPhase = 'Post-Installation'
		
		## Copy config files
		Try { Copy-File -Path "$dirSupportFiles\Config\policies.js" -Destination "$envProgramFilesX86\Mozilla Firefox\defaults\pref\policies.js" -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		Try { Copy-File -Path "$dirSupportFiles\Config\mozilla.cfg" -Destination "$envProgramFilesX86\Mozilla Firefox\mozilla.cfg" -ContinueOnError $false	} Catch { $mainExitCode = $mainExitCode + 1 }
		Try { Copy-File -Path "$dirSupportFiles\Extensions\*.*" -Destination "$envProgramFilesX86\Mozilla Firefox\browser\extensions" -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		
		## Delete shortcuts
		Remove-File -Path "$envCommonDesktop\*Firefox*.lnk" -ContinueOnError $true
		
		## Display a message at the end of the install
		If (-not $useDefaultMsi) { 
			If (!($mainExitCode -eq 0)) {
				Show-InstallationPrompt -Message "Installation failed with exit code [$mainExitCode].`nPlease contact the Service Desk." -ButtonRightText "OK" -Icon "Error" -NoWait -Timeout 60
			}
			Else {
				Show-InstallationPrompt -Message "Installation complete." -ButtonRightText "OK" -Icon "Information" -NoWait -Timeout 60
			}
		}
	}
	Else {
		Write-Log -Message "--&gt; $appName $appVersion or higher is already installed."
		Write-Log -Message "--&gt; Installation aborted."
	}
}
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 "firefox=Mozilla Firefox" -ForceCloseAppsCountdown 1800 -Minimizewindows $false -BlockExecution -PersistPrompt
	
	## Show Progress Message (with the default message)
	Show-InstallationProgress
	
	## &lt;Perform Pre-Uninstallation tasks here&gt;
	Show-InstallationPrompt -Message "Uninstall placeholder." -ButtonRightText 'OK' -Icon "Information" -NoWait -Timeout 60
	
	##*===============================================
	##* 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
	}
	
	## &lt;Perform Uninstallation tasks here&gt;
	
	##*===============================================
	##* POST-UNINSTALLATION
	##*===============================================
	[string]$installPhase = 'Post-Uninstallation'
	
	## &lt;Perform Post-Uninstallation tasks here&gt;
}

##*===============================================
##* 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
}`

I’m sorry about the formatting of the code. I’ll try to paste the example script again as plain text as I can’t edit my first post anymore…

EXAMPLE SCRIPT:
<pre class=“brush: text; gutter: true; first-line: 1; highlight: []; html-script: false”>[CmdletBinding()]
Param (
[Parameter(Mandatory=$false)]
[ValidateSet(‘Install’,‘Uninstall’)]
[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 = &#039;Mozilla&#039;
[string]$appName = &#039;Firefox&#039;
[string]$appVersion = &#039;50.1.0&#039;
[string]$appArch = &#039;x86&#039;
[string]$appLang = &#039;EN&#039;
[string]$appRevision = &#039;01&#039;
[string]$appScriptVersion = &#039;1.0.0&#039;
[string]$appScriptDate = &#039;12/23/2016&#039;
[string]$appScriptAuthor = &#039;Jonathan De Nil&#039;
##*===============================================
## Variables: Install Titles (Only set here to override defaults set by the toolkit)
[string]$installName = &#039;&#039;
[string]$installTitle = &#039;&#039;

##* Do not modify section below
#region DoNotModify

## Variables: Exit Code
[int32]$mainExitCode = 0

## Variables: Script
[string]$deployAppScriptFriendlyName = &#039;Deploy Application&#039;
[version]$deployAppScriptVersion = [version]&#039;3.6.8&#039;
[string]$deployAppScriptDate = &#039;02/06/2016&#039;
[hashtable]$deployAppScriptParameters = $psBoundParameters

## Variables: Environment
If (Test-Path -LiteralPath &#039;variable:HostInvocation&#039;) { $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 = &quot;$scriptDirectory\AppDeployToolkit\AppDeployToolkitMain.ps1&quot;
	If (-not (Test-Path -LiteralPath $moduleAppDeployToolkitMain -PathType &#039;Leaf&#039;)) { Throw &quot;Module does not exist at the specified location [$moduleAppDeployToolkitMain].&quot; }
	If ($DisableLogging) { . $moduleAppDeployToolkitMain -DisableLogging } Else { . $moduleAppDeployToolkitMain }
}
Catch {
	If ($mainExitCode -eq 0){ [int32]$mainExitCode = 60008 }
	Write-Error -Message &quot;Module [$moduleAppDeployToolkitMain] failed to load: <code>n$($_.Exception.Message)</code>n `n$($_.InvocationInfo.PositionMessage)&quot; -ErrorAction &#039;Continue&#039;
	## Exit the script, returning the exit code to SCCM
	If (Test-Path -LiteralPath &#039;variable:HostInvocation&#039;) { $script:ExitCode = $mainExitCode; Exit } Else { Exit $mainExitCode }
}

#endregion
##* Do not modify section above
##*===============================================
##* END VARIABLE DECLARATION
##*===============================================

If ($deploymentType -ine &#039;Uninstall&#039;) {	
	##*===============================================
	##* PRE-INSTALLATION
	##*===============================================
	[string]$installPhase = &#039;Pre-Installation&#039;
	
	## Detection check
	[string]$ProgramPath = &quot;$envProgramFilesX86\Example\Example.exe&quot;
	[boolean]$VersionOK = $false

	If (Test-Path -Path $ProgramPath) {
		[string]$ProgramVersion = Get-FileVersion -File $ProgramPath
		Write-Log -Message &quot;--&gt; ProgramPath = $ProgramPath&quot;
		Write-Log -Message &quot;--&gt; ProgramVersion = $ProgramVersion&quot;
		
		If ([System.Version]$ProgramVersion -ge [System.Version]$appVersion) {
			$VersionOK = $true
			Write-Log -Message &quot;--&gt; Version OK.&quot;
		}
		Else {
			Write-Log -Message &quot;--&gt; Version not OK.&quot;
		}
	}
	
	If (!($VersionOK)) {
		## Show Welcome Message
		Show-InstallationWelcome -CloseApps &quot;firefox=Mozilla Firefox&quot; -ForceCloseAppsCountdown 1800 -Minimizewindows $false -BlockExecution -PersistPrompt
		
		## Show Progress Message (with the default message)
		Show-InstallationProgress
		
		##*===============================================
		##* INSTALLATION 
		##*===============================================
		[string]$installPhase = &#039;Installation&#039;
		
		## Setup
		Try { Execute-Process -Path &quot;$dirFiles\Install\Firefox_Setup.exe&quot; -Parameters &quot;-ms /ini=<code>&amp;quot;$dirSupportFiles\Config\Config.ini</code>&quot;&quot; -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		
		##*===============================================
		##* POST-INSTALLATION
		##*===============================================
		[string]$installPhase = &#039;Post-Installation&#039;
		
		## Copy config files
		Try { Copy-File -Path &quot;$dirSupportFiles\Config\policies.js&quot; -Destination &quot;$envProgramFilesX86\Mozilla Firefox\defaults\pref\policies.js&quot; -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		Try { Copy-File -Path &quot;$dirSupportFiles\Config\mozilla.cfg&quot; -Destination &quot;$envProgramFilesX86\Mozilla Firefox\mozilla.cfg&quot; -ContinueOnError $false	} Catch { $mainExitCode = $mainExitCode + 1 }
		Try { Copy-File -Path &quot;$dirSupportFiles\Extensions\*.*&quot; -Destination &quot;$envProgramFilesX86\Mozilla Firefox\browser\extensions&quot; -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }
		
		## Delete shortcuts
		Remove-File -Path &quot;$envCommonDesktop\*Firefox*.lnk&quot; -ContinueOnError $true
		
		## Display a message at the end of the install
		If (-not $useDefaultMsi) { 
			If (!($mainExitCode -eq 0)) {
				Show-InstallationPrompt -Message &quot;Installation failed with exit code [$mainExitCode].`nPlease contact the Service Desk.&quot; -ButtonRightText &quot;OK&quot; -Icon &quot;Error&quot; -NoWait -Timeout 60
			}
			Else {
				Show-InstallationPrompt -Message &quot;Installation complete.&quot; -ButtonRightText &quot;OK&quot; -Icon &quot;Information&quot; -NoWait -Timeout 60
			}
		}
	}
	Else {
		Write-Log -Message &quot;--&gt; $appName $appVersion or higher is already installed.&quot;
		Write-Log -Message &quot;--&gt; Installation aborted.&quot;
	}
}
ElseIf ($deploymentType -ieq &#039;Uninstall&#039;) {
	##*===============================================
	##* PRE-UNINSTALLATION
	##*===============================================
	[string]$installPhase = &#039;Pre-Uninstallation&#039;
	
	## Show Welcome Message, close Internet Explorer with a 60 second countdown before automatically closing
	Show-InstallationWelcome -CloseApps &quot;firefox=Mozilla Firefox&quot; -ForceCloseAppsCountdown 1800 -Minimizewindows $false -BlockExecution -PersistPrompt
	
	## Show Progress Message (with the default message)
	Show-InstallationProgress
	
	## &lt;Perform Pre-Uninstallation tasks here&gt;
	Show-InstallationPrompt -Message &quot;Uninstall placeholder.&quot; -ButtonRightText &#039;OK&#039; -Icon &quot;Information&quot; -NoWait -Timeout 60
	
	##*===============================================
	##* UNINSTALLATION
	##*===============================================
	[string]$installPhase = &#039;Uninstallation&#039;
	
	## Handle Zero-Config MSI Uninstallations
	If ($useDefaultMsi) {
		[hashtable]$ExecuteDefaultMSISplat =  @{ Action = &#039;Uninstall&#039;; Path = $defaultMsiFile }; If ($defaultMstFile) { $ExecuteDefaultMSISplat.Add(&#039;Transform&#039;, $defaultMstFile) }
		Execute-MSI @ExecuteDefaultMSISplat
	}
	
	## &lt;Perform Uninstallation tasks here&gt;
	
	##*===============================================
	##* POST-UNINSTALLATION
	##*===============================================
	[string]$installPhase = &#039;Post-Uninstallation&#039;
	
	## &lt;Perform Post-Uninstallation tasks here&gt;
}

##*===============================================
##* 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
}

Hi,

I did not read your code. Too much informations.
In which circumstance are you getting a path not found? Why not doing a test-path before running the process?

Thanks,

Hi, Thanks for your reply. I pasted the full script so that anyone who wishes can try on his own computer.

I’m getting ‘Path not found’ when I deliberately use a non-existing name for the setup, just to test things. Like I said: I want to know and control how the script behaves in every situation. It is important to me to give a solid experience to our end users so I would like the script to run till the end and count errors along the way.

If (Test-Path -Path $ProgramPath -ErrorAction SilentlyContinue)

should do the trick.

Thank you, this indeed provides a way to handle this very specific situation. I am aware of this, but I thought the general idea of the toolkit was to take the big lifting out of our hands, and this would imply that I have to build in many checks myself, including writing additional information to the log about what was checked (test-path) and where things go wrong (file not found, counted as error). Don’t get me wrong, I can do that but my guess is that there must be better ways to do that.

The big question is still: how can I choose to have the script execute all my actions (called with functions from AppDeployToolkitMain.ps1) till the end so the user gets a correct notification without the install bailing out in the middle, leaving the user in the cold? Is there any high-level way to achieve this?

I tried changing the $ErrorActionPreference in AppDeployToolkitMain.ps1 but that doesn’t change any behaviour at all. I also tried overwriting this variable in Deploy-Application.ps1 but this makes no difference either. Resulting in that a faulty path in the function ‘Exectue-Process’ causes the the to script to exit, but a faulty path in the function ‘Copy-File’ causes the script to continue, allowing me to catch the error.

Try { Copy-File -Path “$dirSupportFiles\Config\policies.js” -Destination “$envProgramFilesX86\Mozilla Firefox\defaults\pref\policies.js” -ContinueOnError $false } Catch { $mainExitCode = $mainExitCode + 1 }

I was going to suggest the $ErrorActionPreference until I read your reply through :)… I guess it only handles the toolkits own cmdlets then. I.e. Copy-File would be handled since it’s defined in AppDeployToolkitMain.ps1 but Test-Path wouldn’t as its source is Microsoft.PowerShell.Management.

I just try to ensure that my scripts never fail, but of course every situation is different and I guess there’s times you will have various results which need different error handling, but when such situation occurs for me I try and find a different/simpler solution. Sorry for not having a better response.

Hi again and thanks for posting.

I digged into the AppDeployToolkitMain.ps1 and read a lot of code in there. Imo the $ErrorActionPreference is set to ‘Stop’ there but with a lot of commands the author chooses to override it with a ‘Continue’. In some cases always, in some cases dependent on the parameters supplied.

As for the stopping error that exits the script, it is not a global PowerShell option but it is very specifically written in the toolkit that the Exit-Script function should be called in that case. In other words, the ‘Execute-Process’ is designed to make the script stop executing the rest of commands when the process that is being called isn’t found with a If/Else statement.

I could work around this writing my own variant of ‘Execute-Process’ in the Extensions script, but since I see that this function gets called in some other funcitons as well, this would mean I would have to make even more variants of these other functions too. Another option would be modifying the mail toolkit, but this would be very ugly.

Long story short: neither option looks like a good idea to me, so I know I should check the path of executables first when calling Execute-Process if I choose to make the script continue if not found. I believe I can get the expected result from other functions with the available parameters and Try/Catch blocks.

A word from the author about this, confirming this or even telling me that I’m doing something wrong and why, would be nice but this is as best as I can see it. :slight_smile: