Когато създавах собствена методика за управление на резервни копия на множество 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
