כשיצרתי מתודולוגיה משלי לניהול גיבויים במספר שרתי 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
