Durante la creazione di un proprio metodo di gestione dei backup su più server MS-SQL, ho speso molto tempo a studiare il meccanismo di trasmissione dei valori in powershell durante le chiamate remote, quindi scrivo questa nota per me stesso, nel caso possa servire anche a qualcun altro.
Quindi, iniziamo con uno script semplice e lanciamolo localmente:
$exitcode = $args[0]
Write-Host 'In uscita verso l'host.'
Write-Output 'In uscita verso l'output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)Per l'esecuzione degli script utilizzerò il seguente file CMD, non lo riporterò ogni volta:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%Sullo schermo vedremo il seguente output:
In uscita verso l'host.
In uscita verso l'output.
ExitCode: 1
1
ERRORLEVEL=1
Ora eseguiamo lo stesso script tramite WSMAN (remotamente):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Ed ecco il risultato:
In uscita verso l'host.
In uscita verso l'output.
ExitCode: 2
2
ERRORLEVEL=0Ottimo, Errorlevel è sparito, ma abbiamo bisogno di ottenere un valore dallo script! Proviamo la seguente costruzione:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Qui diventa ancora più interessante. L'intero output in Output è scomparso:
In uscita verso l'host.
ExitCode: 2
ERRORLEVEL=0Ora, come digressione, segnalo che se all'interno di una funzione Powershell scrivete Write-Output o semplicemente un'espressione senza assegnarla a una variabile (cosa che implica implicitamente l'output nel canale Output), anche durante l'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 finisce viene considerato il risultato dell'esecuzione della funzione, l'operatore Return aggiunge il valore restituito come ultimo elemento a quella pipeline e trasferisce 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 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 la sua pipeline Output, e se eseguiamo il primo script da CMD, reindirizzando l'output a un file,
PowerShell .TestOutput1.ps1 1 > TestOutput1.txt
vedremo sullo schermo
ERRORLEVEL=1e nel file
Out to host.
Out to output.
ExitCode: 1
1
se invece facciamo 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 altre 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 di avvio da PowerShell questi due flussi esistono separati, e il simbolo di reindirizzamento influisce solo su Output.
Tornando al tema principale, ricordiamo che il modello ad oggetti .NET all'interno di PowerShell esiste pienamente nell'ambito di un singolo computer (un'unica OS). Quando si esegue codice da remoto tramite WSMAN, la trasmissione degli oggetti avviene tramite XML-serializzazione, il che aggiunge molto interesse ai nostri studi. 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:
Out to host.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Impossibile convertire l'argomento "exitCode", con valore: "System.Object[]", per "SetShouldExit" nel tipo "System.Int32": "Impossibile convertire il valore "System.Object[]" di tipo "System.Object[]" in tipo "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 carattere:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : Non specificato: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0Risultato fantastico! Significa che quando chiamiamo Invoke-Command, viene mantenuta la separazione delle pipeline in due flussi (Host e Output), il che ci dà speranza di successo. Proviamo a lasciare nel flusso Output solo un valore, per questo modificheremo il primo script che eseguiamo da remoto:
$exitcode = $args[0]
Write-Host 'Out to host.'
#Write-Output 'Out to output.'
Write-Host ('ExitCode: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)
Eseguiamolo così:
$res=Invoke-Command -ComputerName . -ScriptBlock { '&D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
e… SÌ, sembra una vittoria!
Out to host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Cerchiamo di capire cosa è successo. Abbiamo invocato localmente PowerShell, che a sua volta ha invocato PowerShell su un computer remoto ed eseguito il nostro script lì. Due flussi (Host e Output) dalla macchina remota sono stati serializzati e inviati nuovamente, mentre il flusso Output, se conteneva un valore numerico, è stato convertito in Int32 e trasmesso alla parte ricevente, che lo ha utilizzato come codice di uscita per il PowerShell chiamante.
E come ultima verifica, creiamo su server un'attività SQL a un passo con 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 registro:
Eseguito a nome dell'utente: DOMAINagentuser. Output su host. ExitCode: 6. Codice di uscita del processo 6. Passo terminato con errore.
Conclusioni:
- Evita di usare Write-Output e di indicare espressioni senza assegnazione. Ricorda che spostare questo codice in un'altra parte dello script può portare a risultati inaspettati.
- Negli script non destinati a essere eseguiti manualmente, ma per l'uso nei tuoi meccanismi di automazione, specialmente per le chiamate remote tramite WINRM, effettua una gestione degli errori manuale tramite Try/Catch e assicurati che in ogni caso questo script invii nel flusso Output esattamente un valore di tipo primitivo. Se desideri ottenere un classico Errorlevel, questo valore deve essere numerico.
Fonte: habr.com
