Зварот значэння з 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

Дадаць каментар