Когато създавах собствен метод за управление на резервни копия на множество 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]Това е още по-интересно. Цялото изходно съобщение е изчезнало:
Out to host.
ExitCode: 2
ERRORLEVEL=0Като странична бележка, ако напишете Write-Output или просто израз вътре в PowerShell функция, без да го присвоите на променлива (което имплицитно предполага изход към 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 pipeline и ако изпълним първия скрипт от 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) от отдалечената машина бяха сериализирани и върнати обратно. Изходният поток, който съдържаше една числова стойност, беше преобразуван в 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 и се уверете, че скриптът изпраща точно една примитивна стойност към изходния поток при всички обстоятелства. Ако искате да получите класическото ниво на грешка (Errorlevel), тази стойност трябва да е числова.
Източник: www.habr.com
