При создании собственной методики управления резервными копиями на множестве серверов MS-SQL я потратил кучу времени на изучение механизма передачи значений в powershell при удаленных вызовах, поэтому пишу самому себе памятку, а вдруг кому-то еще пригодится.
Итак, возьмем для начала простейший скрипт и запустим его локально:
Для запуска скриптов я буду пользоваться следующим CMD-файлом, каждый раз его приводить не стану:
На экране мы увидим следующее:
Теперь запустим этот же скрипт через WSMAN (удаленно):
И вот вам результат:
Чудесно, Errorlevel куда-то пропал, но нам ведь нужно получить значение из скрипта! Пробуем следующую конструкцию:
Тут еще интереснее. Весть вывод в Output куда-то исчез:
Теперь в качестве лирического отступления отмечу, что если внутри функции Powershell Вы напишете Write-Output или просто выражение без присваивания его какой-либо переменной (а это неявно подразумевает вывод в канал Output), то даже при локальном запуске на экран ничего не будет выведено! Это следствие конвейерной архитектуры powershell — каждая функция имеет собственный конвейер Output, для него создается массив, и все, что в него попадает, считается результатом выполнения функции, оператор Return добавляет возвращаемое значение в этот же конвейер последним элементом и передает управление в вызвавшую функцию. Для иллюстрации выполним локально следующий скрипт:
И вот его результат:
Главная функция (тело скрипта) также имеет свой конвейер Output, и если мы запустим первый скрипт из CMD, перенаправив вывод в файл,
то на экране мы увидим
а в файле
если же сделаем аналогичный вызов из powershell
то на экране будет
а в файле
Это происходит потому, что CMD запускает powershell, который при отсутствии других указаний смешивает два потока (Host и Output) и отдает их CMD, который отправляет в файл все, что получил, а в случае запуска из powershell эти два потока существуют отдельно, и символ перенаправления влияет только на Output.
Возвращаясь к основной теме, вспомним, что объектная модель .NET внутри powershell полноценно существует в рамках одного компьютера (одной ОС), при удаленном запуске кода через WSMAN передача объектов происходит через XML-сериализацию, что вносит много дополнительного интереса в наши исследования. Продолжим эксперименты запуском следующего кода:
И вот что у нас на экране:
Прекрасный результат! Он означает, что при вызове Invoke-Command сохраняется деление конвейеров на два потока (Host и Output), что дает нам надежду на успех. Попробуем оставить в потоке Output только одно значение, для чего изменим самый первый скрипт, который мы запускаем удаленно:
Запустим его так:
и… ДА, похоже, это победа!
Попытаемся разобраться, что у нас произошло. Мы вызвали локально powershell, который, в свою очередь, вызвал powershell на удаленном компьютере и выполнил там наш скрипт. Два потока (Host и Output) с удаленной машины были сериализованы и переданы обратно, при этом поток Output при наличии в нем одного цифрового значения был преобразован к типу Int32 и в таком виде передан принимающей стороне, а принимающая сторона использовала его в качестве кода завершения вызывающего powershell-а.
И в качестве последней проверки создадим на сервере SQL задание из одного шага с типом «Операционная система (cmdexec)» с таким текстом:
УРА! Задание завершилось с ошибкой, текст в журнале:
Выводы:
Итак, возьмем для начала простейший скрипт и запустим его локально:
$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 — это значение должно быть числовым.