Al desarrollar un método propio para gestionar copias de seguridad en múltiples servidores MS-SQL, pasé mucho tiempo investigando el mecanismo de transmisión de valores en PowerShell durante las llamadas remotas, por lo que estoy escribiendo esta nota para mí, por si a alguien más le sirve.
Así que empecemos con un script simple y ejecútalo localmente:
$exitcode = $args[0]
Write-Host 'Salida a host.'
Write-Output 'Salida a salida.'
Write-Host ('Código de salida: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)Para ejecutar scripts, utilizaré el siguiente archivo CMD, no lo repetiré cada vez:
@Echo OFF
PowerShell .TestOutput1.ps1 1
ECHO ERRORLEVEL=%ERRORLEVEL%En la pantalla veremos lo siguiente:
Salida a host.
Salida a salida.
Código de salida: 1
1
ERRORLEVEL=1
Ahora ejecutaremos el mismo script a través de WSMAN (remotamente):
Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Y aquí está el resultado:
Salida a host.
Salida a salida.
Código de salida: 2
2
ERRORLEVEL=0Maravilloso, Errorlevel ha desaparecido, pero necesitamos obtener el valor del script. Probamos la siguiente construcción:
$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]Aquí se vuelve más interesante. Toda la salida en Output ha desaparecido:
Salida a host.
Código de salida: 2
ERRORLEVEL=0Ahora, como un interludio lírico, señalaré que si dentro de una función de PowerShell escribes Write-Output o simplemente una expresión sin asignarla a ninguna variable (lo que implícitamente implica salida en el canal Output), ¡ni siquiera en la ejecución local se mostrará nada en la pantalla! Esto es consecuencia de la arquitectura de tuberías de PowerShell: cada función tiene su propia tubería Output, para la cual se crea un arreglo, y todo lo que entra se considera resultado de la ejecución de la función. El operador Return añade el valor devuelto a esta misma tubería como último elemento y devuelve el control a la función que llamó. Para ilustrarlo, ejecutemos localmente el siguiente script:
Function Write-Log {
Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
Write-Output ("Función: "+$OutString)
Return "ValorRetornado"
}
Write-Output ("Principal: "+"ValorParametro")
$res = Write-Log "ValorParametro"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Principal: "+$_) }
Y aquí está su resultado:
Principal: ValorParametro
IsPublic IsSerial Nombre TipoBase
-------- -------- ---- --------
True True Object[] System.Array
2
Principal: Función: ValorParametro
Principal: ValorRetornadoLa función principal (el 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
veremos en la pantalla
ERRORLEVEL=1y 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.txtveremos en la pantalla
Out to host.
ExitCode: 1y en el archivo
Out to output.
1Esto ocurre porque CMD inicia PowerShell, que en ausencia de otras instrucciones combina dos flujos (Host y Output) y se los entrega a CMD, que envía a archivo todo lo que recibió; mientras que al ejecutarse desde PowerShell, estos dos flujos existen por separado, y el símbolo de redirección afecta solo a Output.
Regresando al tema principal, recordemos que el modelo de objetos .NET dentro de PowerShell existe plenamente en el marco de un solo ordenador (un solo SO); al ejecutar código remotamente a través de WSMAN, la transmisión de objetos se realiza mediante serialización XML, lo que añade mucho interés a nuestras investigaciones. Continuaremos los 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 la pantalla:
Out to host.
ExitCode: 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
No se puede convertir el argumento "exitCode", con valor: "System.Object[]", para "SetShouldExit" en el tipo "System.Int32": "No se puede convertir el valor "System.Object[]" del tipo "System.Object[]" en el tipo "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 carácter:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
ERRORLEVEL=0¡Un resultado excelente! Esto significa que al llamar a Invoke-Command se mantiene la división de los canales en dos flujos (Host y Output), lo que nos da esperanza de éxito. Intentemos dejar solo un valor en el flujo Output, para lo cual modificaremos el primer script que estamos ejecutando 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)
Lo ejecutaremos de esta manera:
$res=Invoke-Command -ComputerName . -ScriptBlock { 'D:sqlagentTestOutput1.ps1' $args[0] } -ArgumentList $args[0]
$host.SetShouldExit($res)
y... ¡SÍ, parece ser una victoria!
Out to host.
ExitCode: 4
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
ERRORLEVEL=4Intentamos entender qué ha sucedido. Llamamos localmente a PowerShell, que a su vez llamó a PowerShell en la computadora remota y ejecutó nuestro script allí. Dos flujos (Host y Output) de la máquina remota fueron serializados y enviados de vuelta, mientras que el flujo Output, al tener un valor numérico, fue convertido al tipo Int32 y enviado a la parte receptora, que lo utilizó como código de salida para el PowerShell que lo llamó.
Y como última comprobación, crearemos en servidor una tarea SQL de un paso con 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)"¡YUPI! La tarea terminó con un error, el texto en el registro:
Se está ejecutando en nombre del usuario: DOMAINagentuser. Salida al host. ExitCode: 6. Código de salida del proceso 6. El paso terminó con un error.
Conclusiones:
- Evite el uso de Write-Output y la especificación de expresiones sin asignación. Recuerde que mover este código a otro lugar del script puede producir resultados inesperados.
- En scripts destinados no para ejecución manual, sino para ser utilizados en sus mecanismos de automatización, especialmente para llamadas remotas a través de WINRM, haga un manejo de errores manual utilizando Try/Catch, y asegúrese de que, sea cual sea la situación, este script envíe al flujo Output exactamente un valor de tipo primitivo. Si desea obtener el clásico Errorlevel, este valor debe ser numérico.
Fuente: habr.com
