Restituzione di un valore dal comando di chiamata di PowerShell all'agente SQL Server

Quando ho creato la mia metodologia per la gestione dei backup su più server MS-SQL, ho passato molto tempo a studiare il meccanismo di passaggio dei valori in Powershell durante le chiamate remote, quindi mi scrivo un promemoria nel caso fosse utile a qualcun altro.

Quindi, iniziamo con un semplice script ed 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, utilizzerò il seguente file CMD, non lo includerò ogni volta:

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

Sullo schermo vedremo quanto segue:

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


Ora eseguiamo lo stesso script tramite WSMAN (da remoto):

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

E questo è il risultato:

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

Ottimo, Errorlevel è scomparso da qualche parte, ma dobbiamo ottenere il valore dallo script! Proviamo il seguente disegno:

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

Questo è ancora più interessante. Il messaggio nell'output è scomparso da qualche parte:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Ora, come digressione lirica, noterò che se all'interno di una funzione Powershell scrivi Write-Output o semplicemente un'espressione senza assegnarla a nessuna variabile (e questo implica implicitamente l'output sul canale di output), quindi anche quando eseguito localmente, sullo schermo non verrà visualizzato nulla! Questa è una conseguenza dell'architettura della pipeline PowerShell: ogni funzione ha la propria pipeline di output, viene creato un array per essa e tutto ciò che entra in esso è considerato il risultato dell'esecuzione della funzione, l'operatore Return aggiunge il valore restituito allo stesso pipeline come ultimo elemento 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: "+$_) }

E questo è il risultato:

Main: ParameterValue

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

Anche la funzione principale (corpo dello script) ha una propria pipeline di output e se eseguiamo il primo script da CMD, reindirizzando l'output su un file,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

poi vedremo sullo schermo

ERRORLEVEL=1

e nel fascicolo

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

se facciamo una chiamata simile da PowerShell

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

quindi sarà sullo schermo

Out to host.
ExitCode: 1

e nel fascicolo

Out to output.
1

Questo accade perché il CMD lancia powershell, il quale, in assenza di altre istruzioni, mischia due thread (Host e Output) e li consegna al CMD, che invia tutto ciò che ha ricevuto in un file, e nel caso di lancio da powershell, questi due thread esistono separatamente e i reindirizzamenti dei simboli influiscono solo sull'output.

Tornando all'argomento principale, ricordiamo che il modello a oggetti .NET all'interno di PowerShell esiste completamente all'interno di un computer (un sistema operativo), quando si esegue codice in remoto tramite WSMAN, il trasferimento di oggetti avviene tramite serializzazione XML, il che porta molto ulteriore interesse alla nostra ricerca. Continuiamo i nostri esperimenti eseguendo il seguente codice:

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

E questo è ciò che abbiamo sullo schermo:

Out to host.

ExitCode: 3

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Не удается преобразовать аргумент "exitCode", со значением: "System.Object[]", для "SetShouldExit" в тип "System.Int32": "Не удается преобразовать значение "System.Object[]" типа "System.Object[]" в тип "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Ottimo risultato! Ciò significa che quando si chiama Invoke-Command, viene mantenuta la divisione delle pipeline in due thread (Host e Output), il che ci dà speranza di successo. Proviamo a lasciare un solo valore nel flusso di output, per il quale modificheremo il primo script che eseguiamo in 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 in questo modo:

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

e...SI, sembra una vittoria!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Proviamo a capire cosa è successo. Abbiamo chiamato PowerShell localmente, che a sua volta ha chiamato PowerShell sul computer remoto e lì ha eseguito il nostro script. Due flussi (Host e Output) dalla macchina remota sono stati serializzati e restituiti, mentre il flusso di Output, contenente un singolo valore digitale, è stato convertito nel tipo Int32 e come tale passato al lato ricevente, e il lato ricevente lo ha utilizzato come codice di uscita del powershell chiamante.

E come controllo finale, creiamo un lavoro one-step sul server SQL con il 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à è stata completata con un errore, testo nel registro:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Conclusioni:

  • Evitare di utilizzare Write-Output e di specificare espressioni senza assegnazione. Tieni presente che lo spostamento di questo codice altrove nello script potrebbe produrre risultati imprevisti.
  • Negli script destinati non all'avvio manuale, ma all'uso nei meccanismi di automazione, in particolare per le chiamate remote tramite WINRM, eseguire la gestione manuale degli errori tramite Try/Catch e assicurarsi che, in qualsiasi sviluppo di eventi, questo script invii esattamente un valore di tipo primitivo . Se vuoi ottenere il classico Errorlevel, questo valore deve essere numerico.

Fonte: habr.com

Aggiungi un commento