Kembali nilai dari powershell invoke-command ke SQL-Server Agent

Saat membuat metodologi manajemen cadangan saya sendiri di banyak server MS-SQL, saya menghabiskan banyak waktu mempelajari mekanisme mentransfer nilai ke PowerShell untuk panggilan jarak jauh, jadi saya menulis memo untuk diri saya sendiri, dan tiba-tiba akan berguna bagi orang lain.

Jadi, mari kita ambil skrip sederhana untuk memulai dan menjalankannya secara lokal:

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

Untuk menjalankan skrip, saya akan menggunakan file CMD berikut, saya tidak akan memberikannya setiap waktu:

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

Di layar kita akan melihat yang berikut:

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

Sekarang jalankan skrip yang sama melalui WSMAN (jarak jauh):

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

Dan inilah hasilnya:

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

Ajaibnya, Errorlevel menghilang di suatu tempat, tetapi kita perlu mendapatkan nilai dari skrip! Kami mencoba konstruksi berikut:

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

Itu bahkan lebih menarik. Output pesan di Output menghilang di suatu tempat:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Sekarang, sebagai penyimpangan liris, saya perhatikan bahwa jika Anda menulis Write-Output atau hanya ekspresi tanpa menugaskannya ke variabel di dalam fungsi Powershell (dan ini secara implisit menyiratkan output ke saluran Output), bahkan jika Anda menjalankannya secara lokal, tidak ada yang akan ditampilkan! Ini adalah konsekuensi dari arsitektur pipeline powershell - setiap fungsi memiliki Output pipeline sendiri, sebuah array dibuat untuk itu, dan segala sesuatu yang masuk ke dalamnya dianggap sebagai hasil dari fungsi, pernyataan Return menambahkan nilai kembali ke pipeline yang sama dengan elemen terakhir dan mentransfer kontrol ke fungsi pemanggilan. Untuk mengilustrasikan, jalankan skrip berikut secara lokal:

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

Dan inilah hasilnya:

Main: ParameterValue

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

Fungsi utama (badan skrip) juga memiliki pipeline Output sendiri, dan jika kita menjalankan skrip pertama dari CMD, mengarahkan output ke file,

PowerShell .\TestOutput1.ps1 1 > TestOutput1.txt

kemudian di layar kita akan melihat

ERRORLEVEL=1

dan dalam file
Out to host.
Out to output.
ExitCode: 1
1

jika kita melakukan panggilan serupa dari PowerShell

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


maka layar akan menjadi

Out to host.
ExitCode: 1

dan dalam file

Out to output.
1

Ini karena CMD memulai PowerShell, yang, jika tidak ada instruksi lain, mencampur dua aliran (Host dan Output) dan memberikannya ke CMD, yang mengirimkan semua yang diterimanya ke file, dan jika diluncurkan dari PowerShell, kedua aliran ini ada secara terpisah, dan simbol Redirect hanya memengaruhi Output.

Kembali ke topik utama, kita ingat bahwa model objek .NET di dalam powershell sepenuhnya ada dalam kerangka satu komputer (OS tunggal), ketika kode jarak jauh dijalankan melalui WSMAN, objek-objek ditransfer melalui serialisasi XML, yang membawa banyak minat tambahan untuk penelitian kami. Mari kita lanjutkan eksperimen dengan menjalankan kode berikut:

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

Dan inilah yang kami miliki di layar:

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

Hasil yang bagus! Ini berarti bahwa ketika Invoke-Command dipanggil, pipa dibagi menjadi dua aliran (Host dan Output), yang memberi kita harapan untuk sukses. Mari kita coba hanya menyisakan satu nilai dalam aliran Output, untuk itu kita mengubah skrip pertama yang kita jalankan dari jarak jauh:

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

Jalankan seperti ini:

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

dan ... YA, sepertinya kemenangan!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Mari kita coba mencari tahu apa yang terjadi dengan kita. Kami memanggil PowerShell secara lokal, yang pada gilirannya memanggil PowerShell pada komputer jarak jauh dan mengeksekusi skrip kami di sana. Dua aliran (Host dan Output) dari mesin jarak jauh diserialisasi dan ditransfer kembali, dan aliran Output, jika ada satu nilai digital di dalamnya, dikonversi ke tipe Int32 dan dikirim ke sisi penerima dalam bentuk ini, dan sisi penerima menggunakannya sebagai kode panggilan keluar PowerShell.

Dan sebagai pemeriksaan terakhir, kita akan membuat tugas satu langkah pada server SQL dengan tipe "Sistem operasi (cmdexec)" dengan teks berikut:

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

HOORAY! Tugas gagal, teks di log:

   : DOMAIN\agentuser. Out to host. ExitCode: 6.     6.     .

Temuan:

  • Hindari menggunakan Write-Output dan menentukan ekspresi tanpa penugasan. Ingat bahwa memindahkan kode ini ke tempat lain dalam skrip dapat menyebabkan hasil yang tidak terduga.
  • Dalam skrip yang dimaksudkan bukan untuk peluncuran manual, tetapi untuk digunakan dalam mekanisme otomasi Anda, terutama untuk panggilan jarak jauh melalui WINRM, lakukan penanganan kesalahan manual melalui Try / Catch, dan pastikan skrip ini mengirimkan tepat satu ke aliran Output untuk setiap perkembangan acara nilai tipe primitif. Jika Anda ingin mendapatkan Errorlevel klasik - nilai ini harus berupa angka.

All Articles