如何在PowerShell中异步捕获进程输出?

Ci3*_*Ci3 17 powershell events asynchronous

我想从我在Powershell脚本中启动的进程捕获stdout和stderr,并将其异步显示到控制台.我已经通过MSDN和其他博客找到了一些关于这样做的文档.

创建并运行下面的示例后,我似乎无法异步显示任何输出.所有输出仅在进程终止时显示.

$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "cmd.exe"
$ps.StartInfo.UseShellExecute = $false
$ps.StartInfo.RedirectStandardOutput = $true
$ps.StartInfo.Arguments = "/c echo `"hi`" `& timeout 5"

$action = { Write-Host $EventArgs.Data  }
Register-ObjectEvent -InputObject $ps -EventName OutputDataReceived -Action $action | Out-Null

$ps.start() | Out-Null
$ps.BeginOutputReadLine()
$ps.WaitForExit()
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我希望在程序执行结束之前在命令行上看到"hi"的输出,因为应该触发OutputDataReceived事件.

我已经尝试过使用其他可执行文件 - java.exe,git.exe等.所有这些都具有相同的效果,所以我不得不认为有一些我不理解或错过的简单.还需要做什么才能异步读取stdout?

Ale*_*sht 34

不幸的是,如果你想要正确地进行异步读取并不那么容易.如果你在没有超时的情况下调用WaitForExit(),你可以使用像我写的这个函数(基于C#代码):

function Invoke-Executable {
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb
    )

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi

    # Creating string builders to store stdout and stderr.
    $oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
    $oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder

    # Adding event handers for stdout and stderr.
    $sScripBlock = {
        if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
            $Event.MessageData.AppendLine($EventArgs.Data)
        }
    }
    $oStdOutEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'OutputDataReceived' `
        -MessageData $oStdOutBuilder
    $oStdErrEvent = Register-ObjectEvent -InputObject $oProcess `
        -Action $sScripBlock -EventName 'ErrorDataReceived' `
        -MessageData $oStdErrBuilder

    # Starting process.
    [Void]$oProcess.Start()
    $oProcess.BeginOutputReadLine()
    $oProcess.BeginErrorReadLine()
    [Void]$oProcess.WaitForExit()

    # Unregistering events to retrieve process output.
    Unregister-Event -SourceIdentifier $oStdOutEvent.Name
    Unregister-Event -SourceIdentifier $oStdErrEvent.Name

    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $oStdOutBuilder.ToString().Trim();
        "StdErr"   = $oStdErrBuilder.ToString().Trim()
    })

    return $oResult
}
Run Code Online (Sandbox Code Playgroud)

它捕获stdout,stderr和退出代码.用法示例:

$oResult = Invoke-Executable -sExeFile 'ping.exe' -cArgs @('8.8.8.8', '-a')
$oResult | Format-List -Force 
Run Code Online (Sandbox Code Playgroud)

有关更多信息和替代实现(在C#中),请阅读此博客文章.


MiF*_*vil 11

根据Alexander Obersht的回答,我创建了一个使用超时和异步Task类而不是事件处理程序的函数.根据Mike Adelson的说法

不幸的是,这种方法(事件处理程序)无法知道何时接收到最后一位数据.因为一切都是异步的,所以在WaitForExit()返回后触发事件是可能的(我已经观察到了这一点).

function Invoke-Executable {
# from https://stackoverflow.com/a/24371479/52277
    # Runs the specified executable and captures its exit code, stdout
    # and stderr.
    # Returns: custom object.
# from http://www.codeducky.org/process-handling-net/ added timeout, using tasks
param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]$sExeFile,
        [Parameter(Mandatory=$false)]
        [String[]]$cArgs,
        [Parameter(Mandatory=$false)]
        [String]$sVerb,
        [Parameter(Mandatory=$false)]
        [Int]$TimeoutMilliseconds=1800000 #30min
    )
    Write-Host $sExeFile $cArgs

    # Setting process invocation parameters.
    $oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
    $oPsi.CreateNoWindow = $true
    $oPsi.UseShellExecute = $false
    $oPsi.RedirectStandardOutput = $true
    $oPsi.RedirectStandardError = $true
    $oPsi.FileName = $sExeFile
    if (! [String]::IsNullOrEmpty($cArgs)) {
        $oPsi.Arguments = $cArgs
    }
    if (! [String]::IsNullOrEmpty($sVerb)) {
        $oPsi.Verb = $sVerb
    }

    # Creating process object.
    $oProcess = New-Object -TypeName System.Diagnostics.Process
    $oProcess.StartInfo = $oPsi


    # Starting process.
    [Void]$oProcess.Start()
# Tasks used based on http://www.codeducky.org/process-handling-net/    
 $outTask = $oProcess.StandardOutput.ReadToEndAsync();
 $errTask = $oProcess.StandardError.ReadToEndAsync();
 $bRet=$oProcess.WaitForExit($TimeoutMilliseconds)
    if (-Not $bRet)
    {
     $oProcess.Kill();
    #  throw [System.TimeoutException] ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $outText = $outTask.Result;
    $errText = $errTask.Result;
    if (-Not $bRet)
    {
        $errText =$errText + ($sExeFile + " was killed due to timeout after " + ($TimeoutMilliseconds/1000) + " sec ") 
    }
    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
        "ExeFile"  = $sExeFile;
        "Args"     = $cArgs -join " ";
        "ExitCode" = $oProcess.ExitCode;
        "StdOut"   = $outText;
        "StdErr"   = $errText
    })

    return $oResult
}
Run Code Online (Sandbox Code Playgroud)

  • 感谢分享!在PowerShell脚本中使用毫秒进行超时可能有点过分.我无法想象一个需要这种精度的脚本,即使我可以,我也不确定PS是否能胜任这项任务.否则它确实是一种更好的方法.在我深入研究C#之前,我已经编写了我的函数,以完全理解异步在.NET中是如何工作的,但现在是时候进行审查并将其提升一个档次. (3认同)

Kin*_*Bob 5

我无法让这些示例与 PS 4.0 一起使用。

\n\n

我想puppet apply从 Octopus Deploy 包(通过Deploy.ps1)运行并“实时”查看输出,而不是等待该过程完成(一小时后),所以我想出了以下方法:

\n\n
# Deploy.ps1\n\n$procTools = @"\n\nusing System;\nusing System.Diagnostics;\n\nnamespace Proc.Tools\n{\n  public static class exec\n  {\n    public static int runCommand(string executable, string args = "", string cwd = "", string verb = "runas") {\n\n      //* Create your Process\n      Process process = new Process();\n      process.StartInfo.FileName = executable;\n      process.StartInfo.UseShellExecute = false;\n      process.StartInfo.CreateNoWindow = true;\n      process.StartInfo.RedirectStandardOutput = true;\n      process.StartInfo.RedirectStandardError = true;\n\n      //* Optional process configuration\n      if (!String.IsNullOrEmpty(args)) { process.StartInfo.Arguments = args; }\n      if (!String.IsNullOrEmpty(cwd)) { process.StartInfo.WorkingDirectory = cwd; }\n      if (!String.IsNullOrEmpty(verb)) { process.StartInfo.Verb = verb; }\n\n      //* Set your output and error (asynchronous) handlers\n      process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);\n      process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);\n\n      //* Start process and handlers\n      process.Start();\n      process.BeginOutputReadLine();\n      process.BeginErrorReadLine();\n      process.WaitForExit();\n\n      //* Return the commands exit code\n      return process.ExitCode;\n    }\n    public static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) {\n      //* Do your stuff with the output (write to console/log/StringBuilder)\n      Console.WriteLine(outLine.Data);\n    }\n  }\n}\n"@\n\nAdd-Type -TypeDefinition $procTools -Language CSharp\n\n$puppetApplyRc = [Proc.Tools.exec]::runCommand("ruby", "-S -- puppet apply --test --color false ./manifests/site.pp", "C:\\ProgramData\\PuppetLabs\\code\\environments\\production");\n\nif ( $puppetApplyRc -eq 0 ) {\n  Write-Host "The run succeeded with no changes or failures; the system was already in the desired state."\n} elseif ( $puppetApplyRc -eq 1 ) {\n  throw "The run failed; halt"\n} elseif ( $puppetApplyRc -eq 2) {\n  Write-Host "The run succeeded, and some resources were changed."\n} elseif ( $puppetApplyRc -eq 4 ) {\n  Write-Warning "WARNING: The run succeeded, and some resources failed."\n} elseif ( $puppetApplyRc -eq 6 ) {\n  Write-Warning "WARNING: The run succeeded, and included both changes and failures."\n} else {\n  throw "Un-recognised return code RC: $puppetApplyRc"\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

感谢T30Stefan Go\xc3\x9fner

\n