When creating my own backup management method across multiple MS-SQL servers, I spent a lot of time studying the mechanism for passing values in PowerShell during remote calls, so I'm writing this note for myself, in case it might be useful to someone else.
So, let's start with the simplest script and run it locally:
$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)To run scripts, I'll use the following CMD file, I won't show it every time:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%On the screen we will see the following:
Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1
Now let's run the same script through WSMAN (remotely):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]And here is the result:
Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0Wonderful, the Errorlevel is missing somewhere, but we need to get the value from the script! Let's try the following construct:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]This is even more interesting. The entire output in Output seems to have disappeared:
Out to host.
ExitCode: 2
ERRORLEVEL=0Now, as a lyrical digression, I should note that if you write Write-Output inside a PowerShell function or just an expression without assigning it to any variable (which implicitly implies outputting to the Output channel), nothing will be displayed to the screen even during a local run! This is a result of PowerShell's pipeline architecture — each function has its own Output pipeline, an array is created for it, and everything that ends up in it is considered the function's execution result; the Return operator adds the returned value to this pipeline as its last element and passes control back to the calling function. To illustrate, let's run the following script locally:
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: "+$_) }
And here is its result:
Main: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Main: Function: ParameterValue
Main: ReturnValueFuncția principală (corpul scriptului) are, de asemenea, propriul său pipeline de ieșire, iar dacă vom rula primul script din CMD, redirecționând ieșirea într-un fișier,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
atunci pe ecran vom vedea
ERRORLEVEL=1iar în fișier
Iesire către gazdă.
Iesire către ieșire.
ExitCode: 1
1
dacă facem o apelare similară din PowerShell
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txtatunci pe ecran va fi
Iesire către gazdă.
ExitCode: 1iar în fișier
Iesire către ieșire.
1Acest lucru se întâmplă deoarece CMD rulează PowerShell, care, în lipsa altor indicații, combină cele două fluxuri (Gazdă și Ieșire) și le dă către CMD, care trimite în fișier tot ce a primit, iar în cazul în care rulăm din PowerShell aceste două fluxuri există separat, iar simbolul de redirecționare afectează doar Ieșirea.
Întorcându-ne la tema principală, să ne amintim că modelul obiectelor .NET în interiorul PowerShell există complet într-un singur computer (un singur sistem de operare), iar în cazul executării codului de la distanță prin WSMAN, trimiterea obiectelor se face prin serializare XML, ceea ce aduce mult interes suplimentar în cercetările noastre. Vom continua experimentele rulând următorul cod:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
Și iată ce avem pe ecran:
Iesire către gazdă.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Nu se poate converti argumentul "exitCode", cu valoarea: "System.Object[]", pentru "SetShouldExit" la tipul "System.Int32": "Nu se poate converti valoarea "System.Object[]" tip "System.Object[]" la tipul "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 caracter:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Un rezultat minunat! Acesta înseamnă că, atunci când apelăm Invoke-Command, se păstrează separarea pipeline-ului în două fluxuri (Gazdă și Ieşire), ceea ce ne dă speranțe de succes. Să încercăm să lăsăm în fluxul Ieşire doar o singură valoare, pentru care vom modifica cel mai simplu script pe care îl rulăm de la distanță:
$exitcode = $args[0]
Write-Host 'Iesire către gazdă.'
#Write-Output 'Iesire către ieșire.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Să-l rulăm astfel:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
și… DA, se pare că aceasta este o victorie!
Iesire către gazdă.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Să încercăm să înțelegem ce s-a întâmplat. Am apelat local PowerShell, care a apelat la rândul său PowerShell pe computerul la distanță și a executat scriptul nostru acolo. Două fluxuri (Host și Output) din mașina la distanță au fost serializate și trimise înapoi, fluxul Output, în cazul în care conținea o valoare numerică, a fost convertit la tipul Int32 și astfel a fost transmis părții primitoare, care l-a folosit ca cod de ieșire pentru PowerShell-ul apelant.
Și ca ultimă verificare, vom crea pe server o sarcină SQL dintr-un singur pas de tip „Sistem de operare (cmdexec)” cu următorul text:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"URA! Sarcina s-a finalizat cu o eroare, textul din jurnal:
Se execută în numele utilizatorului: DOMAINagentuser. Ieșire către gazdă. ExitCode: 6. Codul de ieșire al procesului 6. Pasul s-a finalizat cu o eroare.
Concluzii:
- Evitați utilizarea Write-Output și specificarea expresiilor fără atribuire. Amintiți-vă că mutarea acestui cod în altă parte a scriptului poate duce la rezultate neașteptate.
- În scripturile care nu sunt destinate rulării manuale, ci pentru utilizare în mecanismele dvs. de automatizare, în special pentru apeluri la distanță prin WINRM, asigurați-vă că faceți gestionarea manuală a erorilor prin Try/Catch și asigurați-vă că, indiferent de evoluția evenimentelor, acest script trimite în fluxul Output exact o valoare de tip simplu. Dacă doriți să obțineți un Errorlevel clasic - această valoare trebuie să fie numerică.
Sursa: habr.com
