【问题标题】:PowerShell: How 'Receive-Job' pulls output from the job's code block in detail?PowerShell:“接收作业”如何从作业的代码块中详细提取输出?
【发布时间】:2018-06-24 22:32:02
【问题描述】:

请查看此测试脚本以及我对“接收作业”如何工作的详细结论。
我还有一些问题要弄清楚,'Receive-Job' 是如何从代码块中提取流的。

<# .SYNOPSIS Test the console output and variable capturing of Write- cmdlet calls in a code block used by 'Start-Job'
   .NOTES
    .NET Version                   4.7.2
    PSVersion                      5.1.16299.431
    PSEdition                      Desktop
    PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
    BuildVersion                   10.0.16299.431
    CLRVersion                     4.0.30319.42000
    WSManStackVersion              3.0
    PSRemotingProtocolVersion      2.3
    SerializationVersion           1.1.0.1
#>

Set-StrictMode -Version latest

if ($host.Name -inotmatch 'consolehost') { Clear-Host }

$errorBuffer = $null
$warningBuffer = $null
$outBuffer = $null
$infoBuffer = $null

# Start the job
$job = Start-Job -ScriptBlock {

    Set-StrictMode -Version latest

PowerShell 在它自己的进程中启动这个脚本块,就像它会启动一个外部可执行文件一样。
因此,PowerShell 只能将代码块中的 stdout/success 和 stderr/error 映射到脚本进程中 PowerShell 的成功 (1) 和错误 (2) 流。
这两个流将由Receive-Job 传递,并且可以按预期在Receive-Job 行中重定向。
Receive-Job 可以根据要求将这两个流存储到变量中。 (-OutVariable -ErrorVariable)
此外,Receive-Job 可以捕获 PowerShell 流信息(流 6)和警告(流 3),也可以将它们存储在变量中。 (-WarningVariable -InformationVariable)
但是将这些流存储在变量中并不是重定向。
每个 Write- cmdlet 调用都可以在控制台上显示一条消息,与 -variable 开关无关。
控制台上的可见消息仅取决于 Write-cmdlet 自己的首选项和 Write-cmdlet 调用中可能的重定向。

    # This will, by default, output to the console over stream 6 (info), and always get captured in $infoBuffer.
    Write-Host "***WRITE_HOST***"           # 6> $null # Supresses the output to the console.

    # This will not output to the console over stream 6 (info) by default, but always get captured in $infoBuffer.
    $InformationPreference = 'Continue'     # Outputs to the console, default is 'SilentlyContinue'.
    Write-Information "***INFO***"          # 6> $null # Supresses the output to the console for preference 'Continue'.
    $InformationPreference = "SilentlyContinue"

    # This will not output to the console over stream 5 (debug) by default, and can't get captured in a variable.
    $DebugPreference = 'Continue'           # Outputs to the console, default is 'SilentlyContinue'.
    Write-Debug "***DEBUG***"               # 5> $null  # Supresses the output to the console for preference 'Continue'.
    $DebugPreference = "SilentlyContinue"

    # This will not output to the console over stream 4 (verbose), by default, and can't get captured in a variable.
    $VerbosePreference = 'Continue'         # Outputs to the console, default is 'SilentlyContinue'.
    Write-Verbose "***Verbose***"           # 4> $null  # Supresses the output to the console for preference 'Continue'.
    $VerbosePreference = 'SilentlyContinue'

    # This will, by default, output to the console over stream 3 (warning), but get captured in $warningBuffer only for
    # preference 'Continue'.
    #$WarningPreference = 'SilentlyContinue'   # Supresses console output AND variable capturing, default is 'Continue'.
    Write-Warning "***WARNING***"              # 3> $null  # Supresses the warning output to the console for preference
    #$WarningPreference = 'Continue'                       # 'Continue'.

    # This will output to the console over stream 2 (error), and always get captured in $errorBuffer, if not redirected
    # in the code block.
    # For 'Receive-Job -ErrorAction Stop' it would raise an execption, the content in $errorBuffer is quite useless then.
    Write-Error '***ERROR***'   # 2> $null # Supresses the output AND variable capturing, but you can supress/redirect
                                           # this stream in the 'Receive-Job' line without breaking the variable
                                           # capturing: 'Receive-Job ... -ErrorVariable errorBuffer 2> $null'

    # These will output to the console over stream 1 (success), and always get captured in $result and $outBuffer, if
    # not redirected in the code block.
    Write-Output '***OUTPUT***'  # 1> $null # Supresses the output AND variable capturing, but you can supress/redirect
    Write-Output '***NEXT_OUTPUT***'        # this stream in the 'Receive-Job' line without breaking the variable
    "***DIRECT_OUT***"                      # capturing: '$result = Receive-Job ... -OutVariable outBuffer 1> $null'
}

# Wait for the job to finish
Wait-Job -Job $job

try
{
    # Working only outside the code block, this is a workaround for catching ALL output.
    #$oldOut = [Console]::Out
    #$stringWriter = New-Object IO.StringWriter
    #[Console]::SetOut($stringWriter)

    # Pull the buffers from the code block
    $result = Receive-Job <#-ErrorAction Stop#> `
                          -Job $job `
                          -ErrorVariable       errorBuffer `
                          -WarningVariable     warningBuffer `
                          -OutVariable         outBuffer `
                          -InformationVariable infoBuffer `
                          # 1> $null #2> $null  # Only the success and error streams can be redirected here, other
                                                # streams are not available.

    # Restore the console
    #[Console]::SetOut($oldOut)

    # Get all catched output
    #$outputOfAllWriteFunctions = $stringWriter.ToString()
}
catch
{
    Write-Host "EXCEPTION: $_" -ForegroundColor Red
}
finally
{
    Write-Host "error: $errorBuffer"
    Write-Host "warning: $warningBuffer"
    Write-Host "out: $outBuffer"
    Write-Host "info: $infoBuffer"
    Write-Host "result: $result"
    #Write-Host "`noutputOfAllWriteFunctions:`n";Write-Host "$outputOfAllWriteFunctions" -ForegroundColor Cyan

    Remove-Job -Job $job
}

我的最终结论:

因为Start-Job的代码块运行在自己的进程中,所以不能直接写入scripts进程控制台。
代码块由捕获机制包装,该机制捕获缓冲区中的所有 6 个 PS 流。
Receive-Job 的调用使用进程间通信来获取所有这些流。
Receive-Job 穿过流 1 和 2 并使其成为自己的输出,因此可用于重定向。
Receive-Job 使用Write-Error 将流2 写入控制台,因此Receive-Job 将引发参数-ErrorAction Stop 的异常。
然后Write-Error 使用Console.Out.WriteLine() 以红色写入控制台。
然后Receive-Job 检查变量存储并存储流 1(成功)、2(错误)、3(警告)和 6(信息)。
最后Receive-Job 使用Console.Out.WriteLine() 将具有不同前景色的流1、3、4、5 和6 写入控制台。
这就是为什么您可以使用 Console.SetOut() 捕获所有这 6 个流输出的原因,甚至是错误流输出,我曾预计需要 Console.SetError()

但这些结论存在一个问题:

Write-Host 的输出默认写入控制台,其输出添加到信息变量中。
所以Write-Host 可能只是写入流 6。
但是Write-Information的输出默认在控制台上是不可见的,但也会添加到信息变量中。
所以Write-Information 不能只与Write-Host 共享同一个IPC 管道。
Write-Warning 可以独立写入控制台和变量,所以这里也只能使用一个流/管道。
看看我的图表来解决这个问题。

Receive-Job输出传输图:

您可以通过重定向代码块中的流 1-6 和脚本中的流 1 或 2 来验证图表。

|<-------- code block process -------->|<-- IPC -->|<-------------------- script process ------------------->|
Method              Preference   Stream                 Stream/Variable           Console output

Write-Out           *        --> 1      --> PIPE 1  --> 1                     --> Console.Out.Write(gray)
                                            PIPE 1  --> Out Variable
Write-Error         *        --> 2      --> PIPE 2  --> 2                     --> Console.Out.Write(red)
                                            PIPE 2  --> Error Variable
Write-Warning       Continue ----??????---> PIPE 3  --> Warning Variable
Write-Warning       Continue --> 3      --> PIPE 4                            --> Console.Out.Write(yellow)
Write-Verbose       Continue --> 4      --> PIPE 4                            --> Console.Out.Write(yellow)
Write-Debug         Continue --> 5      --> PIPE 4                            --> Console.Out.Write(yellow)
Write-Information   Continue --> 6      --> PIPE 6                            --> Console.Out.Write(gray)
Write-Information   *        ----??????---> PIPE 5  --> Information Variable
Write-Host          *        ----??????---> PIPE 5  --> Information Variable
Write-Host          *        --> 6      --> PIPE 6                            --> Console.Out.Write(gray)

IPC : Inter Process Communication
*   : always, independent from Preference or has no own Preference

您不能在Write-InformationWrite-Warning 之后添加重定向来防止存储在它们的变量中。
如果您在方法之后重定向 3 和 6,那么它只会影响控制台输出,而不影响变量存储。
只有当$InformationPreference(非默认)或$WarningPreference(默认)设置为Continue时,它们才会写入流6或3,它们总是以灰色或黄色写入脚本进程的控制台。
只有Write-Warning 需要优先Continue 才能存储在其变量中,Write-Informations 始终写入其变量。

问题:

  • “Write-Warning”和“Write-Information”如何将它们的输出传递给脚本进程中分配给它们的变量?
    (它们不能使用流 7、8、9,因为它们不存在于 Windows 中。)

最佳实践:

Job-Start 的调用之后,您应该在Start-Sleep 1-3 秒内给代码块时间来启动或失败。
然后第一次使用Receive-Job获取当前进度,启动调试信息,警告或错误。
你不应该使用Wait-Job,而是使用你自己的循环来检查作业的运行状态并自己检查超时。
在那个自己的等待循环中,您每隔 X 秒调用一次Receive-Job,以从代码块进程中获取进度、调试和错误信息。
当job的状态为finishedfailed时,最后一次调用Receive-Job获取所有buffer的剩余数据。

要重定向/捕获流 1(成功)和 2(错误),您可以在 Receive-Job 行中使用正常重定向或存储到变量中。
要捕获流 3(警告)和 6(信息和Write-Host),您必须使用变量存储。
您不能直接重定向或捕获流 4(详细)或流 5(调试),但您可以将代码块中的这些流重定向(4&gt;&amp;1 or 5&gt;&amp;1)到流 1(成功)以将它们添加到输出变量。

要抑制Write-OutputWrite-Error 的控制台输出,您只需重定向Receive-Job 行中的流1 或2。
您不必抑制Write-InformationWrite-VerboseWrite-Debug 的控制台输出,因为它们不会使用默认首选项写入控制台。
如果您想在分配的变量中捕获Write-Information 的输出而没有控制台输出,则必须重定向流6:Write-Information &lt;message&gt; 6&gt;$null
要抑制Write-WarningWrite-Host 的控制台输出,您必须在其调用行中重定向流3 或6:Write-Warning &lt;message&gt; 3&gt;$nullWrite-Host &lt;message&gt; 6&gt;$null

注意:

如果您在代码块中重定向流成功(1)或错误(2),它们将不会被转移到脚本进程,不会写入控制台,也不会存储在输出或错误变量中。

【问题讨论】:

  • 你真是太复杂了。例如,Write-Error 不使用Console.Out.WriteLine,它写为ErrorRecord,它取决于主机如何呈现给用户。您还可以通过 stdin 和 stdout 传递所有六个 PowerShell 编号流(加上一些未编号的流)。
  • blogs.technet.microsoft.com/heyscriptingguy/2015/07/04/… 上对流及其处理方式进行了很好的描述
  • 我知道我的问题有点复杂,我尝试将其简化为这个简单的问题:PowerShell 如何将由Write-Information 创建的 InformationRecord 从脚本块进程传输到脚本进程和当信息流被重定向到脚本块中的 $null 时,将其写入那里的变量?
  • @PetSerAl,你说的未编号流是什么?
  • @andiDo 你说的未编号的流是什么? 例如,主机流。 当信息流被重定向到脚本块中的 $null 时,PowerShell 如何将由Write-Information 创建的 InformationRecord 从脚本块进程传输到脚本进程并将其写入那里的变量? 在您的代码中,信息流在哪里重定向到 $null?我没有看到任何未注释的信息流重定向。

标签: powershell asynchronous io-redirection start-job


【解决方案1】:

您的术语使用有点难以理解,但我会以我有限的经验尽力而为。

Write-Host 的输出默认写入控制台,并将其输出添加到信息变量中。
所以 Write-Host 可能只是写入流 6。
但是Write-Information的输出默认在控制台是不可见的,但也会添加到信息变量中。
所以 Write-Information 不能只与 Write-Host 共享同一个 IPC 管道。

首先,我在某处读到(不记得所以无法链接,抱歉)并亲自确认Write-HostWrite-Information 实际上使用相同的流。然而,Write-Host 本质上是Write-Information 的一个特例,它忽略了首选项变量并始终写入。所以我希望Write-Information 在相应的首选项变量设置正确时出现在其相应的变量中。

并且Write-Warning可以独立写入控制台和变量,所以这里也只能使用一个流/管道。

此观察结果可能是一种设计选择。 (我在这里猜测)我希望它的工作方式类似于 Tee-Object cmdlet,因此它确实可以写入控制台和变量,尽管它只是一个流。

$result = 'some string' | Tee-Object -Variable var
Write-Host $result
Write-Host $var
# same string in both variables

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-26
    • 2012-08-18
    相关资源
    最近更新 更多