Powershell and stack depth
First of all, work was done to unify task interfaces and the following methods were identified:
$Task1_Config = ...;
# проверить, возможно ли выполнить шаг развертывания.
function Task1_CheckRequirements() {}
# проверить, необходимо ли выполнять шаг развертывания.
function Task1_CanExecute($project) {}
# выполнить шаг развертывания.
function Task1_Execute($project, $context) {}
Considering that such steps became more and more, maintaining scripts in this form became more and more difficult. Having studied the possible solutions, it was decided to implement each task as a separate object:
function Task1()
{
$result = New-Object -Typename PSObject -Property `
@{
"name" = "Task1"
"config" = ...
}
Add-Member -InputObject $result -MemberType ScriptMethod -Name CheckRequirements -Value `
{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name CanExecute -Value `
{
Param($project)
}
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($project, $context)
}
return $result
}
In this form, deployment scripts worked for a long time and nothing portended trouble. At one point, I faced the challenge of deploying to a remote server. In powershell there is a very convenient WinRM mechanism, which we used very actively earlier, and, accordingly, I settled on it for solving the task.
The solution worked unstable. On some deployment tasks, either an error occurred or Invoke-Command showed that the remote script executed correctly, but in fact it was interrupted.
Failed to process remote command data. Error message: The host process of the WSMan provider did not return the correct answer. The supplier in the lead process may behave incorrectly
Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response. A provider in the host process may have behaved improperly.
In EventViewer, I was able to find that the process on the remote machine ended with error 1726, but no intelligible error information was found. In this case, the launch of the same task on the remote machine always completed successfully.
In the course of numerous experiments, I caught The script failed due to call depth overflow, which determined the further direction of research.
Since PowerShell v2, the maximum stack depth in powershell scripts has been 1,000 calls, in subsequent versions this value has been significantly raised and stack overflow errors have never occurred.
I decided to conduct several tests to determine the depth of the stack when called locally and through WinRM. For this, I prepared testing tools.
$ErrorActionPreference = "Stop"
$cred = New-Object System.Management.Automation.PsCredential(...)
function runLocal($sb, $cnt)
{
Write-Host "Local $cnt"
Invoke-Command -ScriptBlock $sb -ArgumentList @($cnt)
}
function runRemote($sb, $cnt)
{
Write-Host "Remote $cnt"
$s = New-PSSession "." -credential $cred
try
{
Invoke-Command -Session $s -ScriptBlock $sb -ArgumentList @($cnt)
}
finally
{
Remove-PSSession -Session $s
}
}
The first test determined the possible depth of recursion:
$scriptBlock1 =
{
Param($cnt)
function test($cnt)
{
if($cnt -ne 0)
{
test $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
}
test $cnt
}
runLocal $scriptBlock1 3000
runRemote $scriptBlock1 150
runRemote $scriptBlock1 160
----------
Local 3000
Call depth: 3004
Remote 150
Call depth: 152
Remote 160
The script failed due to call depth overflow.
According to the result - locally, the stack depth is more than 3000, remotely - a little more than 150.
150 is a pretty big value. To achieve it in the real work of deployment scripts is unrealistic.
The second test determines the possible depth of recursion when using objects:
$scriptBlock2 =
{
Param($cnt)
function test()
{
$result = New-Object -Typename PSObject -Property @{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($cnt)
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
}
return $result
}
$obj = test
$obj.Execute($cnt)
}
runLocal $scriptBlock2 3000
runRemote $scriptBlock2 130
runRemote $scriptBlock2 135
----------
Local 3000
Call depth: 3004
Remote 130
Call depth: 132
Remote 135
Processing data for a remote command failed with the following error message: The WSMan provider host process did not return a proper response.
The results are a little worse. Remotely stack depth 130-133. But for work it is also very important.
Further study of the source deployment scripts prompted me to check how try-catch blocks work:
$scriptBlock3 =
{
Param($cnt)
function test()
{
$result = New-Object -Typename PSObject -Property @{ }
Add-Member -InputObject $result -MemberType ScriptMethod -Name Execute -Value `
{
Param($cnt)
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
return $result
}
try
{
$obj = test
$obj.Execute($cnt)
}
catch
{
Write-Host " Exception catched"
}
}
runLocal $scriptBlock3 130
runRemote $scriptBlock3 5
runRemote $scriptBlock3 6
----------
Local 130
Call depth: 134
Exception catched
Remote 5
Call depth: 7
Exception catched
Remote 6
Call depth: 8
The script failed due to call depth overflow.
And here a huge surprise awaited me. When using “objects” and generating an exception, the possible stack depth locally was about 130, and remotely only 5.
$scriptBlock4 =
{
Param($cnt)
function test($cnt)
{
if($cnt -ne 0)
{
test $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
try
{
test $cnt
}
catch
{
Write-Host " Exception catched"
}
}
runLocal $scriptBlock4 2000
runRemote $scriptBlock4 150
----------
Local 2000
Call depth: 2004
Exception catched
Remote 150
Call depth: 152
Exception catched
But when you abandoned the use of "objects" the problem disappeared. The stack depths were at the level of the first test.
Classes have appeared in powershell 5. Conducted a test using them:
$scriptBlock5 =
{
Param($cnt)
Class test
{
Execute($cnt)
{
if($cnt -ne 0)
{
$this.Execute($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
}
try
{
$t = [test]::new()
$t.Execute($cnt)
}
catch
{
Write-Host "Exception catched"
}
}
runLocal $scriptBlock5 130
runRemote $scriptBlock5 7
runRemote $scriptBlock5 8
----------
Local 130
Call depth: 134
Exception catched
Remote 7
Call depth: 9
Exception catched
Remote 8
Call depth: 10
The script failed due to call depth overflow.
They didn’t get much win. When called through WinRM, the stack depth was only 7 hopes. What is also not enough for the normal operation of scripts.
Working with test scripts, the idea came up to implement objects using hash + script block.
$scriptBlock6 =
{
Param($cnt)
function Call($self, $scriptName, [parameter(ValueFromRemainingArguments = $true)] $args)
{
$args2 = @($self) + $args
Invoke-Command -ScriptBlock $self.$scriptName -ArgumentList $args2
}
function test()
{
$result = @{ }
$result.Execute =
{
Param($self, $cnt)
if($cnt -ne 0)
{
Call $self Execute $($cnt - 1)
return
}
Write-Host " Call depth: $($(Get-PSCallStack).Count)"
throw "error"
}
return $result
}
try
{
$obj = test
Call $obj Execute $cnt
}
catch
{
Write-Host "Exception catched"
}
}
runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
----------
runLocal $scriptBlock6 1000
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
Local 1000
Call depth: 2005
Exception catched
Remote 55
Call depth: 113
Exception catched
Remote 60
Exception catched
The depth of the stack of 55 hop - this is quite a sufficient value.
The following table summarized the test results of the available stack depth:
| locally | through winRM | |
| Function | > 3000 | ~ 150 |
| Object method | > 3000 | ~ 130 |
| Object methods with try-catch | ~ 130 | 5 |
| Function with try-catch | > 2000 | ~ 150 |
| Class Methods (PS5) with try-catch | ~ 130 | 7 |
| Hash + script block with try-catch | > 1000 | ~ 55 |
I hope that this information will be useful not only to me! :)