Valor de retorno del comando de invocación powershell al Agente SQL Server

Al crear mi propia metodología de administración de copias de seguridad en muchos servidores MS-SQL, pasé mucho tiempo estudiando el mecanismo de transferencia de valores a Powershell para llamadas remotas, por lo que me escribo un memo y de repente será útil para otra persona.

Entonces, tomemos un script simple para comenzar y ejecútelo localmente:

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

Para ejecutar los scripts, usaré el siguiente archivo CMD, no lo daré cada vez:

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

En la pantalla veremos lo siguiente:

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

Ahora ejecute el mismo script a través de WSMAN (de forma remota):

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

Y aqui esta el resultado:

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

Milagrosamente, Errorlevel desapareció en algún lugar, ¡pero necesitamos obtener el valor del script! Intentamos la siguiente construcción:

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

Es aún más interesante. La salida del mensaje en Salida desapareció en alguna parte:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Ahora, como una digresión lírica, noto que si escribe Write-Output o simplemente una expresión sin asignarla a ninguna variable dentro de la función Powershell (y esto implica implícitamente la salida al canal de Salida), incluso si lo ejecuta localmente, ¡no se mostrará nada! Esto es una consecuencia de la arquitectura de canalización de PowerShell: cada función tiene su propia canalización de salida, se crea una matriz para ella y todo lo que ingresa se considera el resultado de la función, la declaración de retorno agrega el valor de retorno a la misma canalización que el último elemento y transfiere el control a la función de llamada. Para ilustrar, ejecute el siguiente script localmente:

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

Y aquí está su resultado:

Main: ParameterValue

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

La función principal (cuerpo del script) también tiene su propia canalización de salida, y si ejecutamos el primer script desde CMD, redirigiendo la salida a un archivo,

PowerShell .\TestOutput1.ps1 1 > TestOutput1.txt

luego en la pantalla veremos

ERRORLEVEL=1

y en el archivo
Out to host.
Out to output.
ExitCode: 1
1

si hacemos una llamada similar desde powershell

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


entonces la pantalla será

Out to host.
ExitCode: 1

y en el archivo

Out to output.
1

Esto se debe a que CMD inicia powershell, que, en ausencia de otras instrucciones, mezcla dos flujos (Host y Output) y los entrega a CMD, que envía todo lo que recibió al archivo, y si se inicia desde powershell, estos dos flujos existen por separado, y el símbolo Los redireccionamientos solo afectan la salida.

Volviendo al tema principal, recordamos que el modelo de objetos .NET dentro de powershell existe completamente dentro del marco de una computadora (sistema operativo único), cuando el código remoto se ejecuta a través de WSMAN, los objetos se transfieren a través de la serialización XML, lo que genera un gran interés adicional para nuestra investigación. Continuemos los experimentos ejecutando el siguiente código:

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

Y esto es lo que tenemos en la pantalla:

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

¡Gran resultado! Significa que cuando se invoca Invoke-Command, la canalización se divide en dos flujos (Host y Output), lo que nos da esperanzas de éxito. Intentemos dejar solo un valor en la secuencia de Salida, para lo cual cambiamos el primer script que ejecutamos de forma remota:

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

Ejecútelo así:

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

y ... ¡SÍ, parece una victoria!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Tratemos de descubrir qué pasó con nosotros. Llamamos localmente powershell, que a su vez llamó powershell en la computadora remota y ejecutamos nuestro script allí. Dos secuencias (Host y Salida) de la máquina remota se serializaron y se transfirieron de regreso, y la secuencia de Salida, si había un valor digital, se convirtió al tipo Int32 y se transmitió al lado receptor de esta forma, y ​​el lado receptor lo utilizó como código de salida de llamada potencia Shell.

Y como última verificación, crearemos en el servidor SQL una tarea de un solo paso con el tipo "Sistema operativo (cmdexec)" con el siguiente texto:

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

HOORAY! La tarea falló, el texto en el registro:

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

Recomendaciones:

  • Evite usar Write-Output y especificar expresiones sin asignación. Recuerde que mover este código a otro lugar en el script puede generar resultados inesperados.
  • En los scripts destinados no para el lanzamiento manual, sino para usar en sus mecanismos de automatización, especialmente para llamadas remotas a través de WINRM, realice el manejo manual de errores a través de Try / Catch, y asegúrese de que este script envíe exactamente uno a la secuencia de Salida para cualquier desarrollo de eventos valor de tipo primitivo Si desea obtener el nivel de error clásico, este valor debe ser numérico.

All Articles