Rückgabewert vom Powershell-Aufrufbefehl an den SQL Server-Agenten

Bei der Erstellung meiner eigenen Backup-Verwaltungsmethode auf vielen MS-SQL-Servern habe ich viel Zeit damit verbracht, den Mechanismus der Übertragung von Werten an Powershell für Remoteaufrufe zu untersuchen. Daher schreibe ich mir selbst ein Memo, das plötzlich für andere nützlich sein wird.

Nehmen wir also zunächst ein einfaches 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)

Zum Ausführen der Skripte verwende ich die folgende CMD-Datei, die ich nicht jedes Mal gebe:

@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

Führen Sie nun dasselbe Skript über WSMAN (remote) aus:

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

Und hier ist das Ergebnis:

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

Wie durch ein Wunder ist Errorlevel irgendwo verschwunden, aber wir müssen den Wert aus dem Skript holen! Wir versuchen folgende Konstruktion:

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

Es ist noch interessanter. Die Nachrichtenausgabe in Ausgabe ist irgendwo verschwunden:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Als lyrischen Exkurs stelle ich fest, dass, wenn Sie Write-Output oder nur einen Ausdruck schreiben, ohne ihn einer Variablen innerhalb der Powershell-Funktion zuzuweisen (und dies impliziert implizit eine Ausgabe an den Ausgabekanal), selbst wenn Sie ihn lokal ausführen, nichts angezeigt wird! Dies ist eine Folge der Powershell-Pipeline-Architektur: Jede Funktion verfügt über eine eigene Output-Pipeline, ein Array wird für sie erstellt und alles, was in sie gelangt, wird als Ergebnis der Funktion betrachtet. Die Return-Anweisung fügt den Rückgabewert derselben Pipeline wie das letzte Element hinzu und überträgt die Steuerung an die aufrufende Funktion. Führen Sie zur Veranschaulichung 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 sein 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 eine eigene Ausgabepipeline. Wenn wir das erste Skript von 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 machen

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


dann wird der Bildschirm sein

Out to host.
ExitCode: 1

und in der Datei

Out to output.
1

Dies liegt daran, dass CMD Powershell startet, das in Abwesenheit anderer Anweisungen die beiden Streams (Host und Output) mischt und an CMD weiterleitet, das alles, was es empfangen hat, an die Datei sendet. Wenn es von Powershell gestartet wird, existieren diese beiden Streams separat und das Symbol Weiterleitungen wirken sich nur auf die Ausgabe aus.

Zurück zum Hauptthema, wir erinnern uns, dass das .NET-Objektmodell in Powershell vollständig im Rahmen eines Computers (eines Betriebssystems) existiert. Wenn Remotecode über WSMAN ausgeführt wird, werden Objekte durch XML-Serialisierung übertragen, was ein großes zusätzliches Interesse für unsere Forschung mit sich bringt. Lassen Sie uns die Experimente fortsetzen, indem Sie den folgenden Code ausführen:

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

Und hier ist, was wir auf dem Bildschirm haben:

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:\sqlagent\TestOutput3.ps1:3 :1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Tolles Ergebnis! Dies bedeutet, dass beim Aufruf von Invoke-Command die Pipeline in zwei Streams (Host und Output) unterteilt wird, was uns Hoffnung auf Erfolg gibt. 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)

Führen Sie es so aus:

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

und ... JA, es scheint ein Sieg zu sein!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Versuchen wir herauszufinden, was mit uns passiert ist. Wir haben lokal Powershell aufgerufen, das wiederum Powershell auf dem Remotecomputer aufgerufen und dort unser Skript ausgeführt hat. Zwei Streams (Host und Output) vom Remote-Computer wurden serialisiert und zurückgesendet, während der Output-Stream, wenn er einen digitalen Wert hatte, in den Int32-Typ konvertiert und in dieser Form an die Empfangsseite gesendet wurde, und die Empfangsseite ihn als anrufenden Exit-Code verwendete Power Shell.

Und als letzte Prüfung erstellen wir auf dem SQL Server eine einstufige Aufgabe vom Typ "Betriebssystem (cmdexec)" mit folgendem Text:

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

HURRA! Die Aufgabe ist fehlgeschlagen, der Text im Protokoll:

   : DOMAIN\agentuser. Out to host. ExitCode: 6.     6.     .

Ergebnisse:

  • Vermeiden Sie die Verwendung von Write-Output und die Angabe von Ausdrücken ohne Zuweisung. Denken Sie daran, 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 vorgesehen sind, sondern für die Verwendung in Ihren Automatisierungsmechanismen, insbesondere für Fernaufrufe über WINRM, eine manuelle Fehlerbehandlung über Try / Catch durch und stellen Sie sicher, dass dieses Skript für die Entwicklung von Ereignissen genau eine Sendung an den Ausgabestream sendet primitiver Typwert. Wenn Sie die klassische Fehlerstufe erhalten möchten, muss dieser Wert numerisch sein.

All Articles