Retornar o valor do comando de chamada do powershell para o SQL-Server Agent

Ao criar minha própria metodologia de gerenciamento de backup em muitos servidores MS-SQL, passei muito tempo estudando o mecanismo de transferência de valores para o PowerShell para chamadas remotas, por isso estou escrevendo um memorando para mim mesmo e, de repente, será útil para outra pessoa.

Então, vamos pegar um script simples para começar e executá-lo 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 executar os scripts, usarei o seguinte arquivo CMD, não o entregarei sempre:

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

Na tela, veremos o seguinte:

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

Agora execute o mesmo script através do WSMAN (remotamente):

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

E aqui está o resultado:

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

Milagrosamente, o Errorlevel desapareceu em algum lugar, mas precisamos obter o valor do script! Tentamos a seguinte construção:

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

É ainda mais interessante. A saída da mensagem em Saída desapareceu em algum lugar:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Agora, como uma digressão lírica, observo que se você escrever Write-Output ou apenas uma expressão sem atribuí-la a qualquer variável dentro da função Powershell (e isso implica implicitamente saída para o canal de saída), mesmo se você executá-lo localmente, nada será exibido! Isso é uma conseqüência da arquitetura do pipeline do PowerShell - cada função tem seu próprio pipeline de saída, uma matriz é criada para ele e tudo o que entra nele é considerado o resultado da função, a instrução Return adiciona o valor de retorno ao mesmo pipeline do último elemento e transfere o controle para a função de chamada. Para ilustrar, execute o seguinte 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: "+$_) }

E aqui está o resultado dele:

Main: ParameterValue

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

A função principal (o corpo do script) também possui seu próprio pipeline de saída e, se executarmos o primeiro script do CMD, redirecionar a saída para um arquivo,

PowerShell .\TestOutput1.ps1 1 > TestOutput1.txt

então na tela veremos

ERRORLEVEL=1

e no arquivo
Out to host.
Out to output.
ExitCode: 1
1

se fizermos uma ligação semelhante do PowerShell

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


então a tela será

Out to host.
ExitCode: 1

e no arquivo

Out to output.
1

Isso ocorre porque o CMD inicia o powershell, que, na falta de outras instruções, mistura dois fluxos (Host e Saída) e os entrega ao CMD, que envia tudo o que recebeu ao arquivo e, se iniciado pelo powershell, esses dois fluxos existem separadamente e o símbolo Os redirecionamentos afetam apenas a saída.

Voltando ao tópico principal, lembramos que o modelo de objeto .NET dentro do powershell existe completamente na estrutura de um computador (SO único), quando código remoto é executado através do WSMAN, os objetos são transferidos por serialização XML, o que traz muito interesse adicional à nossa pesquisa. Vamos continuar os experimentos executando o seguinte código:

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

E aqui está o que temos na tela:

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

Ótimo resultado! Isso significa que, quando Invoke-Command é chamado, o pipeline é dividido em dois fluxos (Host e Saída), o que nos dá esperança de sucesso. Vamos tentar deixar apenas um valor no fluxo de saída, para o qual alteramos o primeiro script executado remotamente:

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

Execute-o assim:

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

e ... SIM, parece uma vitória!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Vamos tentar descobrir o que aconteceu conosco. Chamamos localmente o powershell, que por sua vez chamou o powershell no computador remoto e executamos nosso script lá. Dois fluxos (Host e Saída) da máquina remota foram serializados e transferidos de volta, e o fluxo de Saída, se houvesse um valor digital, foi convertido para o tipo Int32 e transmitido para o lado receptor neste formulário, e o lado receptor o usou como código de saída de chamada PowerShell.

E, como última verificação, criaremos no servidor SQL uma tarefa de uma etapa com o tipo "Sistema operacional (cmdexec)" com o seguinte texto:

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

HOORAY! A tarefa falhou, o texto no log:

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

Constatações:

  • Evite usar Write-Output e especificar expressões sem atribuição. Lembre-se de que mover esse código para outro local no script pode levar a resultados inesperados.
  • Em scripts destinados não ao lançamento manual, mas para uso em seus mecanismos de automação, especialmente para chamadas remotas via WINRM, faça o tratamento manual de erros por meio do Try / Catch e garanta que esse script envie exatamente um ao fluxo de Saída para qualquer desenvolvimento de eventos valor do tipo primitivo. Se você deseja obter o nível de erro clássico - esse valor deve ser numérico.

All Articles