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 не повернув правильну відповідь. Постачальник у провідному процесі може вести себе неправильно
data Processing 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 $3000 scriptBlock1
runRemote $150 scriptBlock1
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 $3000 scriptBlock2
runRemote $130 scriptBlock2
runRemote $scriptBlock2 135
----------
Local 3000
Call depth: 3004
Remote 130
Call depth: 132
Remote 135
Data Processing 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 $130 scriptBlock3
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 $150 scriptBlock4
----------
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 $130 scriptBlock5
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 $1000 scriptBlock6
runRemote $scriptBlock6 55
runRemote $scriptBlock6 60
----------
runLocal $1000 scriptBlock6
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
Сподіваюся, що ця інформація буде корисною не тільки мені! :)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.