当没有输出时 Process.StandardOutput.Readline() 挂起

Ben*_*est 9 .net powershell .net-core powershell-core

注意:我试图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
  }
Run Code Online (Sandbox Code Playgroud)

基本上,条件( $output = $p.StandardOutput.Readline() )的这一部分while似乎一直处于挂起状态,直到有更多输出可供读取。我不确定为什么会这样,因为StreamReader.Readline() 应该返回要读取的下一行,或者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 中运行脚本,都会出现此行为。

mkl*_*nt0 4

  • 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()
}
Run Code Online (Sandbox Code Playgroud)