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 veremosERRORLEVEL=1
e no arquivoOut to host.
Out to output.
ExitCode: 1
1
se fizermos uma ligação semelhante do PowerShellPS D:\sqlagent> .\TestOutput1.ps1 1 > TestOutput1.txt
então a tela seráOut to host.
ExitCode: 1
e no arquivoOut 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-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.