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