從 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

太好了,Errorlevel 已經在某處消失了,但是我們需要從腳本中獲取值! 讓我們試試以下構造:

$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 運算符將返回值添加到相同的結果中. pipeline 作為最後一個元素並將控制權轉移給呼叫函數。 為了說明這一點,讓我們在本地運行以下腳本:

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,在沒有其他指令的情況下,它混合兩個執行緒(主機和輸出)並將它們提供給CMD,CMD 將其收到的所有內容傳送到文件,並且在從powershell 啟動的情況下,這兩個執行緒單獨存在,符號重定向僅影響輸出。

回到主題,讓我們記住powershell內的.NET物件模型完全存在於一台電腦(一個作業系統)內,當透過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 時,將維持管道劃分為兩個執行緒(主機和輸出),這給了我們成功的希望。 讓我們嘗試在輸出流中只保留一個值,為此我們將更改遠端運行的第一個腳本:

$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 並在那裡執行我們的腳本。 來自遠端電腦的兩個流(主機和輸出)被序列化並傳回,而其中包含單一數字值的輸出流被轉換為 Int32 類型,並因此傳遞到接收方,接收方使用它作為呼叫者powershell 的退出代碼。

作為最後的檢查,讓我們在 SQL Server 上建立一個類型為「作業系統 (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 進行手動錯誤處理,並確保在任何事件開發中,此腳本只發送一個原始類型值。 如果你想得到經典的Errorlevel,這個值必須是數字。

來源: www.habr.com

添加評論