Zurückgeben eines Werts vom Powershell-Aufrufbefehl an den SQL Server-Agenten

Als ich meine eigene Methodik zum Verwalten von Backups auf mehreren MS-SQL-Servern erstellt habe, habe ich viel Zeit damit verbracht, den Mechanismus zur Übergabe von Werten in Powershell bei Remote-Aufrufen zu studieren, deshalb schreibe ich mir eine Erinnerung, falls er nützlich sein sollte an jemand anderen.

Beginnen wir also mit einem einfachen Skript und führen es lokal aus:

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

Um Skripte auszuführen, verwende ich die folgende CMD-Datei, ich werde sie nicht jedes Mal einbinden:

@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


Lassen Sie uns nun dasselbe Skript über WSMAN (remote) ausführen:

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

Großartig, Errorlevel ist irgendwo verschwunden, aber wir müssen den Wert aus dem Skript holen! Probieren wir das folgende Design aus:

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

Das ist noch interessanter. Die Meldung in der Ausgabe ist irgendwo verschwunden:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Als lyrischen Exkurs möchte ich anmerken, dass, wenn Sie innerhalb einer Powershell-Funktion „Write-Output“ oder nur einen Ausdruck schreiben, ohne ihn einer Variablen zuzuweisen (und dies implizit eine Ausgabe an den Ausgabekanal impliziert), auch bei lokaler Ausführung Es wird nichts auf dem Bildschirm angezeigt! Dies ist eine Folge der Powershell-Pipeline-Architektur – jede Funktion hat ihre eigene Ausgabepipeline, für sie wird ein Array erstellt und alles, was darin eingeht, wird als Ergebnis der Funktionsausführung betrachtet, der Return-Operator fügt den Rückgabewert dazu hinzu Pipeline als letztes Element und übergibt die Kontrolle an die aufrufende Funktion. Zur Veranschaulichung führen wir das folgende Skript lokal 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 (Skriptkörper) verfügt auch über eine eigene Ausgabepipeline. Wenn wir das erste Skript über 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

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

wenn wir einen ähnlichen Anruf von Powershell aus tätigen

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

dann wird es auf dem Bildschirm angezeigt

Out to host.
ExitCode: 1

und in der Datei

Out to output.
1

Dies geschieht, weil der CMD Powershell startet, das in Ermangelung anderer Anweisungen zwei Threads (Host und Ausgabe) mischt und sie an den CMD weitergibt, der alles, was es empfangen hat, an eine Datei sendet, und im Falle des Starts von Powershell aus: Diese beiden Threads existieren separat und die Symbolumleitungen wirken sich nur auf die Ausgabe aus.

Zurück zum Hauptthema: Erinnern wir uns daran, dass das .NET-Objektmodell in Powershell vollständig auf einem Computer (einem Betriebssystem) existiert. Wenn Code remote über WSMAN ausgeführt wird, erfolgt die Übertragung von Objekten durch XML-Serialisierung, was viel zusätzliches Interesse mit sich bringt zu unserer Forschung. Lassen Sie uns unsere Experimente fortsetzen, indem wir den folgenden Code ausführen:

$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:

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

Tolles Ergebnis! Das bedeutet, dass beim Aufruf von Invoke-Command die Aufteilung der Pipelines in zwei Threads (Host und Output) beibehalten wird, was uns auf Erfolg hoffen lässt. Versuchen wir, nur einen Wert im Ausgabestream zu belassen, für den wir das allererste Skript ändern, das wir remote 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)

Lassen Sie es uns so ausführen:

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

und... JA, es sieht nach einem Sieg aus!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Versuchen wir herauszufinden, was passiert ist. Wir haben Powershell lokal aufgerufen, was wiederum Powershell auf dem Remote-Computer aufgerufen und dort unser Skript ausgeführt hat. Zwei Streams (Host und Output) vom Remote-Computer wurden serialisiert und zurückgegeben, während der Output-Stream, der einen einzelnen digitalen Wert enthielt, in den Typ Int32 konvertiert und als solcher an die Empfangsseite übergeben wurde, die von der Empfangsseite verwendet wurde als Exit-Code der aufrufenden Powershell.

Und als letzte Kontrolle erstellen wir einen One-Step-Job auf dem SQL-Server 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)"

HURRA! Die Aufgabe wurde mit einem Fehler abgeschlossen, Text im Protokoll:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Schlussfolgerungen:

  • Vermeiden Sie die Verwendung von Write-Output und die Angabe von Ausdrücken ohne Zuweisung. Beachten Sie, dass das Verschieben dieses Codes an eine andere Stelle im Skript zu unerwarteten Ergebnissen führen kann.
  • Führen Sie in Skripten, die nicht für den manuellen Start, sondern für die Verwendung in Ihren Automatisierungsmechanismen gedacht sind, insbesondere für Remote-Aufrufe über WINRM, eine manuelle Fehlerbehandlung über Try/Catch durch und stellen Sie sicher, dass dieses Skript bei jeder Entwicklung von Ereignissen genau einen primitiven Typwert sendet . Wenn Sie den klassischen Errorlevel erhalten möchten, muss dieser Wert numerisch sein.

Source: habr.com

Kommentar hinzufügen