Při vytváření vlastní metodiky pro správu záloh na více serverech MS-SQL jsem strávil spoustu času studiem mechanismu předávání hodnot v Powershell během vzdálených hovorů, takže si píšu připomenutí pro případ, že by to bylo užitečné někomu jinému.
Začněme tedy jednoduchým skriptem a spusťte jej lokálně:
$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Ke spouštění skriptů použiji následující soubor CMD, nebudu jej zahrnout pokaždé:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%
Na obrazovce uvidíme následující:
Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1
Nyní spusťte stejný skript přes WSMAN (vzdáleně):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
A tady je výsledek pro vás:
Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0
Skvělé, Errorlevel někam zmizel, ale potřebujeme získat hodnotu ze skriptu! Zkusme následující konstrukci:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
To je ještě zajímavější. Zpráva ve výstupu někam zmizela:
Out to host.
ExitCode: 2
ERRORLEVEL=0
Nyní, jako lyrická odbočka, poznamenám, že pokud uvnitř funkce Powershell napíšete Write-Output nebo jen výraz, aniž byste jej přiřadili jakékoli proměnné (a to implicitně implikuje výstup do výstupního kanálu), pak i když běží lokálně, na obrazovce se nic nezobrazí! Je to důsledek architektury powershell pipeline – každá funkce má svůj Output pipeline, je pro ni vytvořeno pole a vše, co do něj vstoupí, je považováno za výsledek provedení funkce, operátor Return přičte návratovou hodnotu ke stejnému potrubí jako poslední prvek a předá řízení volající funkci. Pro ilustraci spusťte lokálně následující skript:
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: "+$_) }
A tady je výsledek:
Main: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Main: Function: ParameterValue
Main: ReturnValue
Hlavní funkce (tělo skriptu) má také svůj výstupní kanál, a pokud spustíme první skript z CMD, přesměrování výstupu do souboru,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
pak uvidíme na obrazovce
ERRORLEVEL=1
a v souboru
Out to host.
Out to output.
ExitCode: 1
1
pokud provedeme podobné volání z powershell
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt
pak to bude na obrazovce
Out to host.
ExitCode: 1
a v souboru
Out to output.
1
Děje se tak proto, že CMD spustí powershell, který při absenci jiných instrukcí smíchá dvě vlákna (Host a Output) a předá je CMD, které vše, co přijalo, odešle do souboru a v případě spuštění z powershell, tato dvě vlákna existují samostatně a přesměrování symbolů ovlivňuje pouze výstup.
Vrátíme-li se k hlavnímu tématu, připomeňme, že objektový model .NET uvnitř powershell plně existuje v rámci jednoho počítače (jednoho OS), při vzdáleném spouštění kódu přes WSMAN dochází k přenosu objektů pomocí XML serializace, což přináší spoustu zajímavostí navíc k našemu výzkumu. Pokračujme v experimentech spuštěním následujícího kódu:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
A toto máme na obrazovce:
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
Skvělý výsledek! Znamená to, že při volání Invoke-Command je zachováno rozdělení pipelines do dvou vláken (Host a Output), což nám dává naději na úspěch. Zkusme ve Output streamu ponechat pouze jednu hodnotu, u které změníme úplně první skript, který vzdáleně spustíme:
$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Spustíme to takto:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
a... ANO, vypadá to na vítězství!
Out to host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4
Zkusme přijít na to, co se stalo. Lokálně jsme zavolali powershell, což zase zavolalo powershell na vzdáleném počítači a tam spustilo náš skript. Dva proudy (hostitel a výstup) ze vzdáleného stroje byly serializovány a předány zpět, zatímco výstupní proud, který měl v sobě jedinou digitální hodnotu, byl převeden na typ Int32 a jako takový předán přijímací straně a přijímající strana jej používala. jako výstupní kód powershell volajícího.
A jako poslední kontrolu vytvořte jednokrokovou úlohu na serveru SQL s typem „Operating system (cmdexec)“ s následujícím textem:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"
HURÁ! Úloha dokončena s chybou, text v protokolu:
Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6. Код завершения процесса 6. Шаг завершился с ошибкой.
Závěry:
- Vyhněte se používání Write-Output a zadávání výrazů bez přiřazení. Uvědomte si, že přesunutí tohoto kódu jinam ve skriptu může způsobit neočekávané výsledky.
- Ve skriptech určených ne pro ruční spouštění, ale pro použití ve vašich automatizačních mechanismech, zejména pro vzdálená volání přes WINRM, provádějte ruční zpracování chyb pomocí Try/Catch a zajistěte, aby při jakémkoli vývoji událostí tento skript odeslal právě jednu hodnotu primitivního typu. . Pokud chcete získat klasický Errorlevel, tato hodnota musí být číselná.
Zdroj: www.habr.com