Bei der Entwicklung einer eigenen Backup-Management-Methode für mehrere MS-SQL-Server habe ich viel Zeit mit dem Verständnis des Wertes übertragenden Mechanismus in PowerShell bei Remoteaufrufen verbracht. Daher schreibe ich mir eine Erinnerungsnotiz, falls es jemand anderem nützlich sein sollte.
Lassen Sie uns zunächst ein einfaches Skript nehmen und es lokal ausführen:
$exitcode = $args[0]
Write-Host 'Ausgabe an den Host.'
Write-Output 'Ausgabe an die Ausgabe.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)Um Skripte auszuführen, werde ich die folgende CMD-Datei verwenden, die ich nicht jedes Mal wiederholen möchte:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%Auf dem Bildschirm sehen wir Folgendes:
Ausgabe an den Host.
Ausgabe an die Ausgabe.
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:
Ausgabe an den Host.
Ausgabe an die Ausgabe.
ExitCode: 2
2
ERRORLEVEL=0Wunderbar, der Errorlevel ist verschwunden, aber wir müssen doch das Ergebnis aus dem Skript erhalten! Wir probieren die folgende Konstruktion:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Hier wird es noch interessanter. Die gesamte Ausgabe ist irgendwo verschwunden:
Ausgabe an den Host.
ExitCode: 2
ERRORLEVEL=0Als eine kleine Anmerkung möchte ich 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), beim lokalen Ausführen nichts auf dem Bildschirm angezeigt wird! Dies ist eine Konsequenz der Pipeline-Architektur von Powershell – jede Funktion hat ihre eigene Output-Pipeline, für die ein Array erstellt wird, und alles, was in dieses Array gelangt, wird als Ergebnis der Funktionsausführung betrachtet. Der Return-Operator fügt den Rückgabewert als letztes Element in diese Pipeline ein und übergibt die Kontrolle an die aufrufende Funktion. Zum besseren Verständnis führen wir nun folgendes Skript lokal aus:
Function Write-Log {
Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
Write-Output ("Funktion: "+$OutString)
Return "Rückgabewert"
}
Write-Output ("Haupt: "+"Parameterwert")
$res = Write-Log "Parameterwert"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Haupt: "+$_) }
Und hier ist das Ergebnis:
Haupt: Parameterwert
IstÖffentlich IstSerial Name BasisTyp
-------- -------- ---- --------
True True Object[] System.Array
2
Haupt: Funktion: Parameterwert
Haupt: RückgabewertDie Hauptfunktion (Körper des Skripts) hat ebenfalls ihre eigene Output-Pipeline, und wenn wir das erste Skript aus der CMD ausführen und die Ausgabe in eine Datei umleiten,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
sehen wir auf dem Bildschirm
ERRORLEVEL=1und in der Datei
Out to host.
Out to output.
ExitCode: 1
1
Wenn wir jedoch einen ähnlichen Aufruf aus PowerShell tätigen,
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txtsehen wir auf dem Bildschirm
Out to host.
ExitCode: 1und in der Datei
Out to output.
1Das passiert, weil CMD PowerShell startet, welches ohne weitere Anweisungen die beiden Streams (Host und Output) mischt und diese an CMD übergibt, das alles, was es erhält, in die Datei schreibt. Bei einem Start aus PowerShell existieren diese beiden Streams separat, und das Umleitungszeichen beeinflusst nur den Output.
Um zur Hauptthema zurückzukehren, erinnern wir uns, dass das objektorientierte Modell .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 unser Experimentieren weiter bereichert. Lassen Sie uns mit dem folgenden Code fortfahren:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
Und das sehen wir auf dem Bildschirm:
Ausgabe an den Host.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Argument "exitCode" mit dem Wert: "System.Object[]" kann nicht in den Typ "System.Int32" konvertiert werden: "Der Wert "System.Object[]" vom Typ "System.Object[]" kann nicht in den Typ "System.Int32" konvertiert werden."
D:sqlagentTestOutput3.ps1:3 Zeichen:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ Kategorieinfo : Nicht angegeben: (:) [], MethodException
+ Vollständig qualifizierte Fehler-ID : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Ausgezeichnetes Ergebnis! Es bedeutet, dass bei der Verwendung von Invoke-Command die Pipeline in zwei Ströme (Host und Output) geteilt wird, was uns Hoffnung auf Erfolg gibt. Lassen Sie uns versuchen, im Output-Stream nur einen Wert zu belassen, indem wir das allererste Skript, das wir remote ausführen, ändern:
$exitcode = $args[0]
Write-Host 'Ausgabe an den Host.'
#Write-Output 'Ausgabe an den 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!
Ausgabe an den Host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Lassen Sie uns versuchen, herauszufinden, was passiert ist. Wir haben lokal PowerShell aufgerufen, das wiederum PowerShell auf dem entfernten Computer aufgerufen hat, und unser Skript dort ausgeführt. Zwei Streams (Host und Output) von dem entfernten System wurden serialisiert und zurückgesendet. Dabei wurde der Output-Stream, wenn er einen digitalen Wert enthielt, in den Typ Int32 umgewandelt und so an die empfangende Seite übertragen. Die empfangende Seite nutzte diesen als Rückgabecode des aufrufenden PowerShells.
Und als letzte Überprüfung erstellen wir auf Server SQL einen Schrittauftrag vom 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 Auftrag wurde mit einem Fehler abgeschlossen, der Text im Protokoll:
Wird im Namen des Benutzers ausgeführt: DOMAINagentuser. Aus zu Host. ExitCode: 6. Prozess Exitcode 6. Der Schritt ist mit einem Fehler beendet.
Fazit:
- Vermeiden Sie die Verwendung von Write-Output und das Angeben von Ausdrücken ohne Zuweisung. Denken Sie daran, dass das Verschieben dieses Codes an eine andere Stelle im Skript unerwartete Ergebnisse verursachen kann.
- In Skripten, die nicht für die manuelle Ausführung, sondern für die Nutzung in Ihren Automatisierungsmechanismen, insbesondere für Remote-Aufrufe über WINRM gedacht sind, sollten Sie eine manuelle Fehlerbehandlung über Try/Catch implementieren und sicherstellen, dass das Skript bei jedem Szenario genau einen Wert des primitiven Typs an den Output-Stream sendet. Wenn Sie einen klassischen Errorlevel erhalten möchten, muss dieser Wert numerisch sein.
Quelle: habr.com
