החזרת ערך מ-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 או רק ביטוי מבלי להקצות אותו לשום משתנה (וזה מרמז באופן מרומז על פלט לערוץ ה-Output), אז גם כאשר הוא פועל באופן מקומי, שום דבר לא יוצג על המסך! זו תוצאה של ארכיטקטורת ה-powershell pipeline - לכל פונקציה יש צינור Output משלה, נוצר עבורה מערך וכל מה שנכנס אליו נחשב לתוצאה של ביצוע הפונקציה, האופרטור 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, שבהיעדר הוראות אחרות, מערבב שני שרשורים (Host ו-Output) ונותן אותם ל-CMD, ששולח את כל מה שהוא קיבל לקובץ, ובמקרה של השקה מ-powershell, שני השרשורים הללו קיימים בנפרד, והפניות מחדש של הסמל משפיעות רק על פלט.

נחזור לנושא הראשי, הבה נזכור שמודל האובייקט .NET בתוך powershell קיים במלואו בתוך מחשב אחד (מערכת הפעלה אחת), כאשר מפעילים קוד מרחוק באמצעות 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 נשמרת חלוקת הצינורות לשני פתילים (Host ו-Output), מה שנותן לנו תקווה להצלחה. בואו ננסה להשאיר רק ערך אחד בזרם הפלט, שעבורו נשנה את הסקריפט הראשון שאנו מפעילים מרחוק:

$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 במחשב המרוחק וביצענו את הסקריפט שלנו שם. שני זרמים (Host ו-Output) מהמכונה המרוחקת הועברו בסידרה והועברו חזרה, בעוד שזרם ה-Output, בעל ערך דיגיטלי בודד, הומר לסוג Int32 וככזה הועבר לצד המקבל, והצד המקבל השתמש בו כקוד היציאה של ה-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, וודא שבכל פיתוח של אירועים, הסקריפט הזה שולח בדיוק ערך סוג פרימיטיבי אחד . אם אתה רוצה לקבל את רמת השגיאה הקלאסית, ערך זה חייב להיות מספרי.

מקור: www.habr.com

הוספת תגובה