Back to Home

Powershell and stack depth

Powershell · StackDepth · WinRM

Powershell and stack depth

    As a developer, I often develop deployment scripts. In one of the projects, I had the task of automating the deployment of the project, which consisted of several dozen tasks, with the ability to customize the composition of the components deployed to the stand.

    First of all, work was done to unify task interfaces and the following methods were identified:

    Deployment Job Interface
    $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:

    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, 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.

    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 depth of recursion:

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

    Determining recursion depth 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:

    Determining recursion depth when 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 a huge surprise awaited me. When using “objects” and generating an exception, the possible stack depth locally was about 130, and remotely only 5.

    Determining recursion depth when 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 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:

    Determining recursion depth when 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.
    


    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.

    Determining recursion depth 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 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:
    locallythrough winRM
    Function> 3000~ 150
    Object method> 3000~ 130
    Object methods with try-catch~ 1305
    Function with try-catch> 2000~ 150
    Class Methods (PS5) with try-catch~ 1307
    Hash + script block with try-catch> 1000~ 55

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

    Read Next