Vrácení hodnoty z příkazu powershell invoke-command agentovi SQL Server

Při vytváření vlastní metodiky pro správu záloh na více serverech MS-SQL jsem strávil spoustu času studiem mechanismu předávání hodnot v Powershell během vzdálených hovorů, takže si píšu připomenutí pro případ, že by to bylo užitečné někomu jinému.

Začněme tedy jednoduchým skriptem a spusťte jej lokálně:

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

Ke spouštění skriptů použiji následující soubor CMD, nebudu jej zahrnout pokaždé:

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

Na obrazovce uvidíme následující:

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


Nyní spusťte stejný skript přes WSMAN (vzdáleně):

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

A tady je výsledek pro vás:

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

Skvělé, Errorlevel někam zmizel, ale potřebujeme získat hodnotu ze skriptu! Zkusme následující konstrukci:

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

To je ještě zajímavější. Zpráva ve výstupu někam zmizela:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Nyní, jako lyrická odbočka, poznamenám, že pokud uvnitř funkce Powershell napíšete Write-Output nebo jen výraz, aniž byste jej přiřadili jakékoli proměnné (a to implicitně implikuje výstup do výstupního kanálu), pak i když běží lokálně, na obrazovce se nic nezobrazí! Je to důsledek architektury powershell pipeline – každá funkce má svůj Output pipeline, je pro ni vytvořeno pole a vše, co do něj vstoupí, je považováno za výsledek provedení funkce, operátor Return přičte návratovou hodnotu ke stejnému potrubí jako poslední prvek a předá řízení volající funkci. Pro ilustraci spusťte lokálně následující skript:

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: "+$_) }

A tady je výsledek:

Main: ParameterValue

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

Hlavní funkce (tělo skriptu) má také svůj výstupní kanál, a pokud spustíme první skript z CMD, přesměrování výstupu do souboru,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

pak uvidíme na obrazovce

ERRORLEVEL=1

a v souboru

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

pokud provedeme podobné volání z powershell

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

pak to bude na obrazovce

Out to host.
ExitCode: 1

a v souboru

Out to output.
1

Děje se tak proto, že CMD spustí powershell, který při absenci jiných instrukcí smíchá dvě vlákna (Host a Output) a předá je CMD, které vše, co přijalo, odešle do souboru a v případě spuštění z powershell, tato dvě vlákna existují samostatně a přesměrování symbolů ovlivňuje pouze výstup.

Vrátíme-li se k hlavnímu tématu, připomeňme, že objektový model .NET uvnitř powershell plně existuje v rámci jednoho počítače (jednoho OS), při vzdáleném spouštění kódu přes WSMAN dochází k přenosu objektů pomocí XML serializace, což přináší spoustu zajímavostí navíc k našemu výzkumu. Pokračujme v experimentech spuštěním následujícího kódu:

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

A toto máme na obrazovce:

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

Skvělý výsledek! Znamená to, že při volání Invoke-Command je zachováno rozdělení pipelines do dvou vláken (Host a Output), což nám dává naději na úspěch. Zkusme ve Output streamu ponechat pouze jednu hodnotu, u které změníme úplně první skript, který vzdáleně spustíme:

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

Spustíme to takto:

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

a... ANO, vypadá to na vítězství!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Zkusme přijít na to, co se stalo. Lokálně jsme zavolali powershell, což zase zavolalo powershell na vzdáleném počítači a tam spustilo náš skript. Dva proudy (hostitel a výstup) ze vzdáleného stroje byly serializovány a předány zpět, zatímco výstupní proud, který měl v sobě jedinou digitální hodnotu, byl převeden na typ Int32 a jako takový předán přijímací straně a přijímající strana jej používala. jako výstupní kód powershell volajícího.

A jako poslední kontrolu vytvořte jednokrokovou úlohu na serveru SQL s typem „Operating system (cmdexec)“ s následujícím textem:

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

HURÁ! Úloha dokončena s chybou, text v protokolu:

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

Závěry:

  • Vyhněte se používání Write-Output a zadávání výrazů bez přiřazení. Uvědomte si, že přesunutí tohoto kódu jinam ve skriptu může způsobit neočekávané výsledky.
  • Ve skriptech určených ne pro ruční spouštění, ale pro použití ve vašich automatizačních mechanismech, zejména pro vzdálená volání přes WINRM, provádějte ruční zpracování chyb pomocí Try/Catch a zajistěte, aby při jakémkoli vývoji událostí tento skript odeslal právě jednu hodnotu primitivního typu. . Pokud chcete získat klasický Errorlevel, tato hodnota musí být číselná.

Zdroj: www.habr.com

Přidat komentář