Renvoyer la valeur de la commande d'invocation de powershell à l'Agent SQL-Server

Lors de la création de ma propre méthodologie de gestion des sauvegardes sur de nombreux serveurs MS-SQL, j'ai passé beaucoup de temps à étudier le mécanisme de transfert de valeurs vers PowerShell pour les appels distants.J'écris donc un mémo pour moi-même et, soudain, il sera utile pour quelqu'un d'autre.

Prenons donc un script simple pour commencer et l'exécuter localement:

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

Pour exécuter les scripts, je vais utiliser le fichier CMD suivant, je ne le donnerai pas à chaque fois:

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

Sur l'écran, nous verrons ce qui suit:

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

Exécutez maintenant le même script via WSMAN (à distance):

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

Et voici le résultat:

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

Miraculeusement, Errorlevel a disparu quelque part, mais nous devons obtenir la valeur du script! Nous essayons la construction suivante:

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

C’est encore plus intéressant. La sortie du message dans Output a disparu quelque part:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Maintenant, en tant que digression lyrique, je note que si vous écrivez Write-Output ou simplement une expression sans l'assigner à une variable à l'intérieur de la fonction Powershell (et cela implique implicitement une sortie sur le canal de sortie), alors même en cas d'exécution locale, rien ne sera affiché! Ceci est une conséquence de l'architecture du pipeline PowerShell - chaque fonction a son propre pipeline de sortie, un tableau est créé pour elle et tout ce qui y pénètre est considéré comme le résultat de la fonction, l'instruction Return ajoute la valeur de retour au même pipeline que le dernier élément et transfère le contrôle à la fonction appelante. Pour illustrer, exécutez le script suivant localement:

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

Et voici son résultat:

Main: ParameterValue

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

La fonction principale (corps du script) a également son propre pipeline de sortie, et si nous exécutons le premier script à partir de CMD, redirigeant la sortie vers un fichier,

PowerShell .\TestOutput1.ps1 1 > TestOutput1.txt

puis sur l'écran on verra

ERRORLEVEL=1

et dans le dossier
Out to host.
Out to output.
ExitCode: 1
1

si nous faisons un appel similaire de PowerShell

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


alors l'écran sera

Out to host.
ExitCode: 1

et dans le dossier

Out to output.
1

En effet, CMD démarre PowerShell, qui, en l'absence d'autres instructions, mélange les deux flux (hôte et sortie) et les donne à CMD, qui envoie tout ce qu'il a reçu dans le fichier, et s'il est lancé à partir de PowerShell, ces deux flux existent séparément et le symbole Les redirections n'affectent que la sortie.

Revenant au sujet principal, nous rappelons que le modèle d'objet .NET à l'intérieur de PowerShell existe pleinement dans le cadre d'un ordinateur (système d'exploitation unique), lorsque le code à distance est exécuté via WSMAN, les objets sont transférés via la sérialisation XML, ce qui apporte beaucoup d'intérêt supplémentaire à nos recherches. Continuons les expériences en exécutant le code suivant:

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

Et voici ce que nous avons à l'écran:

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:\sqlagent\TestOutput3.ps1:3 :1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Excellent résultat! Cela signifie que lorsque Invoke-Command est appelé, le pipeline est divisé en deux flux (hôte et sortie), ce qui nous donne de l'espoir de réussir. Essayons de ne laisser qu'une seule valeur dans le flux de sortie, pour laquelle nous modifions le tout premier script que nous exécutons à distance:

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

Exécutez-le comme ceci:

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

et ... OUI, cela semble être une victoire!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Essayons de comprendre ce qui s'est passé avec nous. Nous avons appelé localement powershell, qui à son tour a appelé powershell sur l'ordinateur distant et y avons exécuté notre script. Deux flux (hôte et sortie) de la machine distante ont été sérialisés et renvoyés, tandis que le flux de sortie, s'il avait une valeur numérique, a été converti en type Int32 et transmis au côté réception sous cette forme, et le côté réception l'a utilisé comme code de sortie appelant PowerShell.

Et comme dernière vérification, nous allons créer sur le serveur SQL une tâche en une étape avec le type "Système d'exploitation (cmdexec)" avec le texte suivant:

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

HOORAY! La tâche a échoué, le texte dans le journal:

   : DOMAIN\agentuser. Out to host. ExitCode: 6.     6.     .

Résultats:

  • Évitez d'utiliser Write-Output et de spécifier des expressions sans affectation. N'oubliez pas que déplacer ce code vers un autre endroit du script peut entraîner des résultats inattendus.
  • Dans les scripts destinés non pas au lancement manuel, mais à utiliser dans vos mécanismes d'automatisation, en particulier pour les appels distants via WINRM, effectuez une gestion manuelle des erreurs via Try / Catch et assurez-vous que ce script en envoie exactement un au flux de sortie pour tout développement d'événements. valeur de type primitif. Si vous souhaitez obtenir le niveau d'erreur classique - cette valeur doit être numérique.

All Articles