Powershell -- cascading exit codes through nested shells
Finally resolved why I couldn't repro the issue in this cut down case; so, for future reference, just the real problem, and none of the dead ends:
I have a problem. I want to run a set of PowerShell scripts from an orchestrating PowerShell script, each in their own process so that I can relinquish assemblies that they've Add-Type
d quickly, and thus allow them to be updated when I re-deploy the whole system. And those scripts can potentially fail for some reasons, and the failure can be soft (retry with different parameters) or hard (abort entirely).
Plus, I don't want to capture the (write-)output of the inner scripts as I want to watch their progress; which leaves me with the exit code as mechanism, which is enough for my need.
We can test this mechanism with a simple script that we can make fail on demand:
#InnerScript.ps1 | |
param ( | |
[int]$code = 0 | |
) | |
$here = Get-Location | |
try { | |
Write-Output "output" | |
Write-Host "host" | |
[DateTime]::Now | |
exit $code | |
Write-Host "wtf??" | |
} | |
finally { | |
Set-Location $here | |
Write-Output "Inner script done" | |
} | |
Write-Output "Inner script REALLY done" | |
exit 0 |
And drive it like
#OuterScript.ps1 | |
$failWith = 23 | |
$succedWith = 0 | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $succedWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $failWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $succedWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } |
This results in
PS> $LASTEXITCODE 0 PS> .\OuterScript.ps1 output host 01 December 2014 17:34:50 Inner script done Got file code 0 output host 01 December 2014 17:34:52 Inner script done Got file code 23 PS> $LASTEXITCODE 23
However, if I add in one line (the one with the comment):
#OuterScript.ps1 | |
$failWith = 23 | |
$succedWith = 0 | |
$LASTEXITCODE = 0 # Make sure we start clean | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $succedWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $failWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } | |
& powershell -NoProfile -File .\InnerScript.ps1 -Code $succedWith | |
$code = $LASTEXITCODE | |
Write-Host "Got file code $code" | |
if ($code) { exit $code } |
We get
PS> $LASTEXITCODE 23 PS> $LASTEXITCODE = 0 PS> $LASTEXITCODE 0 PS> .\OuterScript.ps1 output host 01 December 2014 17:36:14 Inner script done Got file code 0 output host 01 December 2014 17:36:15 Inner script done Got file code 0 output host 01 December 2014 17:36:17 Inner script done Got file code 0 PS> $LASTEXITCODE 0 PS>
we get bitten by PowerShell's odd behaviour regarding automatic variables, which makes the local use of the name somehow refer to a different (and locally overriding) thing to what gets set by process exit -- another variation on the gotcha I hit a couple of years ago.
What I'd been hitting was just that explicit zeroing of the exit code (in a dense block of initialisations, where I'd not spotted it) had been happening, before a process launch and completion had created the "real" $LASTEXITCODE
. Remove that line, leave the value unset on start, and it all just works.