Devolver un valor del comando de invocación de PowerShell al agente de SQL Server

Al crear mi propia metodología para administrar copias de seguridad en múltiples servidores MS-SQL, pasé mucho tiempo estudiando el mecanismo para pasar valores en Powershell durante llamadas remotas, así que me escribo un recordatorio en caso de que sea útil. para alguien más.

Entonces, comencemos con un script simple 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 scripts, usaré el siguiente archivo CMD, no lo incluiré siempre:

@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 ejecutemos el mismo script a través de WSMAN (de forma remota):

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

Y aqui esta el resultado:

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

Genial, Errorlevel ha desaparecido en alguna parte, ¡pero necesitamos obtener el valor del script! Probemos el siguiente diseño:

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

Esto es aún más interesante. El mensaje en Salida ha desaparecido en alguna parte:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Ahora, como una digresión lírica, señalaré que si dentro de una función de Powershell escribes Write-Output o simplemente una expresión sin asignarla a ninguna variable (y esto implícitamente implica salida al canal de salida), incluso cuando se ejecuta localmente, ¡No se mostrará nada en la pantalla! 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 entra en ella se considera el resultado de la ejecución de la función, el operador de retorno agrega el valor de retorno al mismo. pipeline como último elemento y transfiere el control a la función que llama. Para ilustrar, ejecutemos 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 aqui esta el 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

entonces veremos en la pantalla

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 estará en la pantalla

Out to host.
ExitCode: 1

y en el archivo

Out to output.
1

Esto sucede porque el CMD lanza powershell, que a falta de otras instrucciones mezcla dos hilos (Host y Output) y se los entrega al CMD, que envía todo lo recibido a un archivo, y en el caso de iniciar desde powershell, Estos dos subprocesos existen por separado y las redirecciones de símbolos solo afectan la salida.

Volviendo al tema principal, recordemos que el modelo de objetos .NET dentro de PowerShell existe completamente dentro de una computadora (un sistema operativo), cuando se ejecuta el código de forma remota a través de WSMAN, la transferencia de objetos se produce a través de la serialización XML, lo que genera mucho interés adicional. a nuestra investigación. Continuaremos nuestros experimentos ejecutando el siguiente código:

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

Y esto es lo que tenemos en 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:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

¡Gran resultado! Significa que al llamar a Invoke-Command, se mantiene la división de los pipelines en dos subprocesos (Host y Output), lo que nos da esperanzas de éxito. Intentemos dejar solo un valor en el flujo de salida, para lo cual cambiaremos 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)

Ejecutémoslo así:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.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

Intentemos averiguar qué pasó. Llamamos a powershell localmente, que a su vez llamó a powershell en la computadora remota y ejecutó nuestro script allí. Se serializaron y devolvieron dos flujos (Host y Salida) de la máquina remota, mientras que el flujo de Salida, que tenía un único valor digital, se convirtió al tipo Int32 y, como tal, se pasó al lado receptor, y el lado receptor lo usó. como código de salida del powershell de la persona que llama.

Y como comprobación final, creemos un trabajo de un solo paso en el servidor SQL del tipo “Sistema operativo (cmdexec)” con el siguiente texto:

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

¡HURRA! La tarea se completó con un error, texto en el registro:

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

Conclusiones:

  • Evite utilizar Write-Output y especificar expresiones sin asignación. Tenga en cuenta que mover este código a otra parte del script puede producir resultados inesperados.
  • En los scripts destinados no al inicio manual, sino a su uso en sus mecanismos de automatización, especialmente para llamadas remotas a través de WINRM, realice un manejo manual de errores a través de Try/Catch y asegúrese de que, en cualquier desarrollo de eventos, este script envíe exactamente un valor de tipo primitivo. . Si desea obtener el nivel de error clásico, este valor debe ser numérico.

Fuente: habr.com

Añadir un comentario