Rückgabe von Werten aus PowerShell invoke-command an den SQL-Server-Agenten

Bei der Erstellung meiner eigenen Backup-Management-Methode für mehrere MS-SQL-Server habe ich viel Zeit mit dem Studium des Währungsmechanismus in PowerShell bei Remoteaufrufen verbracht, deshalb schreibe ich mir selbst diese Gedächtnisstütze, vielleicht ist sie auch für jemand anderen nützlich.

Lassen Sie uns zunächst ein einfaches Skript nehmen und es lokal ausführen:

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

Für die Ausführung der Skripts werde ich die folgende CMD-Datei verwenden, die ich nicht jedes Mal vorschreibe:

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

Auf dem Bildschirm sehen wir Folgendes:

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


Jetzt führen wir dasselbe Skript über WSMAN (remote) aus:

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

Und hier ist das Ergebnis:

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

Wunderbar, das Errorlevel ist verschwunden, aber wir müssen den Wert aus dem Skript erhalten! Versuchen wir die folgende Konstruktion:

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

Hier wird es noch interessanter. Der gesamte Output ist irgendwo verschwunden:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Jetzt möchte ich als lyrischen Einschub erwähnen, dass wenn Sie innerhalb einer PowerShell-Funktion Write-Output oder einfach einen Ausdruck ohne Zuweisung an eine Variable schreiben (was implizit die Ausgabe in den Output-Kanal bedeutet), selbst bei der lokalen Ausführung nichts auf dem Bildschirm ausgegeben wird! Das ist eine Folge der Pipeline-Architektur von PowerShell — jede Funktion hat ihre eigene Output-Pipeline, für die ein Array erstellt wird, und alles, was dorthin gelangt, wird als Ergebnis der Funktionsausführung betrachtet, der Return-Befehl fügt den Rückgabewert als letztes Element in diese Pipeline ein und überträgt die Kontrolle an die aufrufende Funktion. Zum Veranschaulichen führen wir lokal das folgende Skript aus:

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

Und hier ist das Ergebnis:

Main: ParameterValue

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

Die Hauptfunktion (der Skriptkörper) hat auch ihre eigene Output-Pipeline, und wenn wir das erste Skript aus CMD ausführen und die Ausgabe in eine Datei umleiten,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

Dann werden wir auf dem Bildschirm sehen

ERRORLEVEL=1

und in der Datei

Ausgabe an Host.
Ausgabe an Ergebnis.
ExitCode: 1
1

wenn wir jedoch einen ähnlichen Aufruf aus PowerShell machen

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

dann wird auf dem Bildschirm

Ausgabe an Host.
ExitCode: 1

und in der Datei

Ausgabe an Ergebnis.
1

Das geschieht, weil CMD PowerShell startet, das ohne weitere Anweisungen die beiden Ströme (Host und Output) mischt und CMD alles, was es erhält, in die Datei schreibt. Wenn jedoch aus PowerShell gestartet wird, existieren diese beiden Ströme separat, und das Umleitungssymbol beeinflusst nur den Output.

Zurück zum Hauptthema, erinnern wir uns, dass das Objektmodell .NET innerhalb von PowerShell vollständig auf einem Computer (einem Betriebssystem) existiert. Bei der Remote-Ausführung von Code über WSMAN erfolgt die Übertragung von Objekten über XML-Serialisierung, was viel zusätzliches Interesse an unseren Forschungen mit sich bringt. Lassen Sie uns die Experimente mit folgendem Code fortsetzen:

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

Und das haben wir auf dem Bildschirm:

Ausgabe an Host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Kann das Argument "exitCode" mit dem Wert: "System.Object[]" für "SetShouldExit" nicht in den Typ "System.Int32" konvertieren: "Kann den Wert "System.Object[]" vom Typ "System.Object[]" nicht in den Typ "System.Int32" konvertieren."
D:sqlagentTestOutput3.ps1:3 Zeichen:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + KategorieInfo          : Nicht angegeben: (:) [], MethodException
    + VollständigQualifizierteFehlerId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Wunderbares Ergebnis! Das bedeutet, dass beim Aufruf von Invoke-Command die Trennung der Pipelines in zwei Ströme (Host und Output) erhalten bleibt, was uns Hoffnung auf Erfolg gibt. Lassen Sie uns versuchen, im Output-Strom nur einen einzigen Wert zu belassen, indem wir das allererste Skript, das wir remote ausführen, ändern:

$exitcode = $args[0]
Write-Host 'Ausgabe an Host.'
#Write-Output 'Ausgabe an Ergebnis.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Lassen Sie uns es so ausführen:

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

und… JA, es scheint ein Sieg zu sein!

Ausgabe an Host.
ExitCode: 4

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


ERRORLEVEL=4

Lass uns herausfinden, was passiert ist. Wir haben lokal PowerShell aufgerufen, das seinerseits PowerShell auf dem Remote-Computer gestartet hat und unser Skript dort ausgeführt hat. Zwei Streams (Host und Output) von der Remote-Maschine wurden serialisiert und zurückgesendet, wobei der Output-Stream, wenn er einen einzelnen Zahlenwert enthielt, in den Typ Int32 umgewandelt und in dieser Form an die empfangende Seite übergeben wurde, die ihn als Rückgabecode des aufrufenden PowerShell-Prozesses verwendet hat.

Und als letzter Test erstellen wir auf Server SQL einen Job aus einem Schritt mit dem Typ „Betriebssystem (cmdexec)“ mit folgendem Text:

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

JUCHU! Der Job wurde mit einem Fehler abgeschlossen, der Text im Protokoll:

Ausgeführt im Namen des Benutzers: DOMAINagentuser. Ausgeben an Host. ExitCode: 6. Der Prozess-Rückgabecode ist 6. Der Schritt wurde mit einem Fehler beendet.

Fazit:

  • Vermeiden Sie die Nutzung von Write-Output und das Angeben von Ausdrücken ohne Zuweisung. Denken Sie daran, dass das Verschieben dieses Codes an einen anderen Ort im Skript zu unerwarteten Ergebnissen führen kann.
  • In Skripten, die nicht für die manuelle Ausführung, sondern für die Verwendung in Ihren Automatisierungsmechanismen, insbesondere für Remote-Aufrufe über WINRM, gedacht sind, sollten Sie die manuelle Fehlerbehandlung über Try/Catch durchführen und sicherstellen, dass dieses Skript unter allen Umständen genau einen primitiven Wert an den Output-Stream sendet. Wenn Sie einen klassischen Errorlevel wünschen - dieser Wert muss numerisch sein.

Quelle: habr.com

60GB SSD 8Gb DDR4