从Powershell invoke-command返回值到SQL-Server Agent

在许多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:\sqlagent\TestOutput1.ps1' $args[0] } -ArgumentList $args[0]

结果如下:

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

神奇的是,Errorlevel消失在某个地方,但是我们需要从脚本中获取值!我们尝试以下构造:

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

更加有趣。输出中的消息输出在某处消失了:

Out to host.
ExitCode: 2
ERRORLEVEL=0

现在,作为抒情的题外话,我注意到,如果您编写Write-Output或仅将表达式赋给Powershell函数中的任何变量(这隐含地暗示了输出到Output通道),即使您在本地运行它,也不会显示任何内容!这是Powershell流水线体系结构的结果-每个函数都有其自己的输出流水线,为其创建一个数组,进入其中的所有内容均被视为该函数的结果,Return语句将返回值添加到与最后一个元素相同的流水线中,并将控制权转移给调用函数。为了说明,请在本地执行以下脚本:

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,在没有其他指令的情况下,它混​​合了两个流(主机和输出)并将它们提供给CMD,后者将接收到的所有内容发送到文件,如果从powershell启动,则这两个流分别存在,并且符号重定向仅影响输出。

回到主题,我们回想起Powershell中的.NET对象模型完全存在于一台计算机(单个OS)的框架中,当通过WSMAN运行远程代码时,对象通过XML序列化进行传输,这给我们的研究带来了很多额外的兴趣。让我们通过运行以下代码来继续实验:

$res=Invoke-Command -ComputerName . -ScriptBlock { &'D:\sqlagent\TestOutput1.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:\sqlagent\TestOutput3.ps1:3 :1
+ $host.SetShouldExit($res)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

ERRORLEVEL=0

好结果!这意味着调用Invoke-Command时,管道分为两个流(主机和输出),这给了我们成功的希望。让我们尝试在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:\sqlagent\TestOutput1.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 Server上创建一个类型为“操作系统(cmdexec)”的单步任务,并带有以下文本:

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

哇!任务失败,日志中的文本:

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

发现:

  • 避免使用Write-Output并指定未分配的表达式。请记住,将此代码移到脚本中的其他位置可能会导致意外结果。
  • 在不是用于手动启动的脚本中,而是在自动化机制中使用的脚本(尤其是用于通过WINRM进行的远程调用)中,请通过Try / Catch进行手动错误处理,并确保此脚本向事件流的任何发送都准确地发送到Output流基本类型值。如果要获取经典的Errorlevel,则此值必须为数字。

All Articles