PowerShell invoke-command から SQL Server エージェントに値を返す

複数の MS-SQL サーバーでバックアップを管理するための独自の方法論を作成するときに、リモート呼び出し中に Powershell で値を渡すメカニズムを研究するのに多くの時間を費やしたため、役立つ場合に備えて自分自身への備忘録を書いています。他の誰かに。

それでは、簡単なスクリプトから始めて、ローカルで実行してみましょう。

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

スクリプトを実行するには、次の CMD ファイルを使用します。毎回これを含めるわけではありません。

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

画面には次のことが表示されます。

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


次に、同じスクリプトを WSMAN 経由で (リモートで) 実行してみましょう。

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

そしてここに結果があります:

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

エラーレベルはどこかに消えてしまいましたが、スクリプトから値を取得する必要があります。 次のデザインを試してみましょう。

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

これはさらに興味深いことです。 出力内のメッセージがどこかに消えてしまいました。

Out to host.
ExitCode: 2
ERRORLEVEL=0

ここで、叙情的な余談になりますが、Powershell 関数内で Write-Output または変数に割り当てずに式だけを記述した場合 (これは暗黙的に出力チャネルへの出力を意味します)、ローカルで実行している場合でも、画面には何も表示されません! これは PowerShell パイプライン アーキテクチャの結果です。各関数には独自の出力パイプラインがあり、配列が作成され、そこに入るすべてのものが関数実行の結果とみなされ、Return 演算子は同じ出力パイプラインに戻り値を追加します。最後の要素としてパイプラインを作成し、呼び出し元の関数に制御を渡します。 説明のために、次のスクリプトをローカルで実行してみましょう。

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: "+$_) }

そして結果は次のとおりです。

Main: ParameterValue

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

メイン関数 (スクリプト本体) にも独自の出力パイプラインがあり、CMD から最初のスクリプトを実行して出力をファイルにリダイレクトすると、

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

それから画面に表示されます

ERRORLEVEL=1

そしてファイル内に

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

PowerShell から同様の呼び出しを行った場合

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

そうすれば画面上に表示されます

Out to host.
ExitCode: 1

そしてファイル内に

Out to output.
1

これは、CMD が PowerShell を起動するために発生します。PowerShell は、他の命令がない場合、XNUMX つのスレッド (ホストと出力) を混合して CMD に渡し、受け取ったすべてをファイルに送信します。PowerShell から起動する場合は、これら XNUMX つのスレッドは別々に存在し、シンボルのリダイレクトは出力にのみ影響します。

本題に戻りますが、PowerShell 内の .NET オブジェクト モデルは XNUMX つのコンピュータ (XNUMX つの OS) 内に完全に存在することを思い出してください。WSMAN 経由でコードをリモートで実行する場合、オブジェクトの転送は XML シリアル化を通じて行われ、これによりさらに多くの関心がもたらされます。私たちの研究に。 次のコードを実行して実験を続けてみましょう。

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

そして、これが画面に表示されているものです。

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

素晴らしい結果です! これは、Invoke-Command を呼び出すときにパイプラインの XNUMX つのスレッド (ホストと出力) への分割が維持されることを意味し、成功の期待が持てます。 出力ストリームには XNUMX つの値だけを残して、リモートで実行する最初のスクリプトを変更してみましょう。

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

次のように実行しましょう:

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

そして...はい、勝利のようです!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

何が起こったのか理解してみましょう。 ローカルで powershell を呼び出し、リモート コンピューター上の powershell を呼び出して、そこでスクリプトを実行しました。 リモート マシンからの 32 つのストリーム (ホストと出力) はシリアル化されて戻され、一方、単一のデジタル値を含む出力ストリームは IntXNUMX 型に変換されて受信側に渡され、受信側はそれを使用しました。呼び出し元の PowerShell の終了コードとして。

そして最終チェックとして、SQL サーバー上に次のテキストを含むタイプ「オペレーティング システム (cmdexec)」のワンステップ ジョブを作成してみましょう。

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

万歳! タスクはエラーとともに完了しました。ログには次のテキストが記録されます。

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

結論:

  • Write-Output を使用したり、代入なしで式を指定したりすることは避けてください。 このコードをスクリプト内の他の場所に移動すると、予期しない結果が生じる可能性があることに注意してください。
  • 手動起動ではなく、自動化メカニズムでの使用を目的としたスクリプト (特に WINRM 経由のリモート呼び出しの場合) では、Try/Catch による手動エラー処理を実行し、イベントの開発においてこのスクリプトが XNUMX つのプリミティブ型値を確実に送信するようにします。 。 従来の Errorlevel を取得したい場合、この値は数値である必要があります。

出所: habr.com

コメントを追加します