Retornando um valor do comando de invocação do PowerShell para o agente SQL Server

Ao criar minha própria metodologia para gerenciar backups em vários servidores MS-SQL, passei muito tempo estudando o mecanismo de passagem de valores no Powershell durante chamadas remotas, por isso estou escrevendo um lembrete para mim mesmo caso seja útil para outra pessoa.

Então, vamos começar com um script simples 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 scripts, usarei o seguinte arquivo CMD, não irei incluí-lo 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 vamos executar o mesmo script via WSMAN (remotamente):

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

E aqui está o resultado:

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

Ótimo, o Errorlevel desapareceu em algum lugar, mas precisamos obter o valor do script! Vamos tentar o seguinte design:

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

Isto é ainda mais interessante. A mensagem em Output desapareceu em algum lugar:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Agora, como uma digressão lírica, observarei que se dentro de uma função Powershell você escrever Write-Output ou apenas uma expressão sem atribuí-la a nenhuma variável (e isso implica implicitamente na saída para o canal Output), mesmo quando executado localmente, nada será exibido na tela! Isso é uma consequência da arquitetura do pipeline do PowerShell - cada função tem seu próprio pipeline de saída, um array é criado para ela e tudo o que entra nele é considerado o resultado da execução da função, o operador Return adiciona o valor de retorno ao mesmo pipeline como o último elemento e transfere o controle para a função de chamada. Para ilustrar, vamos executar 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:

Main: ParameterValue

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

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

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

então veremos na tela

ERRORLEVEL=1

e no arquivo

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

se fizermos uma chamada semelhante do PowerShell

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

então estará na tela

Out to host.
ExitCode: 1

e no arquivo

Out to output.
1

Isso acontece porque o CMD lança o powershell, que, na falta de outras instruções, mistura dois threads (Host e Output) e os entrega ao CMD, que envia tudo o que recebeu para um arquivo, e no caso de lançar do powershell, esses dois threads existem separadamente e os redirecionamentos de símbolos afetam apenas a saída.

Voltando ao tópico principal, lembremos que o modelo de objeto .NET dentro do powershell existe totalmente dentro de um computador (um SO), ao executar código remotamente via WSMAN, a transferência de objetos ocorre através de serialização XML, o que traz muito interesse adicional à nossa pesquisa. Vamos continuar nossos 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 é isso 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:sqlagentTestOutput3.ps1:3 знак:1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

Ótimo resultado! Isso significa que ao chamar o Invoke-Command, a divisão dos pipelines em dois threads (Host e Output) é mantida, o que nos dá esperança de sucesso. Vamos tentar deixar apenas um valor no fluxo de saída, para o qual alteraremos o primeiro script que executarmos remotamente:

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

Vamos executar assim:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:sqlagentTestOutput1.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. Chamamos o powershell localmente, que por sua vez chamou o powershell no computador remoto e executou nosso script lá. Dois fluxos (Host e Saída) da máquina remota foram serializados e passados ​​de volta, enquanto o fluxo de saída, contendo um único valor digital, foi convertido para o tipo Int32 e, como tal, passado para o lado receptor, e o lado receptor o utilizou. como o código de saída do powershell do chamador.

E como verificação final, vamos criar um job de uma etapa no SQL server com o 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)"

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

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Conclusões:

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

Fonte: habr.com

Adicionar um comentário