Return value from powershell invoke-command to SQL-Server agent

When creating my own backup management technique on many MS-SQL servers, I spent a lot of time studying the mechanism for passing values ​​\uXNUMXb\uXNUMXbto powershell during remote calls, so I am writing a memo to myself, in case it is useful to someone else.

So, let's start with the simplest script and run it locally:

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

To run scripts, I will use the following CMD file, I will not give it each time:

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

On the screen we will see the following:

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


Now let's run the same script via WSMAN (remotely):

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

And here is the result:

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

Great, Errorlevel disappeared somewhere, but we need to get the value from the script! Let's try the following construction:

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

It's even more interesting here. The whole output in Output has disappeared somewhere:

Out to host.
ExitCode: 2
ERRORLEVEL=0

Now, as a lyrical digression, I note that if you write Write-Output or just an expression inside the Powershell function without assigning it to any variable (and this implicitly implies output to the Output channel), then nothing will be displayed on the screen even when launched locally! This is a consequence of the powershell pipeline architecture - each function has its own Output pipeline, an array is created for it, and everything that falls into it is considered the result of the function execution, the Return statement adds the return value to the same pipeline as the last element and transfers control to the calling function. To illustrate, run the following script locally:

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

And here is his result:

Main: ParameterValue

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

The main function (script body) also has its own Output pipeline, and if we run the first script from CMD, redirecting the output to a file,

PowerShell .TestOutput1.ps1 1 > TestOutput1.txt

then on the screen we will see

ERRORLEVEL=1

and in the file

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

if we make a similar call from powershell

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

then the screen will

Out to host.
ExitCode: 1

and in the file

Out to output.
1

This is because CMD launches powershell, which, unless otherwise specified, mixes two streams (Host and Output) and gives them to CMD, which sends everything it received to the file, and in the case of launch from powershell, these two streams exist separately, and the symbol redirection only affects Output.

Returning to the main topic, let's recall that the .NET object model inside powershell fully exists within one computer (one OS), when the code is run remotely via WSMAN, objects are transferred via XML serialization, which brings a lot of additional interest to our research. Let's continue the experiments by running the following code:

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

And this is what we have on the screen:

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

Excellent result! It means that when calling Invoke-Command, the division of pipelines into two streams (Host and Output) is preserved, which gives us hope for success. Let's try to leave only one value in the Output stream, for which we will change the very first script that we run remotely:

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

Let's run it like this:

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

and… YES, it looks like a win!

Out to host.
ExitCode: 4

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


ERRORLEVEL=4

Let's try to figure out what happened. We called powershell locally, which in turn called powershell on the remote machine and executed our script there. Two streams (Host and Output) from the remote machine were serialized and sent back, while the Output stream, if it contained one digital value, was converted to type Int32 and passed to the receiving side as such, and the receiving side used it as the exit code of the caller powershell.

And as a final check, let's create a one-step job on the SQL server with the type "Operating system (cmdexec)" with the following text:

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

HOORAY! The task ended with an error, the text in the log:

Выполняется от имени пользователя: DOMAINagentuser. Out to host. ExitCode: 6.  Код завершения процесса 6.  Шаг завершился с ошибкой.

Conclusions:

  • Avoid using Write-Output and specifying expressions without assignment. Be aware that moving this code to another place in the script can lead to unexpected results.
  • In scripts that are not intended for manual launch, but for use in your automation mechanisms, especially for remote calls via WINRM, do manual error handling via Try / Catch, and ensure that, in any event, this script sends exactly one primitive type value. If you want to get the classic Errorlevel, this value must be numeric.

Source: habr.com

Add a comment