Повернення значення з powershell invoke-command агенту SQL-Server

При створенні власної методики управління резервними копіями на безлічі серверів MS-SQL я витратив купу часу на вивчення механізму передачі значень у powershell при віддалених викликах, тому пишу самому собі пам'ятку, а раптом комусь ще знадобиться.

Отже, візьмемо спочатку найпростіший скрипт і запустимо його локально:

$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Для запуску скриптів я користуватимуся наступним CMD-файлом, щоразу його наводити не стану:

@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%

На екрані ми побачимо таке:

Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1


Тепер запустимо цей же скрипт через WSMAN (віддалено):

Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

І ось вам результат:

Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0

Чудово, Errorlevel кудись зник, але ж нам потрібно отримати значення зі скрипту! Пробуємо наступну конструкцію:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

Тут ще цікавіше. Звістка в Output кудись зникла:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Тепер як ліричний відступ відзначу, що якщо всередині функції Powershell Ви напишете Write-Output або просто вираз без привласнення його будь-якої змінної (а це неявно передбачає виведення в канал Output), то навіть при локальному запуску на екран нічого не буде виведено! Це наслідок конвеєрної архітектури powershell - кожна функція має власний конвеєр Output, для нього створюється масив, і все, що в нього потрапляє, вважається результатом виконання функції, оператор Return додає значення, що повертається в цей же конвеєр останнім елементом і передає управління в функцію, що викликала. Для ілюстрації виконаємо локально наступний скрипт:

Function Write-Log {
  Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
  Write-Output ("Function: "+$OutString)
  Return "ReturnValue"
}
Write-Output ("Main: "+"ParameterValue")
$res = Write-Log "ParameterValue"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Main: "+$_) }

І ось його результат:

Main: ParameterValue

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Main: Function: ParameterValue
Main: ReturnValue

Головна функція (тіло скрипта) також має свій конвеєр Output, і якщо ми запустимо перший скрипт із CMD, перенаправивши вивід у файл,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

то на екрані ми побачимо

ERRORLEVEL=1

а у файлі

Out to host.
Out to output.
ExitCode: 1
1

якщо ж зробимо аналогічний виклик з powershell

PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt

то на екрані буде

Out to host.
ExitCode: 1

а у файлі

Out to output.
1

Це відбувається тому, що CMD запускає PowerShell, який за відсутності інших вказівок змішує два потоки (Host і Output) і віддає їх CMD, який відправляє у файл все, що отримав, а у разі запуску з PowerShell ці два потоки існують окремо, і символ Перенаправлення впливає лише на Output.

Повертаючись до основної теми, згадаємо, що об'єктна модель .NET всередині powershell повноцінно існує в рамках одного комп'ютера (однієї ОС), при віддаленому запуску коду через WSMAN передача об'єктів відбувається через XML-серіалізацію, що вносить багато додаткового інтересу до наших досліджень. Продовжимо експерименти запуском наступного коду:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)

І ось що у нас на екрані:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Не удается преобразовать аргумент "exitCode", со значением: "System.Object[]", для "SetShouldExit" в тип "System.Int32": "Не удается преобразовать значение "System.Object[]" типа "System.Object[]" в тип "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Чудовий результат! Він означає, що при виклику Invoke-Command зберігається поділ конвеєрів на два потоки (Host та Output), що дає нам надію на успіх. Спробуємо залишити в потоці Output тільки одне значення, для чого змінимо перший скрипт, який ми запускаємо віддалено:

$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Запустимо його так:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)

і ... ТАК, схоже, це перемога!

Out to host.
ExitCode: 4

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Спробуймо розібратися, що в нас сталося. Ми викликали локально PowerShell, який, у свою чергу, викликав PowerShell на віддаленому комп'ютері і виконав там наш скрипт. Два потоки (Host і Output) з віддаленої машини були серіалізовані і передані назад, при цьому потік Output при наявності в ньому одного цифрового значення був перетворений до типу Int32 і в такому вигляді переданий стороні, що приймає, а сторона, що приймає, використовувала його в якості коду завершення викликаючого PowerShell-а.

І як остання перевірка створимо на сервері SQL завдання з одного кроку з типом «Операційна система (cmdexec)» з таким текстом:

PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"

УРА! Завдання завершилося з помилкою, текст у журналі:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Висновки:

  • Уникайте використання Write-Output та вказівок виразів без надання. Пам'ятайте, що перенесення цього коду до іншого місця скрипту може призвести до несподіваних результатів.
  • У скриптах, призначених не для ручного запуску, а для використання у Ваших механізмах автоматизації, особливо для віддалених викликів через WINRM, робіть ручну обробку помилок через Try/Catch, і домагайтеся того, щоб при будь-якому розвитку подій цей скрипт відправив у потік Output рівно одне значення примітивного типу Якщо хочете отримати класичний Errorlevel – це значення має бути числовим.

Джерело: habr.com

Додати коментар або відгук