Retornando valor de powershell invoke-command para o agente SQL-Server

Ao criar minha própria metodologia de gerenciamento de backups em vários servidores MS-SQL, gastei muito tempo estudando o mecanismo de passagem de valores no PowerShell durante chamadas remotas, então estou escrevendo um lembrete para mim mesmo, caso isso ajude alguém mais.

Então, vamos começar com um script bem simples e executá-lo localmente:

$exitcode = $args[0]
Write-Host 'Saída para o host.'
Write-Output 'Saída para a saída.'
Write-Host ('Código de Saída: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Para executar scripts, eu usarei o seguinte arquivo CMD, não vou apresentar novamente todas as vezes:

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

Na tela, veremos o seguinte:

Saída para o host.
Saída para a saída.
Código de Saída: 1
1
ERRORLEVEL=1


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

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

E aqui está o resultado:

Saída para o host.
Saída para a saída.
Código de Saída: 2
2
ERRORLEVEL=0

Maravilha, o Errorlevel desapareceu, mas precisamos obter o valor do script! Vamos tentar a seguinte construção:

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

Aqui fica ainda mais interessante. Toda a saída para Output desapareceu:

Saída para o host.
Código de Saída: 2
ERRORLEVEL=0

Agora, como uma observação lírica, vou ressaltar que se dentro de uma função PowerShell você escrever Write-Output ou apenas uma expressão sem atribuí-la a qualquer variável (o que implicitamente indica saída para o canal Output), nada será exibido na tela, mesmo durante a execução local! Isso é resultado da arquitetura em pipeline do PowerShell — cada função tem seu próprio pipeline Output, um array é criado para isso, e tudo que cai nele é considerado o resultado da execução da função, o operador Return adiciona o valor retornado como o último elemento neste mesmo pipeline e transfere o controle para a função que a chamou. Para ilustrar, vamos executar o seguinte script localmente:

Function Write-Log {
  Param( [Parameter(Mandatory=$false, ValueFromPipeline=$true)] [String[]] $OutString = "`r`n" )
  Write-Output ("Função: "+$OutString)
  Return "ValorRetornado"
}
Write-Output ("Principal: "+"ValorParâmetro")
$res = Write-Log "ValorParâmetro"
$res.GetType()
$res.Length
$res | Foreach-Object { Write-Host ("Principal: "+$_) }

E aqui está o resultado:

Principal: ValorParâmetro

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
2
Principal: Função: ValorParâmetro
Principal: ValorRetornado

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

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

Então na tela veremos

ERRORLEVEL=1

e no arquivo

Saída para o host.
Saída para a saída.
Código de saída: 1
1

se fizermos uma chamada semelhante a partir do PowerShell

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

então na tela será

Saída para o host.
Código de saída: 1

e no arquivo

Saída para a saída.
1

Isso acontece porque o CMD executa o PowerShell, que na ausência de outras instruções mistura os dois fluxos (Host e Saída) e os devolve ao CMD, que envia para o arquivo tudo o que recebeu, enquanto que ao executar a partir do PowerShell, esses dois fluxos existem separadamente, e o símbolo de redirecionamento afeta apenas a Saída.

Voltando ao tema principal, lembremos que o modelo de objetos .NET dentro do PowerShell existe plenamente dentro de um único computador (um único SO), ao executar código remotamente via WSMAN, a transmissão de objetos ocorre através da serialização XML, o que traz muito interesse adicional em nossas investigações. Continuaremos os experimentos executando o seguinte código:

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

E aqui está o que temos na tela:

Saída para o host.

Código de saída: 3

IsPublic IsSerial Nome                                     Tipo Base
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
Não é possível converter o argumento "exitCode", com o valor: "System.Object[]", para "SetShouldExit" no tipo "System.Int32": "Não é possível converter o valor "System.Object[]" do tipo "System.Object[]" para o tipo "System
.Int32"."
D:sqlagentTestOutput3.ps1:3 sinal:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Ótimo resultado! Isso significa que ao chamar Invoke-Command, a divisão dos fluxos é mantida em dois (Host e Saída), o que nos dá esperança de sucesso. Vamos tentar manter no fluxo Saída apenas um valor, para isso vamos alterar o primeiro script que estamos executando remotamente:

$exitcode = $args[0]
Write-Host 'Saída para o host.'
#Write-Output 'Saída para a saída.'
Write-Host ('Código de saída: ' + $exitcode)
Write-Output $exitcode
$host.SetShouldExit($exitcode)

Vamos executá-lo assim:

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

e… SIM, parece que isso é uma vitória!

Saída para o host.
Código de saída: 4

IsPublic IsSerial Nome                                     Tipo Base
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType


ERRORLEVEL=4

Vamos tentar entender o que aconteceu. Chamamos o PowerShell localmente, que por sua vez chamou o PowerShell no computador remoto e executou nosso script lá. Dois fluxos (Host e Output) da máquina remota foram serializados e enviados de volta, sendo que o fluxo Output, ao conter um valor numérico, foi convertido para o tipo Int32 e enviado dessa forma para a parte receptora, que o utilizou como código de saída do PowerShell chamado.

E como última verificação, criaremos em servidor uma tarefa SQL de um passo do tipo 'Sistema Operacional (cmdexec)' com o seguinte texto:

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

UFA! A tarefa foi concluída com erro, texto no log:

Executado em nome do usuário: DOMAINagentuser. Saída para o host. ExitCode: 6. Código de saída do processo 6. A etapa terminou com erro.

Conclusões:

  • Evite o uso de Write-Output e a especificação de expressões sem atribuição. Lembre-se de que mover esse código para outro local do script pode resultar em comportamentos inesperados.
  • Em scripts destinados não para execução manual, mas para uso em seus mecanismos de automação, especialmente para chamadas remotas via WINRM, faça o tratamento manual de erros através de Try/Catch e assegure-se de que, em qualquer cenário, este script envie para o fluxo Output exatamente um valor de tipo primitivo. Se você deseja obter o clássico Errorlevel — esse valor deve ser numérico.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster