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=0Maravilha, 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=0Agora, 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: ValorRetornadoA 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=1e 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.txtentão na tela será
Saída para o host.
Código de saída: 1e no arquivo
Saída para a saída.
1Isso 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=4Vamos 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
