Возврат значения из 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:\sqlagent\TestOutput1.ps1' $args[0] } -ArgumentList $args[0]

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

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

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

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:\sqlagent\TestOutput1.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:\sqlagent\TestOutput1.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:\sqlagent\TestOutput3.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:\sqlagent\TestOutput1.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:\sqlagent\TestOutput1.ps1' 6}; $host.SetShouldExit($res)"

УРА! Задание завершилось с ошибкой, текст в журнале:

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

Выводы:

  • Избегайте использования Write-Output и указания выражений без присваивания. Помните, что перенос этого кода в другое место скрипта может привести к неожиданным результатам.
  • В скриптах, предназначенных не для ручного запуска, а для использования в Ваших механизмах автоматизации, особенно для удаленных вызовов через WINRM, делайте ручную обработку ошибок через Try/Catch, и добивайтесь того, чтобы при любом развитии событий этот скрипт отправил в поток Output ровно одно значение примитивного типа. Если хотите получить классический Errorlevel — это значение должно быть числовым.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    0
    В продолжение темы сущая мелочь — у нас еще применяется абстракция имен серверов, реальные имена скрыты за DNS-алиасами, чтобы в случае замены сервера не переписывать кучу скриптов. Выяснилось, что попытка удаленного запуска powershell по такому алиасу вызывает ошибку Kerberos, поэтому задание агента SQL пришлось дополнить кодом для вычисления реального имени компьютера. Так как длина строки в CMDEXEC не резиновая, и пришлось бороться за длину этого кода, привожу его здесь:

    PowerShell -NonInteractive -NoProfile "$CN='backup02.technical'; $P=(Resolve-DnsName -DnsOnly -Name $CN -Type PTR -ErrorAction SilentlyContinue); If ($P -ne $NULL){$CN=$P[-1].NameHost}; $res=Invoke-Command -ComputerName $CN -ConfigurationName SQLAgent -ScriptBlock {&'C:\sqlagent\TestOutput1.ps1' 0}; Exit $res"

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

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