Zwracanie wartości z polecenia powershell invoke-command do agenta SQL-Server

Tworząc własną metodologię zarządzania kopiami zapasowymi na wielu serwerach MS-SQL, poświęciłem mnóstwo czasu na naukę mechanizmu przekazywania wartości w PowerShell podczas zdalnych wywołań, dlatego piszę sobie notatkę, może komuś innemu się przyda.

Zacznijmy od najprostszego skryptu i uruchomimy go lokalnie:

$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Do uruchamiania skryptów będę korzystał z następującego pliku CMD, nie będę go przytaczał za każdym razem:

@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%

Na ekranie zobaczymy następujące:

Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1


Teraz uruchomimy ten sam skrypt za pomocą WSMAN (zdalnie):

Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

I oto wynik:

Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0

Wspaniale, Errorlevel gdzieś zniknął, ale potrzebujemy przecież uzyskać wartość ze skryptu! Spróbujmy następnej konstrukcji:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]

Tutaj jest jeszcze ciekawiej. Cały wynik w Output gdzieś zniknął:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Teraz jako liryczny dygresję zaznaczam, że jeśli wewnątrz funkcji PowerShell napiszesz Write-Output lub po prostu wyrażenie bez przypisania go do jakiejkolwiek zmiennej (co niejawnie sugeruje wyjście do kanału Output), to nawet przy lokalnym uruchomieniu nic nie zostanie wyświetlone na ekranie! To rezultat architektury potokowej PowerShell – każda funkcja ma własny potok Output, dla niego tworzona jest tablica, a wszystko, co się w niej znajdzie, uznawane jest za wynik wykonania funkcji, operator Return dodaje wartość zwróconą jako ostatni element do tego samego potoku i przekazuje kontrolę do wywołującej funkcji. Dla ilustracji lokalnie wykonajmy następujący skrypt:

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: "+$_) }

I oto wynik:

Main: ParameterValue

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Main: Function: ParameterValue
Main: ReturnValue

Główna funkcja (ciało skryptu) również ma swój potok Output, a jeśli uruchomimy pierwszy skrypt z CMD, przekierowując wyjście do pliku,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

to na ekranie zobaczymy

ERRORLEVEL=1

a w pliku

Wyjście do hosta.
Wyjście do output.
ExitCode: 1
1

jeśli jednak wykonamy analogiczne wywołanie z powershell

PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txt

to na ekranie będzie

Wyjście do hosta.
ExitCode: 1

a w pliku

Wyjście do output.
1

Dzieje się tak, ponieważ CMD uruchamia powershell, który przy braku innych wskazówek miesza dwa strumienie (Host i Output) i przekazuje je do CMD, który zapisuje wszystko, co otrzymał, a w przypadku uruchomienia z powershell, te dwa strumienie istnieją osobno, a symbol przekierowania wpływa tylko na Output.

Wracając do głównego tematu, przypomnijmy, że model obiektowy .NET w powershell w pełni istnieje w ramach jednego komputera (jednego systemu operacyjnego), przy zdalnym uruchamianiu kodu przez WSMAN, przesyłanie obiektów odbywa się za pomocą serializacji XML, co wnosi wiele dodatkowego zainteresowania w nasze badania. Kontynuujmy eksperymenty uruchamiając następujący kod:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)

I oto co mamy na ekranie:

Wyjście do hosta.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Nie można przekonwertować argumentu "exitCode", o wartości: "System.Object[]", dla "SetShouldExit" na typ "System.Int32": "Nie można przekonwertować wartość "System.Object[]" typu "System.Object[]" na typ "System.Int32".
D:sqlagentTestOutput3.ps1:3 znak:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Wspaniały wynik! Oznacza to, że przy wywołaniu Invoke-Command zachowuje się podział potoków na dwa strumienie (Host i Output), co daje nam nadzieję na sukces. Spróbujmy zostawić w potoku Output tylko jedną wartość, w tym celu zmienimy pierwszy skrypt, który uruchamiamy zdalnie:

$exitcode = $args[0]
Write-Host 'Wyjście do hosta.'
#Write-Output 'Wyjście do output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Uruchomimy go tak:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)

i… TAK, wygląda na to, że to jest zwycięstwo!

Wyjście do hosta.
ExitCode: 4

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Spróbujmy zrozumieć, co się stało. Uruchomiliśmy lokalnie PowerShell, który z kolei uruchomił PowerShell na zdalnym komputerze i wykonał nasz skrypt. Dwa strumienie (Host i Output) z zdalnej maszyny zostały zserializowane i przesłane z powrotem, przy czym strumień Output, jeśli zawierał jedną wartość liczbową, został przekształcony na typ Int32 i w tej formie przesłany do strony odbierającej, która użyła go jako kodu zakończenia wywołującego PowerShell.

A jako ostatnie sprawdzenie utworzymy na serwerze zadanie SQL z jednym krokiem typu „System operacyjny (cmdexec)” z takim tekstem:

PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"

HURA! Zadanie zakończyło się błędem, tekst w dzienniku:

Wykonywane przez użytkownika: DOMAINagentuser. Wyjście do hosta. ExitCode: 6. Kod zakończenia procesu 6. Krok zakończył się błędem.

Wnioski:

  • Unikaj używania Write-Output i wskazywania wyrażeń bez przypisania. Pamiętaj, że przeniesienie tego kodu w inne miejsce skryptu może prowadzić do nieoczekiwanych rezultatów.
  • W skryptach przeznaczonych nie do ręcznego uruchamiania, a do użycia w Twoich mechanizmach automatyzacji, szczególnie do zdalnych wywołań przez WINRM, zapewnij ręczne przetwarzanie błędów przez Try/Catch i zadbaj o to, aby w każdej sytuacji ten skrypt zwracał dokładnie jedną wartość prymitywnego typu w strumieniu Output. Jeśli chcesz uzyskać klasyczny Errorlevel — ta wartość musi być numeryczna.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster