Powershell и глубина стека

    Как разработчик, я часто разрабатываю скрипты развертывания. В одном из проектов передо мной возникла задача автоматизировать развертывание проекта, которое состояло из нескольких десятков заданий, с возможностью настраивать состав разворачиваемых на стенд компонентов.

    В первую очередь была проведена работа по унификации интерфейсов заданий и были выделены следующие методы:

    Интерфейс задания развертывания
    $Task1_Config = ...;
    
    # проверить, возможно ли выполнить шаг развертывания.
    function Task1_CheckRequirements() {}
    
    # проверить, необходимо ли выполнять шаг развертывания.
    function Task1_CanExecute($project) {}
    
    # выполнить шаг развертывания.
    function Task1_Execute($project, $context) {}
    


    Учитывая, что подобных шагов становилось все больше и больше, поддерживать в таком виде скрипты становилось все сложнее. Изучив возможные решения, было принято решение реализовать каждое задание как отдельный объект:

    Интерфейс `объекта` для задания развертывания
    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
    }
    


    В таком виде скрипты развертывания работали продолжительное время и ничто не предвещало беды. В один прекрасный момент передо мной встала задача провести развертывание на удаленном сервере. В powershell есть очень удобный механизм WinRM, который мы ранее очень активно использовали, и, соответственно, для решения поставленной задачи остановился на нем-же.

    Решение работало нестабильно. На некоторых заданиях развертывания возникали либо ошибка либо Invoke-Command показывал, что удаленный скрипт выполнился корректно, но по факту он прерывался.
    Не удалось обработать данные удаленной команды. Сообщение об ошибке: Ведущий процесс поставщика WSMan не вернул правильный ответ. Поставщик в ведущем процессе может вести себя неправильно

    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.

    В EventViewer смог найти, что процесс на удаленной машине завершался с ошибкой 1726, но никакой вразумительной информации об ошибке обнаружить не удавалось. При этом запуск того-же самого задания на удаленной машине всегда завершалось успешно.

    В ходе многочисленных экспериментов поймал в ошибку The script failed due to call depth overflow которая определила дальнейшее направление исследований.

    Со времен PowerShell v2 максимальная глубина стека в скриптах powershell составляет 1000 вызовов, в последующих версиях это значение было еще существенно поднято и ошибок типа stack overflow никогда не возникало.

    Решил провести несколько тестов для определения глубины стека при вызове локально и через WinRM. Для этого подготовил инструментарий тестирования.

    Инструментарий тестирования
    $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
        }
    }
    


    Первый тест определял возможную глубину рекурсии:

    Определение глубины рекурсии
    $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.
    


    По результату — локально глубина стека более 3000, удаленно — немного больше 150.

    150 — довольно большое значение. Достичь его в реальной работе скриптов развертывания нереально.

    Второй тест определяет возможную глубину рекурсии при использовании объектов:

    Определение глубины рекурсии при использовании объектов
    $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. 
    


    Результаты немного хуже. Удаленно глубина стека 130-133. Но для работы это тоже очень большое значение.

    Дальнейшее изучение исходных скриптов развертывания натолкнуло на мысль проверить, как работают try-catch блоки:

    Определение глубины рекурсии при использовании объектов и 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.
    


    И вот тут меня ожидал огромный сюрприз. При использовании «объектов» и генерации исключительной ситуации возможная глубина стека локально составила около 130, а удаленно всего 5.

    Определение глубины рекурсии при использовании try-catch без объектов
    $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
    


    Но при отказе от использования «объектов» проблема исчезала. Значения глубины стека оказались на уровне первого теста.

    В powershell 5 появились классы. Провел тест с их использованием:

    Определение глубины рекурсии при использовании try-catch без объектов
    $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.
    


    Особого выигрыша не получили. При вызове через WinRM глубина стека составила всего 7 хопов. Чего так-же недостаточно для нормальной работы скриптов.

    Работая со скриптами тестирования пришла мысль реализовать объекты при помощи hash + script block.

    Определение глубины рекурсии при использовании try-catch и 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
    


    Глубина стека в 55 хопов — это уже вполне достаточное значение.

    Ниже свел в одну таблицу результаты тестирования доступной глубина стека:
    локально через winRM
    Функция >3000 ~150
    Метода объекта >3000 ~130
    Метода объекта с try-catch ~130 5
    Функция с try-catch >2000 ~150
    Метода класса (PS5) с try-catch ~130 7
    Hash + script block с try-catch >1000 ~55

    Надеюсь, что эта информация окажется полезной не только мне! :)

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 9 207 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 2

      0
      В недалеком прошлом приходилось работать с Windows стеком, программисты писали на C#, и автоматизация тоже была на PoweShell.
      Правда это были не голые PowerShell скрипты — мы их использовали с PowerShell DRS:
      https://blogs.technet.microsoft.com/privatecloud/2013/08/30/introducing-powershell-desired-state-configuration-dsc/

      Если не ошибаюсь, PowerShell DRS работает через WinRM, наблюдали ли Вы там подобные проблемы?
        0
        Честно говоря я не представляю, как без особо сильных заморочек, с использование MOF манифеста, заставить dsc сгенерировать исключительную ситуацию.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое