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

в файла

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

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

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

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

Изход към хоста.
ExitCode: 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)

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

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

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 'Изход към хоста.'
#Write-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)

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

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

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