在創建自己的在多個 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
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