从 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

太好了,Errorlevel 已经在某处消失了,但是我们需要从脚本中获取值! 我们来尝试一下下面的设计:

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

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

Out to host.
ExitCode: 2
ERRORLEVEL=0

现在,作为一个抒情的题外话,我会注意到,如果在 Powershell 函数中编写 Write-Output 或只是一个表达式,而不将其分配给任何变量(这隐含地意味着输出到输出通道),那么即使在本地运行时,屏幕上不会显示任何内容! 这是 powershell 管道架构的结果 - 每个函数都有自己的输出管道,为其创建一个数组,进入其中的所有内容都被视为函数执行的结果,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,在没有其他指令的情况下,它混​​合两个线程(主机和输出)并将它们提供给 CMD,CMD 将其收到的所有内容发送到文件,并且在从 powershell 启动的情况下,这两个线程单独存在,符号重定向仅影响输出。

回到主题,让我们记住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

很好的结果! 这意味着在调用 Invoke-Command 时,将维持管道划分为两个线程(主机和输出),这给了我们成功的希望。 让我们尝试在输出流中只保留一个值,为此我们将更改远程运行的第一个脚本:

$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 类型,并因此传递到接收方,接收方使用它作为调用者 powershell 的退出代码。

作为最后的检查,让我们在 SQL Server 上创建一个类型为“操作系统 (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 进行手动错误处理,并确保在任何事件开发中,此脚本只发送一个原始类型值。 如果你想获得经典的Errorlevel,这个值必须是数字。

来源: habr.com

添加评论