Home > Net >  Powershell Custom Recursion and BeginInvoke()
Powershell Custom Recursion and BeginInvoke()

Time:06-03

I need some help with a async Powershell call that fails with either (1) max recursion depth exceeded or (2) invalid input argument.

The basic setup is as follows...

  • Coding and debugging done in VSCode
  • Powershell version is 5.1.19041 - desktop edition
  • Function to connect to SSAS server
  • Recursive function called by first function to convert object to XML
  • Create a Powershell session to execute the functions with parameters
  • BeginInvoke(), wait for completion, process results

Is there something with the async call that is messing with my code such that the comparison between $NestLevel and $MaxDepth is invalid or that $MaxDepth is set to 0?

I've gone over this a hundred times and I don't see anywhere $MaxDepth is ever set to anything below 1. The "if ( $NestLevel -gt $MaxDepth ) { return }" should terminate recursion but doesn't seem like it is.

Am I missing something?

Code example...

function Test-Recursion {
            Param (
                [Parameter(Mandatory=$false)]
                $InputObject,
        
                [Parameter(Mandatory=$false)]
                [ValidateRange(1,100)]
                [Int16] $MaxDepth   = 2
            )
        
            [Int16] $_NestLevel_  = 1
            if ( $_NestLevel_ -gt $MaxDepth ) { return }
        
            "<item><nest_level>$($_NestLevel_)</nest_level><max_depth>$($MaxDepth)</max_depth></item>"
            & $MyInvocation.MyCommand.ScriptBlock -InputObject $InputObject -MaxDepth $MaxDepth
        }
        
function Get-Info {
                Param (
                    [Parameter(Mandatory=$true)]
                    [String] $HostName,
        
                    [Parameter(Mandatory=$true)]
                    [ScriptBlock] $ConvertTo
                )
                
                $some_collection | `
                ForEach-Object {
                    @(
                        $_as  | ForEach-Object {
                            & $ConvertTo -InputObject $_ -MaxDepth 3 
                        }
                    ) | Sort-Object
                }
        }
        
        # this code fails with either...
        #   - excessive recursion depth
        #   - invalid $MaxDepth value of 0
        try {
            $_ps    = [powershell]::Create()
            $null   = $_ps.AddScript(${function:Get-Info})
            $null   = $_ps.AddParameter('HostName', 'some_server_name')
            $null   = $_ps.AddParameter('ConvertTo', ${function:Test-Recursion}) 
        
            $_handle    = $_ps.BeginInvoke()
            while ( -Not $_handle.IsCompleted ) {
                Start-Sleep -Seconds 5
            }
        
            $_result    = $_ps.EndInvoke($_handle)
            $_result
        } catch {
            $_
        } finally {
            $_ps.Dispose()
        }
        
        # this code works as expected
        $items = Get-Info -HostName 'some_server_name' -ConvertTo ${function:Test-Recursion}
        $items.table_data

CodePudding user response:

I'm not going to go into the details of what you're looking to accomplish with these functions, will just point out what the issue is, your powershell instance is running out of stack memory due to an infinite loop caused by your Test-Recursion function not knowing when to stop, hence the addition of a new parameter ([Int16] $NestingLevel) that will be passed as argument on each recursive call:

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2,

        [Parameter(DontShow)]
        [Int16] $NestingLevel
    )

    [Int16] $NestingLevel  = 1
    if ( $NestingLevel -gt $MaxDepth ) {
        return
    }

    "<item><nest_level>$NestingLevel</nest_level><max_depth>$MaxDepth</max_depth></item>"
    Test-Recursion -InputObject $InputObject -MaxDepth $MaxDepth -NestingLevel $NestingLevel
}

Test-Recursion -InputObject hello -MaxDepth 3

It is also worth noting that your recursive function can be simplified with a while loop:

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2,

        [Parameter(DontShow)]
        [Int16] $NestingLevel
    )

    while($NestingLevel   -lt $MaxDepth) {
        "<item><nest_level>$NestingLevel</nest_level><max_depth>$MaxDepth</max_depth></item>"
    }
}

Regarding your latest comment:

The root call initializes the variable to 1. Recursive calls would increment the variable and effectively provide a way to check the current nesting level.

No, the variable lives on each call of the function and then the incremented value is lost after a recursive call. You can however initialize the variable in the $script: scope and then it would work:

[Int16] $script:_NestLevel_  = 1

On a personal note, I do not like having $script: scoped variables inside my functions, there are better ways to do it in my opinion.

An easy way to demonstrate this is by modifying the function to output $_NestLevel_ on each call, as-is, you would see an infinite amount of 1 being displayed to your console.

function Test-Recursion {
    Param (
        [Parameter(Mandatory = $false)]
        $InputObject,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1,100)]
        [Int16] $MaxDepth = 2
    )

    [Int16] $_NestLevel_  = 1
    if ( $_NestLevel_ -gt $MaxDepth ) { return }
    $_NestLevel_
    & $MyInvocation.MyCommand.ScriptBlock -InputObject $InputObject -MaxDepth $MaxDepth
}
  • Related