Връщане на стойност от 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

в файла

Изход към хост.
Изход към изхода.
Код за изход: 1
1

ако направим аналогичен повик от powershell

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

на екрана ще бъде

Изход към хост.
Код за изход: 1

в файла

Изход към изхода.
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)

И ето какво имаме на екрана:

Изход към хост.

Код за изход: 3

IsPublic IsSerial Име                                   Основен тип
-------- -------- ----                                   --------
True     True     Object[]                               System.Array
Не може да се преобразува аргумент "exitCode" със стойност: "System.Object[]" в тип "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 'Изход към хост.'
#Write-Output 'Изход към изхода.'
Write-Host ('Код за изход: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Ще го стартираме така:

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

и… ДА, изглежда, че е победа!

Изход към хост.
Код за изход: 4

IsPublic IsSerial Име                                   Основен тип
-------- -------- ----                                   --------
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. Изход към хоста. ExitCode: 6. Кодът за завършване на процеса 6. Стъпката приключи с грешка.

Изводи:

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

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster