powershell - 检测管道中外部程序已退出的方法

标签 powershell shell

在管道中

GenerateLotsOfText | external-program | ConsumeExternalProgramOutput
当外部程序退出时,管道会继续运行直到 GenerateLotsOfText 完成。假设外部程序只生成一行输出,那么
GenerateLotsOfText | external-program | Select-Object -First 1 | ConsumeExternalProgramOutput
将从外部程序生成输出的那一刻起停止整个管道。这是我正在寻找的行为,但需要注意的是,当外部程序不生成任何输出但过早退出时(例如,因为 ctrl-c),管道仍会继续运行。所以我现在正在寻找一种很好的方法来检测何时发生并在它发生时终止管道。
似乎可以编写一个使用 System.Diagnostics.Process 的 cmdlet然后使用 Register-ObjectEvent监听“退出”事件,但这与处理 I/O 流等相当相关,因此我宁愿找到另一种方法。
我认为几乎所有其他 shell 都通过 || 内置了这个“程序退出时产生输出”的内容。和 &&这确实有效:
GenerateLotsOfText | cmd /c '(external-program && echo FOO) || echo FOO' | Select-Object -First 1 | FilterOutFooString | ConsumeExternalProgramOutput
所以无论外部程序做什么,当它退出时总是会产生一行 FOO,所以管道会立即停止(然后 FilterOutFooString 只负责产生实际输出)。这不是特别“好”,并且有额外的开销,因为一切都需要通过 cmd 进行管道传输(或者我假设的任何其他 shell 都可以正常工作)。我希望pipeline chain operators native 允许这样做,但他们似乎不允许:尝试相同的语法会导致 Expressions are only allowed as the first element of a pipeline.链接确实按预期工作,只是不在管道中,例如这会产生前面提到的 ParserError:
GenerateLotsOfText | ((external-program && echo FOO) || echo FOO) | Select-Object -First 1
有没有另一种方法来实现这一点?
更新 其他可能的方法:
  • 使用单独的运行空间来轮询 $LASTEXITCODE在运行外部命令的运行空间中。虽然没有找到以线程安全的方式做到这一点的方法(例如,当管道运行时,不能从另一个线程调用 $otherRunspace.SessionStateProxy.GetVariable('LASTEXITCODE'))
  • 相同的想法,但要轮询其他内容:在 NativeCommandProcessor 中可以看出它会设置ExecutionFailed一旦外部进程退出,父管道上的失败标志,但我没有找到访问该标志的方法,更不用说以线程安全的方式
  • 我可能会使用 SteppablePipeline 做一些事情。如果我正确理解它,它会得到一个允许手动执行管道迭代的对象。这将允许检查 $LASTEXITCODE在迭代之间。

  • 在实现了最后一个非常简单的想法之后研究这个问题,以上都不是一个选项:进程退出代码基本上只有在 end 后才被确定。管道块运行,即在上游管道元素产生其所有输出之后

    最佳答案

    要明确:作为originally reported for Unix然后 for Windows , PowerShell 的当前行为(从 v7.2 开始)应被视为错误 这有望得到修复:PowerShell 应检测到 native (外部程序)何时退出,并应在该事件中停止所有上游命令。

    下面显示了一个 通用解决方法 ,然而,总是 减慢速度 有限制 :

  • 遗憾的是,从 v7.2 开始,PowerShell 没有提供停止上游命令的公共(public)功能(请参阅 GitHub feature request #);在内部,它使用私有(private)异常,正如 Select-Object -First 所使用的那样。 , 例如。下面的函数使用反射和临时编译的 C# 代码来抛出这个异常(也在 this answer 中演示)。这意味着在 session 中第一次调用函数时会支付编译的性能损失。

  • 定义函数 nw (“ native 包装器”),其源代码如下,然后按如下方式调用它,例如:
  • 在 Windows 上(假设 WSL;省略 -Verbose 以抑制详细输出):
    1..1e8 | nw wsl head -n 10 -Verbose
    
  • 在类 Unix 平台上(省略 -Verbose 以抑制详细输出):
    1..1e8 | nw head -n 10 -Verbose
    

  • 您将看到上游命令 - 大型输入数组的枚举 - 在 head 时终止终止(出于所述原因,上游命令的这种终止会在 session 中的第一次调用时导致性能损失)。
    实现说明 :
  • nw被实现为代理(包装器)功能,使用可步进的管道 - 参见 this answer想要查询更多的信息。

  • function nw {
      [CmdletBinding(PositionalBinding = $false)]
      param(
        [Parameter(Mandatory, ValueFromRemainingArguments)]
        [string[]] $ExeAndArgs
        ,
        [Parameter(ValueFromPipeline)]
        $InputObject
      )
      
      begin {
    
        # Split the arguments into executable name and its arguments.
        $exe, $exeArgs = $ExeAndArgs
    
        # Determine the process name matching the executable.
        $exeProcessName = [IO.Path]::GetFileNameWithoutExtension($exe)
    
        # The script block to use with the steppable pipeline.
        # Simply invoke the external program, and PowerShell will pipe input
        # to it when `.Process($_)` is called on the steppable pipeline in the `process` block,
        # including automatic formatting (implicit Out-String -Stream) for non-string input objects.
        # Also, $LASTEXITCODE is set as usual.
        $scriptCmd = { & $exe $exeArgs }
    
        # Create the steppable pipeline.
        try {
          $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
          # Record the time just before the external process is created.
          $beforeProcessCreation = [datetime]::Now
          # Start the pipeline, which creates the process for the external program
          # (launches it) - but note that when this call returns the
          # the process is *not* guaranteed to exist yet.
          $steppablePipeline.Begin($PSCmdlet)
        }
        catch {
          throw # Launching the external program failed.
        }
        # Get a reference to the newly launched process by its name and start time.
        # Note: This isn't foolproof, but probably good enough in practice.
        # Ideally, we'd also filter by having the current process as the parent process,
        # but that would require extra effort (a Get-CimInstance call on Windows, a `ps` call on Unix)
        $i = 0
        while (-not ($ps = (Get-Process -ErrorAction Ignore $exeProcessName | Where-Object StartTime -GT $beforeProcessCreation | Select-Object -First 1))) {
          if (++$i -eq 61) { throw "A (new) process named '$exeProcessName' unexpectedly did not appear with in the timeout period or exited right away." }
          Start-Sleep -Milliseconds 50
        }
      }
      
      process {
        
        # Check if the process has exited prematurely.
        if ($ps.HasExited) {
    
          # Note: $ps.ExitCode isn't available yet (even $ps.WaitForExit() wouldn't help), 
          #       so we cannot use it in the verbose message.
          #       However, $LASTEXITCODE should be set correctly afterwards.
          Write-Verbose "Process '$exeProcessName' has exited prematurely; terminating upstream commands (performance penalty on first call in a session)..."
          
          $steppablePipeline.End()
    
          # Throw the private exception that stops the upstream pipeline
          # !! Even though the exception type can be obtained and instantiated in
          # !! PowerShell code as follows:
          # !!   [System.Activator]::CreateInstance([psobject].assembly.GetType('System.Management.Automation.StopUpstreamCommandsException'), $PSCmdlet)
          # !! it cannot be *thrown* in a manner that *doesn't* 
          # !! result in a *PowerShell error*. Hence, unfortunately, ad-hoc compilation
          # !! of C# code is required, which incurs a performance penalty on first call in a given session.
        (Add-Type -PassThru -TypeDefinition '
          using System.Management.Automation;
          namespace net.same2u.PowerShell {
            public static class CustomPipelineStopper {
              public static void Stop(Cmdlet cmdlet) {
                throw (System.Exception) System.Activator.CreateInstance(typeof(Cmdlet).Assembly.GetType("System.Management.Automation.StopUpstreamCommandsException"), cmdlet);
              }
            }
        }')::Stop($PSCmdlet)
        
        }
    
        # Pass the current pipeline input object to the target process
        # via the steppable pipeline.
        $steppablePipeline.Process($_)
    
      }
      
      end {
        $steppablePipeline.End()
      }
    }
    

    关于powershell - 检测管道中外部程序已退出的方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69822180/

    相关文章:

    powershell - 在 Powershell 中显示具有大小的目录结构

    Powershell - Get-AdGroupMember 超出限制

    linux - 创建可以导入变量并针对来自外部文本文件的变量执行代码的 bash 脚本

    powershell - throw 和 $pscmdlet.ThrowTerminateerror() 之间的区别?

    windows - 从文件中删除与模式匹配的所有行,但第一次出现的行除外

    Powershell 类继承行为

    linux - 当我按下键盘上的一个键并且它显示在 shell 上时,实际发生的 Action 路径是什么?

    shell - 需要解释ffmpeg和管道命令的细节

    linux - 查找具有某些属性的文件

    linux - 守护进程 : fatal: failed to tell if . ... 是安全的