Nel creare il mio metodo di gestione dei backup su molti server MS-SQL, ho speso un sacco di tempo a studiare il meccanismo di passaggio dei valori in PowerShell durante le chiamate remote, quindi scrivo questo promemoria per me stesso, nel caso possa essere utile a qualcun altro.
Cominciamo con uno script semplice e eseguiamolo localmente:
$exitcode = $args[0]
Write-Host 'Out to host.'
Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)Per eseguire gli script userò il seguente file CMD, non lo riporterò ogni volta:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%Sullo schermo vedremo il seguente output:
Out to host.
Out to output.
ExitCode: 1
1
ERRORLEVEL=1
Ora eseguiamo lo stesso script tramite WSMAN (a distanza):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Ecco il risultato:
Out to host.
Out to output.
ExitCode: 2
2
ERRORLEVEL=0Ottimo, l'Errorlevel è scomparso, ma dobbiamo ottenere un valore dallo script! Proviamo la seguente costruzione:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Qui la situazione è ancora più interessante. Tutto l'output scompare:
Out to host.
ExitCode: 2
ERRORLEVEL=0Ora, come parentesi lirica, voglio sottolineare che se all'interno di una funzione PowerShell scrivete Write-Output o semplicemente un'espressione senza assegnarla a una variabile (il che implica implicitamente l'output nel canale Output), anche in esecuzione locale non verrà visualizzato nulla sullo schermo! Questo è il risultato dell'architettura a pipeline di PowerShell: ogni funzione ha la propria pipeline Output, viene creato un array per essa e tutto ciò che vi entra è considerato il risultato dell'esecuzione della funzione. L'operatore Return aggiunge il valore di ritorno a quest'ultima pipeline come ultimo elemento e restituisce il controllo alla funzione chiamante. Per illustrare, eseguiamo localmente il seguente script:
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: "+$_) }
Ed ecco il suo risultato:
Main: ParameterValue
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
2
Main: Function: ParameterValue
Main: ReturnValueLa funzione principale (corpo dello script) ha anche il suo pipeline di output, e se eseguiamo il primo script da CMD, reindirizzando l'output in un file,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
sullo schermo vedremo
ERRORLEVEL=1e nel file
Out to host.
Out to output.
ExitCode: 1
1
se effettuiamo una chiamata analoga da PowerShell
PS D:sqlagent> .TestOutput1.ps1 1 > TestOutput1.txtsullo schermo ci sarà
Out to host.
ExitCode: 1e nel file
Out to output.
1Questo accade perché CMD avvia PowerShell, che in assenza di ulteriori indicazioni mescola i due flussi (Host e Output) e li restituisce a CMD, che invia nel file tutto ciò che ha ricevuto; mentre, nel caso dell'esecuzione da PowerShell, questi due flussi esistono separatamente e il simbolo di reindirizzamento influisce solo su Output.
Tornando al tema principale, ricordiamo che il modello a oggetti .NET all'interno di PowerShell esiste pienamente nell'ambito di un singolo computer (un singolo OS); durante l'esecuzione di codice remoto tramite WSMAN, il trasferimento di oggetti avviene tramite serializzazione XML, il che aggiunge molto interesse alle nostre indagini. Continuiamo gli esperimenti eseguendo il seguente codice:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$res.GetType()
$host.SetShouldExit($res)
Ecco cosa abbiamo sullo schermo:
In uscita per l'hosting.
Codice di uscita: 3
IsPublic IsSerial Nome BaseType
-------- -------- ---- --------
True True Object[] System.Array
Impossibile convertire l'argomento "exitCode", con valore: "System.Object[]", per "SetShouldExit" in tipo "System.Int32": "Impossibile convertire il valore "System.Object[]" di tipo "System.Object[]" in tipo "System.Int32"."
D:sqlagentTestOutput3.ps1:3 posizione:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Risultato eccellente! Significa che, quando si chiama Invoke-Command, si conserva la separazione dei pipeline in due flussi (Host e Output), il che ci dà speranza di successo. Proviamo a mantenere nel flusso Output solo un valore, per cui modificheremo il primo script che eseguiamo in remoto:
$exitcode = $args[0]
Write-Host 'In uscita per l'hosting.'
#Write-Output 'In uscita per l'output.'
Write-Host ('Codice di uscita: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Eseguiamolo in questo modo:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
e… SÌ, sembra una vittoria!
In uscita per l'hosting.
Codice di uscita: 4
IsPublic IsSerial Nome BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Cerchiamo di capire cosa è successo. Abbiamo invocato localmente PowerShell, il quale ha a sua volta chiamato PowerShell su un computer remoto ed eseguito il nostro script lì. Due stream (Host e Output) dalla macchina remota sono stati serializzati e restituiti, mentre lo stream Output, se contenente un valore numerico, è stato convertito in tipo Int32 e passato al destinatario, il quale lo ha utilizzato come codice di uscita per PowerShell.
E come ultima verifica, creeremo su server un'attività SQL in un singolo passaggio di tipo "Sistema operativo (cmdexec)" con il seguente testo:
PowerShell -NonInteractive -NoProfile "$res=Invoke-Command -ComputerName BACKUPSERVER -ConfigurationName SQLAgent -ScriptBlock {&'D:sqlagentTestOutput1.ps1' 6}; $host.SetShouldExit($res)"Evviva! L'attività è terminata con errore, testo nel log:
Eseguito a nome dell'utente: DOMAINagentuser. Output al host. ExitCode: 6. Codice di uscita del processo 6. Passaggio terminato con errore.
Conclusioni:
- Evitare di utilizzare Write-Output e di specificare espressioni senza assegnazione. Ricordate che trasferire questo codice in un'altra parte dello script potrebbe portare a risultati inaspettati.
- Nei script destinati non all'avvio manuale, ma per l'uso nei vostri meccanismi di automazione, in particolare per le chiamate remote tramite WINRM, effettuate una gestione degli errori manuale tramite Try/Catch, assicurandovi che, in qualsiasi situazione, questo script invii nel flusso Output esattamente un valore di tipo primitivo. Se desiderate un classico Errorlevel, questo valore deve essere numerico.
Fonte: habr.com
