📜 ⬆️ ⬇️

Powershell and stack depth

As a developer, I often develop deployment scripts. In one of the projects, the task arose in front of me to automate the deployment of the project, which consisted of several dozen tasks, with the ability to customize the composition of the components deployed on the stand.

First of all, work was carried out on the unification of tasks interfaces and the following methods were highlighted:

Deployment Task Interface
$Task1_Config = ...; # ,     . function Task1_CheckRequirements() {} # ,     . function Task1_CanExecute($project) {} #   . function Task1_Execute($project, $context) {} 


Considering that such steps became more and more, it was becoming more and more difficult to maintain scripts in this form. After examining possible solutions, it was decided to implement each task as a separate object:

Object Interface for Deployment Task
 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, the deployment scripts worked for a long time and nothing foreshadowed trouble. At one point, I was faced with the task of deploying to a remote server. In powershell there is a very convenient WinRM mechanism, which we used to use very actively, and, accordingly, to solve the problem, we stopped on it.
')
The solution was unstable. On some deployment tasks, either an error occurred or Invoke-Command showed that the remote script was executed correctly, but in fact it was interrupted.
Unable to process remote command data. Error message: WSMan provider lead process did not return the correct answer. Vendor in leading process may behave incorrectly.

The WSMan has not been accepted. A provider may be improperly.

In EventViewer, I was able to find that the process on the remote machine was terminated with an error of 1726, but no intelligible information about the error could be detected. At the same time, the launch of the same task on the remote machine was always successful.

In the course of numerous experiments I caught The script failed due to the call depth overflow, which determined the future direction of research.

Since PowerShell v2, the maximum stack depth in powershell scripts is 1000 calls, in subsequent versions this value was still significantly raised and stack overflow errors never occurred.

I decided to conduct several tests to determine the stack depth when calling locally and through WinRM. To do this, prepared a testing tool.

Testing Toolkit
 $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 recursion depth:

Determining the 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 - the local stack depth is more than 3000, remotely - a little more than 150.

150 is a pretty big deal. To achieve it in the real work of deployment scripts is unrealistic.

The second test determines the possible recursion depth when using objects:

Determining the 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 bit worse. Remotely stack depth 130-133. But for work it is also very important.

Further study of the original deployment scripts prompted the idea to check how try-catch blocks work:

Determining the depth of recursion using objects and try-catch
 $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 I was waiting for a huge surprise. When using “objects” and generating an exceptional situation, the possible stack depth is locally about 130, and remotely only 5.

Determining the depth of recursion using try-catch without objects
 $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 with the abandonment of the use of "objects" the problem disappeared. The stack depths were at the level of the first test.

In powershell 5, classes appeared. Conducted a test using them:

Determining the depth of recursion using try-catch without objects
 $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. 


No special winnings were received. When calling via WinRM, the stack depth was only 7 hops. What is also not enough for the normal operation of scripts.

Working with testing scripts, the idea came to implement objects with the hash + script block.

Determining the depth of recursion using try-catch and 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 stack depth of 55 hops is already quite sufficient.

The following summarizes the available stack depth in one table:
locallyvia winRM
Function> 3000~ 150
Object methods> 3000~ 130
Object methods with try-catch~ 130five
Function with try-catch> 2000~ 150
Class method (ps5) with try-catch~ 1307
Hash + script block with try-catch> 1000~ 55

I hope that this information will be useful not only for me! :)

Source: https://habr.com/ru/post/320970/


All Articles