Při vytváření vlastní metodiky správy záloh na mnoha serverech MS-SQL jsem strávil hodně času studiem mechanismu předávání hodnot v PowerShell při vzdálených voláních, proto si píšu poznámku pro sebe, ať se třeba hodí i někomu jinému.
Takže si nejprve vezmeme nejjednodušší skript a spustíme ho 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)K spuštění skriptů budu používat následující CMD soubor, pokaždé ho nebudu uvádět:
@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í spustíme stejný skript přes WSMAN (na dálku):
Invoke-Command -ComputerName . -ScriptBlock { & 'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]A tady máte výsledek:
Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0Skvěle, Errorlevel někam zmizel, ale potřebujeme přece získat hodnotu ze skriptu! Zkoušíme následující konstrukci:
$res=Invoke-Command -ComputerName . -ScriptBlock { & 'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Tady to je ještě zajímavější. Celý výstup do Output někam zmizel:
Out to host.
ExitCode: 2
ERRORLEVEL=0Nyní jako lyrickou odbočku poznamenám, že pokud uvnitř funkce PowerShell napíšete Write-Output nebo prostě výraz bez přiřazení k nějaké proměnné (což implicitně předpokládá výstup do kanálu Output), tak ani při lokálním spuštění se na obrazovce nic nezobrazí! Je to důsledek konvenční architektury PowerShell — každá funkce má svůj vlastní konvenční Output, pro něj se vytváří pole, a všechno, co do něj přichází, je považováno za výsledek provedení funkce, operátor Return přidává vracenou hodnotu jako poslední prvek do tohoto konvenčního kanálu a předává řízení do volající funkce. Pro ilustraci spusťme 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 jeho výsledek:
Main: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Main: Function: ParameterValue
Main: ReturnValueHlavní funkce (tělo skriptu) má také svůj vlastní konvenční Output, a pokud spustíme první skript z CMD, přesměrujeme výstup do souboru,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
na obrazovce uvidíme
ERRORLEVEL=1a v souboru
Out to host.
Out to output.
ExitCode: 1
1
pokud však provedeme obdobné volání z powershellu
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txtna obrazovce bude
Out to host.
ExitCode: 1a v souboru
Out to output.
1To se děje proto, že CMD spouští powershell, který při absenci dalších pokynů míchá dva proudy (Host a Output) a předává je CMD, které zapisuje do souboru vše, co obdrželo; naopak při spuštění z powershellu existují tyto dva proudy odděleně a symbol přesměrování ovlivňuje pouze Output.
Vrátíme-li se k hlavnímu tématu, připomeňme si, že objektní model .NET v rámci powershellu plně existuje na jednom počítači (jednom OS); při vzdáleném spuštění kódu přes WSMAN se přenos objektů odehrává pomocí XML serializace, což přináší mnoho dalších zajímavostí do našich výzkumů. Pokračujme experimenty 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 zde je, co máme na obrazovce:
Out to host.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Nelze převést argument "exitCode" s hodnotou: "System.Object[]" na "SetShouldExit" typu "System.Int32": "Nelze převést hodnotu "System.Object[]" typu "System.Object[]" na typ "System.Int32"."
D:sqlagentTestOutput3.ps1:3 znak:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Skvělý výsledek! Znamená to, že při volání Invoke-Command se zachovává dělení konvojů na dva proudy (Host a Output), což nám dává naději na úspěch. Zkusíme ponechat v proudu Output pouze jednu hodnotu, k čemuž změníme úplně první skript, který spouštíme vzdáleně:
$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 jako vítězství!
Out to host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Pokusíme se pochopit, co se nám stalo. Místně jsme spustili PowerShell, který pak spustil PowerShell na vzdáleném počítači a provedl náš skript. Dva proudy (Host a Output) z vzdáleného stroje byly serializovány a odeslány zpět, přičemž proud Output, pokud obsahoval jednu číselnou hodnotu, byl převeden na typ Int32 a takto předán přijímající straně, která jej použila jako ukončovací kód volajícího PowerShellu.
A jako poslední kontrolu vytvoříme na serveru SQL úkol z jednoho kroku s typem „Operační systém (cmdexec)“ se následujícím textem:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"HURÁ! Úkol byl dokončen s chybou, text v protokolu:
Provádí se jménem uživatele: DOMAINagentuser. Out to host. ExitCode: 6. Ukončovací kód procesu 6. Krok skončil chybou.
Závěry:
- Vyhněte se používání Write-Output a uvádění výrazů bez přiřazení. Pamatujte, že přesun tohoto kódu na jiné místo ve skriptu může vést k nečekaným výsledkům.
- Ve skriptech určených nikoli k ručnímu spuštění, ale k 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 tento skript vždy poslal do proudu Output přesně jednu hodnotu primitivního typu. Pokud chcete získat klasickou Errorlevel - tato hodnota by měla být číselná.
Zdroj: habr.com
