【问题标题】:Process.StandardOutput.Readline() is hanging when there is no output没有输出时 Process.StandardOutput.Readline() 挂起
【发布时间】:2021-12-07 16:22:52
【问题描述】:

注意:我正在尝试将packer.exe 作为后台进程运行,以解决azure-arm 构建器的特定问题,我需要观察输出。我没有使用
Start-Process,因为我不想使用中间文件来消耗输出。

我有以下代码设置packer.exe 在后台运行,这样我就可以使用它的输出并根据某个日志消息采取行动。这是一个较大脚本的一部分,但这是有问题的部分,行为不正确:

  $builderDir = ( Split-Path -Parent $PSCommandPath )
  Push-Location $builderDir
  
  # Set up the packer command to run asynchronously
  $pStartProps = @{
    FileName               = ( Get-Command -CommandType Application packer ).Source
    Arguments              = "build -var-file ""${builderDir}\win-dev.pkrvars.hcl"" -only ""azure-arm.base"" ."
    UseShellExecute        = $false
    RedirectStandardOutput = $true
    RedirectStandardError  = $false
    LoadUserProfile        = $true
  }
  $pStartInfo = New-Object ProcessStartInfo -Property $pStartProps

  $p = [Process]::Start($pStartInfo)

  while ( $null -ne ( $output = $p.StandardOutput.Readline() ) -or !$p.HasExited ) {
    # Do stuff
  }

基本上,while 条件的( $output = $p.StandardOutput.Readline() ) 部分似乎一直挂起,直到有更多输出可供读取。我不确定这是为什么,因为如果没有更多的输出,StreamReader.Readline()should either return the next line to be read, or null。关于我期望获得的日志消息,我对此进行了相当多的处理,因此当没有进一步的输出消耗时读取 STDOUT 时阻塞会使脚本无用。在packer.exe 继续执行时,它还在前台执行其他操作。

我可以在调试器中确认Readline() 确实读取了空行("" 的值),这似乎发生在没有进一步的输出消耗时。这可能是相切的,但这也会导致调试器出错。

发生此问题时,VSCode 调试器会在此位置上显示
$output = $p.StandardOutput.Readline() 突出显示几秒钟,然后调试器停止(一切都消失,不再跟踪变量等),直到 Readline() 停止阻塞和执行继续,此时调试器似乎重新初始化了跟踪的变量、监视的表达式等。所以当这种情况发生时我根本无法使用调试器。甚至
PowerShell Integrated Console(与调试器一起使用的那个)也挂起,我无法输入任何内容。


对于完整的上下文,这个脚本的目标是让packer.exe 在我不断循环到:

  1. 显示来自packer.exe 的更多输出
  2. 检查是否存在特定日志消息
  3. packer.exe一点时间尝试自己做它需要做的事情
  4. 如果等待时间过长,我会针对节点执行一个脚本,因为 packer.exe 应该自己完成的操作可能会失败
    • 我正在使用Invoke-AzVMRunCommand 来执行此操作,对于我正在解决的问题,在packer.exe 状态下无法执行此操作。它必须在 packer.exe 运行本身的带外执行。
  5. 在应用解决方法后继续构建,我只是继续将 packer.exe 的输出转发到控制台,直到进程退出

但是由于脚本在没有输出时挂起,所以第 4 步将永远无法工作,因为我必须给加壳程序时间来尝试自己完成配置,这就是我在第一名。


为什么Readline() 在这里阻塞?我做错了什么吗?无论我在 Windows PowerShell 还是 PowerShell Core 中运行我的脚本,都会出现此行为。

【问题讨论】:

  • 哈哈,很高兴知道。这个脚本可能最终会在 Azure 中的 *nix 构建器上运行,所以我会记住在 StreamReader 周围可能存在跨平台的奇怪之处,尽管从文件中读取是我试图避免的事情,所以它可能与我无关。不过,目前的目标是从工作站上获得这座建筑。这个打包机的东西让我把头发扯掉了!
  • 我认为 file 的使用是我的示例代码附带的——我只是为了方便而使用它。如果您将示例调整为您的Process 场景(而不是while ($true),使用while (-not $p.HasExited) 等等 - 尽管您可能希望确保在退出循环。
  • 是的,我不得不求助于布尔标志来结束输入,因为我依赖null,这意味着没有新信息可供阅读。但是,如果null 表示输出流已关闭,我应该能够假设该进程也已退出(尽管最后我仍然检查这个)。无论哪种情况,我都在尝试您的建议,但可能要到明天早上才能测试。
  • 也许只是检查$task.IsCompleted 之后循环会找到最后一行,如果有的话。
  • 我实现它的方式不需要它。所有这些都在循环的顶部完成,大部分代码都是读取该行后要做什么的逻辑。无论如何它都很好用,所以谢谢。

标签: .net powershell .net-core powershell-core


【解决方案1】:
  • StreamReader.ReadLine() 被设计阻止。

  • 有一个异步替代方案,.ReadLineAsync(),它返回一个Task<string> 实例,您可以通过其.IsCompleted 属性轮询 以完成,而不会阻塞您的前台线程(轮询是您在 PowerShell 中的唯一选择,因为它没有类似于 C# 的 await 的语言功能)。

这是一个简化的示例,重点关注从恰好是一个文件的StreamReader 实例的异步读取,新行只会定期添加到该文件中;使用 Ctrl-C 中止。

如果您将代码调整到您的标准输出读取 System.Diagnostics.Process 代码,我希望代码能够正常工作。

# Create a sample input file.
$n=3
1..$n > tmp.txt

# Open the file for reading, and convert it to a System.IO.StreamReader instance.
[IO.StreamReader] $reader = 
  [IO.File]::Open("$pwd/tmp.txt", 'Open', 'Read', 'ReadWrite')

try {
  $task = $reader.ReadLineAsync() # Start waiting for the first line.
  while ($true) { # Loop indefinitely to wait for new lines.
    if ($task.IsCompleted) {  # A new line has been received.
      $task.Result # Output
      # Start waiting for the next line.
      $task.Dispose(); $task = $reader.ReadLineAsync(); 
    }
    else { # No new line available yet, do other things.
      Write-Host '.' -NoNewline
      Start-Sleep 1
    }
    # Append a new line to the sample file every once in a while.
    if (++$n % 10 -eq 0) { $n >> tmp.txt }
  }
}
finally {
  $reader.Dispose()
}

【讨论】:

    【解决方案2】:

    来自标准输出的StreamReader 正在等待 IO - 来自StandardOutput 的流在获得更多数据或Process 关闭之前无法打开以供读取。在 TCP 流上使用 StreamReader 也有类似的问题,您可以在其中享受等待网络流量的乐趣。

    解决它的通常方法是让不同的任务通过异步读取来等待它,例如ReadLineAsync()。这对您的while 循环没有帮助,因为它仍然会等待相同的时间,然后才能继续读取输出的StreamReader

    如果packer.exe 的输出表现良好,您可以尝试使用powershell 作业的输出属性而不是[process]?我使用 ping 进行了测试,它给出了相同的等待 IO 锁定行为:

    # Locks up for full ping timeout example: 
    $pStartProps = @{ FileName  = 'ping.exe' ; Arguments = '10.bad.ip.0' ... }
    $p = [System.Diagnostics.Process]::Start($pStartInfo)
    
    # Locks when reading
    $output = $p.StandardOutput.ReadLine()
    while ( $output -ne $null ) {
      $output = $p.StandardOutput.ReadLine(); ## read the output
      # Do stuff
    }
    
    #####
    
    # Does not lock when using Job output instead:
    $job = Start-Job -ScriptBlock { ping.exe 10.bad.ip.0 }
    
    While ($job.State -ne 'Completed') {
      $job.ChildJobs[0].Output  ## read the output
      # Do stuff
    }
    

    【讨论】:

    • 这是 Start-Job 的一个很好的用法,但是这个脚本需要在打包程序产生输出时逐行处理输出。 Job.Output 一次生成作业的所有输出,即使在使用它之后,不是吗?
    • @BendertheGreatest 你是对的,它输出一个多行字符串。例如,您可以将$output 与前一个循环进行比较以检查新数据。不过,可能会有更好的解决方案,具体取决于您需要如何处理输出。
    • 是的,这就是我选择 process.start 路线的原因,但没有记得 Readline() 实际上是等待输出时的阻塞操作。这是我以前在 C# SE 时遇到过的事情(现在看起来像是上辈子了,哈哈)。根据我已经编写脚本的方式,ReadlineAsync() 更容易“加入”。
    【解决方案3】:

    如上使用StandardOutput.ReadLine(),您将输出重定向流设置为使用同步读取模式。这意味着当您调用$p.StandardOutput.ReadLine() 时,它的行为就像一个普通的函数调用。

    你调用它,然后等待返回值。如果到达流的末尾,则返回值是完整行$null。这意味着当正在运行的程序用输出填充System.Diagnostics.Process 输出流缓冲区时,您将继续成为一个优秀的同步 PowerShell 男孩并等待函数调用返回。一旦System.Diagnostics.Process 在流中检测到换行符,函数调用$p.StandardOutput.ReadLine() 就会返回您的数据,即完整的行

    这就是为什么它看起来像是阻塞的原因。它必须等到它有一个完整的行才能返回。

    另一个返回值$null 更有意义,因为知道StreamReader 对象用于许多不同的事物,这些事物可能具有不同的流结尾。如果您使用StreamReader 读取文本文件,则流/文件的结尾是您点击EOF 标记时。如果您正在读取字符数组,它可能是 \0 字符串终止符。 $null 在这种情况下是流的通用结束。

    对于System.Diagnostics.Process 对象,“流结束”的定义是流程完成时,而不是“我此时没有输出”。

    为了消除这种阻塞,您必须以异步方式执行您的阅读,即ReadLineAsync()

    不想踩到@mklement0 脚趾,因为他正确地解决了您的问题,方法是使用ReadLineAsync() 使其异步,并通过轮询将检查/验证脚本保持在同一范围内。但另一种异步读取数据的方法是使用Process.BeginOutputReadLine()OutputDataReceived 事件在您有更多数据时异步触发脚本块。

    # Script to run when you have another line of data
    $NewOutputSB = {
        param([object]$sender, [System.Diagnostics.DataReceivedEventArgs]$e)
        Write-Host "  #  " $e.Data
    }
    
    $pStartProps = @{
        FileName               = ( Get-Command -CommandType Application ping.exe ).Source
        Arguments              = "8.8.8.8"
        UseShellExecute        = $false
        RedirectStandardOutput = $true
        RedirectStandardError  = $false
        LoadUserProfile        = $true
    }
    $pStartInfo = New-Object System.Diagnostics.ProcessStartInfo -Property $pStartProps
    
    $p = New-Object System.Diagnostics.Process
    
    # Register the Event Handler
    $EventSub = Register-ObjectEvent $p -EventName OutputDataReceived -Action $NewOutputSB
    
    $p.StartInfo = $pStartInfo
    
    $p.Start() | Out-Null
    
    # Begin asynchronous reading on the Output stream
    $p.BeginOutputReadLine()
    
    # Do other work while we wait for it to finish running...
    while ( !$p.HasExited ) {
        # Do stuff
        Start-Sleep -Milliseconds 250
        Write-Host "--- Do stuff"
    }
    
    # Cleanup Event Handler
    Unregister-Event -SubscriptionId $EventSub.Id
    

    运行此代码后,输出显示以异步模式运行的流:

    PS C:\>
      #   
      #   Pinging 8.8.8.8 with 32 bytes of data:
      #   Reply from 8.8.8.8: bytes=32 time=28ms TTL=119
    --- Do stuff
    --- Do stuff
    --- Do stuff
      #   Reply from 8.8.8.8: bytes=32 time=28ms TTL=119
    --- Do stuff
    --- Do stuff
    --- Do stuff
    --- Do stuff
      #   Reply from 8.8.8.8: bytes=32 time=31ms TTL=119
    --- Do stuff
    --- Do stuff
    --- Do stuff
    --- Do stuff
      #   Reply from 8.8.8.8: bytes=32 time=28ms TTL=119
      #
      #   Ping statistics for 8.8.8.8:
      #       Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
      #   Approximate round trip times in milli-seconds:
      #       Minimum = 28ms, Maximum = 31ms, Average = 28ms
      #
    --- Do stuff
    

    【讨论】:

    • 我不知道有一个可以监听的 OutputDataReceived 事件。如果我围绕这个范式设计它,这将使我的脚本更容易。以后我会记住的。
    猜你喜欢
    • 1970-01-01
    • 2011-07-27
    • 2016-05-25
    • 2019-06-09
    • 2021-09-03
    • 2016-03-04
    • 1970-01-01
    • 1970-01-01
    • 2013-07-25
    相关资源
    最近更新 更多