Пры стварэнні ўласнай методыкі кіравання рэзервовымі копіямі на мностве сервераў 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