在许多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-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,则此值必须为数字。