Returning a value from PowerShell invoke-command to SQL Server agent

Når jeg skabte min egen metode til at administrere sikkerhedskopier på flere MS-SQL-servere, brugte jeg en masse tid på at studere mekanismen til at overføre værdier i PowerShell ved afstandsopkald, så jeg skriver denne note til mig selv, måske er der andre, der kan få glæde af den.

Så lad os tage et simpel skript og køre det lokalt:

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

Til at køre skripter vil jeg bruge følgende CMD-fil, som jeg ikke vil gentage hver gang:

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

På skærmen vil vi se følgende:

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


Nu vil vi køre det samme skript via WSMAN (fjerbetjening):

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

Og her er resultatet:

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

Fremragende, Errorlevel er forsvundet, men vi har brug for at få værdien fra skriptet! Lad os prøve følgende konstrukt:

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

Det her er mere interessant. Hele outputsendelsen til Output er forsvundet:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Nu vil jeg bemærke, at hvis du inden for en PowerShell-funktion skriver Write-Output eller blot en udtryk uden at tildele det til nogen variabel (og dette indebærer implicit output til Output-kanalen), så vil der ikke blive udsendt noget til skærmen, selv ved lokal kørsel! Dette er en konsekvens af den pipelinede arkitektur i PowerShell - hver funktion har sin egen Output-pipeline, der oprettes et array til, og alt, hvad der kommer ind i det, betragtes som resultatet af funktionen, Return-operatoren tilføjer det returnerede værdi som det sidste element i denne pipeline og overfører kontrollen til den kaldende funktion. For at illustrere, lad os køre følgende skript lokalt:

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

Og her er resultatet:

Main: ParameterValue

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

Hovedfunktionen (kroppe af skriptet) har også sin egen Output-pipeline, og hvis vi kører det første skript fra CMD ved at omdirigere output til en fil,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

Så på skærmen ser vi

FEJLLEVEL=1

og i filen

Ud til vært.
Ud til output.
ExitCode: 1
1

hvis vi laver et tilsvarende kald fra powershell

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

så på skærmen vil der være

Ud til vært.
ExitCode: 1

og i filen

Ud til output.
1

Dette sker, fordi CMD starter powershell, som i mangel af andre anvisninger blander to strømme (Vært og Output) og sender dem til CMD, som sender alt, hvad den modtog, til filen, mens disse to strømme eksisterer separat ved kørsel fra powershell, og omdirigeringssymbolet kun påvirker Output.

Når vi vender tilbage til hovedemnet, husker vi, at objektmodellen .NET inden for powershell eksisterer fuldt ud på én computer (en OS), og ved fjernudførelse af kode via WSMAN overføres objekterne gennem XML-serialisering, hvilket tilføjer mange ekstra interessante aspekter til vores undersøgelser. Vi fortsætter eksperimenterne ved at køre følgende kode:

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

Og her er, hvad vi har på skærmen:

Ud til vært.

ExitCode: 3

IsPublic IsSerial Navn                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Kan ikke konvertere argumentet "exitCode" med værdien: "System.Object[]" til "SetShouldExit" til typen "System.Int32": "Kan ikke konvertere værdien "System.Object[]" af typen "System.Object[]" til typen "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 tegn:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + KategorieInfo          : Ikke specificeret: (:) [], MethodException
    + FullyQualifiedErrorId : MetodeArgumentKonverteringUgyldigCastingArgument

FEJLLEVEL=0

Fantastisk resultat! Det betyder, at når Invoke-Command kaldes, bevares opdelingen af rørledninger til to strømme (Vært og Output), hvilket giver os håb for succes. Lad os forsøge at efterlade kun én værdi i Output-strømmen ved at ændre det allerførste script, som vi kører fjernmæssigt:

$exitcode = $args[0]
Write-Host 'Ud til vært.'
#Write-Output 'Ud til output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Vi starter det sådan her:

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

og… JA, det ser ud til at være en sejr!

Ud til vært.
ExitCode: 4

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


FEJLLEVEL=4

Lad os forsøge at forstå, hvad der er sket. Vi kaldte lokalt PowerShell, som igen kaldte PowerShell på den fjerde computer og udførte vores script der. To strømme (Host og Output) fra den fjerde maskine blev serialiseret og sendt tilbage, hvor Output-strømmen, hvis den indeholdt et enkelt heltal, blev konverteret til typen Int32 og sendt til den modtagende part, der brugte det som afslutningskode for den kaldende PowerShell.

Og som sidste kontrol vil vi oprette en serveren SQL-opgave fra et enkelt trin af typen "Operativsystem (cmdexec)" med følgende tekst:

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

HUURRA! Opgaven blev afsluttet med en fejl, teksten i loggen:

Kører som bruger: DOMAINagentuser. Ud til vært. ExitCode: 6. Procesens afslutningskode 6. Trinnet mislykkedes.

Konklusioner:

  • Undgå at bruge Write-Output og angive udtryk uden tildeling. Husk, at flytning af denne kode til et andet sted i scriptet kan føre til uventede resultater.
  • I scripts, der ikke er beregnet til manuel kørsel, men til brug i dine automatiseringsmekanismer, især til fjernaftaler via WINRM, sørg for manuel fejlhåndtering via Try/Catch, og sørg for, at dette script sender præcis én værdi af primitiv type til Output-strømmen, uanset hvad der sker. Hvis du ønsker et klassisk Errorlevel, skal dette værdi være numerisk.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster