إرجاع قيمة من أمر استدعاء بوويرشيل إلى وكيل 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 كتابة إخراج أو مجرد تعبير دون تعيينه لأي متغير (وهذا يعني ضمنيًا الإخراج إلى قناة الإخراج)، فحتى عند التشغيل محليًا، لن يتم عرض أي شيء على الشاشة! هذا نتيجة لبنية خط أنابيب بوويرشيل - كل وظيفة لها خط أنابيب الإخراج الخاص بها، ويتم إنشاء مصفوفة لها، وكل ما يدخل فيها يعتبر نتيجة تنفيذ الوظيفة، ويضيف عامل الإرجاع قيمة الإرجاع إلى نفسه خط الأنابيب هو العنصر الأخير وينقل التحكم إلى وظيفة الاستدعاء. للتوضيح، لنقم بتشغيل البرنامج النصي التالي محليًا:

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

إذا قمنا بإجراء مكالمة مماثلة من بوويرشيل

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

ثم سيكون على الشاشة

Out to host.
ExitCode: 1

وفي الملف

Out to output.
1

يحدث هذا لأن CMD يطلق powershell، والذي، في حالة عدم وجود تعليمات أخرى، يمزج خيطين (المضيف والإخراج) ويعطيهما إلى CMD، الذي يرسل كل ما يتلقاه إلى ملف، وفي حالة التشغيل من 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

نتيجة عظيمة! وهذا يعني أنه عند استدعاء أمر الاستدعاء، يتم الحفاظ على تقسيم خطوط الأنابيب إلى خيطين (المضيف والإخراج)، مما يمنحنا الأمل في النجاح. دعونا نحاول ترك قيمة واحدة فقط في دفق الإخراج، والتي سنقوم بتغيير البرنامج النصي الأول الذي نقوم بتشغيله عن بعد:

$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 وتم تمريره على هذا النحو إلى الجانب المتلقي، واستخدمه الجانب المتلقي كرمز خروج بوويرشيل المتصل.

وكفحص نهائي، دعونا ننشئ مهمة من خطوة واحدة على خادم 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، وتأكد من أنه في أي تطور للأحداث، يرسل هذا البرنامج النصي قيمة نوع بدائية واحدة بالضبط . إذا كنت تريد الحصول على مستوى الخطأ الكلاسيكي، فيجب أن تكون هذه القيمة رقمية.

المصدر: www.habr.com

إضافة تعليق