2014-06-23 53 views
11

我想从一个在Powershell脚本中启动并将其异步显示到控制台的进程捕获stdout和stderr。我已经通过MSDN和other blogs发现了一些文档。如何在PowerShell中异步捕获进程输出?

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

$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() 

在这个例子中,我期待看到程序执行结束前的“喜”在命令行上的输出,因为OutputDataReceived事件应该已经触发。

我试过这个使用其他可执行文件 - java.exe,git.exe等。所有这些都有相同的效果,所以我留下来认为有一些简单的,我不理解或错过了。还有什么需要做异步读取标准输出?

回答

18

不幸的是,异步阅读并不是那么容易,如果你想正确地做到这一点。如果调用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 
} 

它捕获标准输出,标准错误和退出代码。示例用法:

有关更多信息和替代实现(在C#中),请阅读this blog post

+0

来关闭该过程。不幸的是,运行此代码后,我没有得到任何stdout或stderr。 – Ci3

+0

@ChrisHarris重新测试(在PS 2.0中),它对我有用。你有什么异常吗?当你直接运行相同的命令时,你会得到任何输出吗? –

+0

我得到StdOut,StdErr的空值返回的对象。退出码是“0”。我在等待ping.exe的输出,包括回复,字节,时间等。是吗?我完全按照你在这里的速度跑它。我正在运行Powershell 4.嗯,只是在Powershell 2上运行它,它按预期工作! – Ci3

6

根据Alexander Obersht's answer我创建了一个使用超时和异步Task类而不是事件处理函数的函数。 据Mike Adelson

不幸的是,这个方法(事件处理程序)提供了没有办法知道 当接收数据的最后一位。因为一切都是异步的,所以在WaitForExit()返回后,有可能(并且我已经观察到这种情况)发生了事件 fire。

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 
} 
+1

使用任务而不是事件处理程序作为更安全的方法感谢您的分享!在PowerShell脚本中使用毫秒超时可能是矫枉过正的。我无法想象一个需要这样的精度的脚本,即使我可以,我也不确定PS是否能胜任这项任务。否则,这确实是一个更好的方法。在深入研究C#之前,我写了我的函数,深入了解了.NET中异步工作的方式,但现在是时候进行审查并把它提升一个档次了。 –

+0

你知道分流的方法吗?我想要允许写入和捕获。这种方式可以写入控制台,以便用户可以看到实时发生的事情,并且可以捕获输出,以便管道中的其他站点可以处理它。 – Lucas

+0

@Lucas,请尝试ConsoleCopy类http://stackoverflow.com/a/6927051/52277 –

2

我无法得到任何的这些例子与PS 4.0工作。

我想跑从八达通部署包puppet apply(通过Deploy.ps1),看看在“实时”的输出,而不是等待进程结束(一小时后),所以我想出了以下内容:

# Deploy.ps1 

$procTools = @" 

using System; 
using System.Diagnostics; 

namespace Proc.Tools 
{ 
    public static class exec 
    { 
    public static int runCommand(string executable, string args = "", string cwd = "", string verb = "runas") { 

     //* Create your Process 
     Process process = new Process(); 
     process.StartInfo.FileName = executable; 
     process.StartInfo.UseShellExecute = false; 
     process.StartInfo.CreateNoWindow = true; 
     process.StartInfo.RedirectStandardOutput = true; 
     process.StartInfo.RedirectStandardError = true; 

     //* Optional process configuration 
     if (!String.IsNullOrEmpty(args)) { process.StartInfo.Arguments = args; } 
     if (!String.IsNullOrEmpty(cwd)) { process.StartInfo.WorkingDirectory = cwd; } 
     if (!String.IsNullOrEmpty(verb)) { process.StartInfo.Verb = verb; } 

     //* Set your output and error (asynchronous) handlers 
     process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler); 
     process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler); 

     //* Start process and handlers 
     process.Start(); 
     process.BeginOutputReadLine(); 
     process.BeginErrorReadLine(); 
     process.WaitForExit(); 

     //* Return the commands exit code 
     return process.ExitCode; 
    } 
    public static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) { 
     //* Do your stuff with the output (write to console/log/StringBuilder) 
     Console.WriteLine(outLine.Data); 
    } 
    } 
} 
"@ 

Add-Type -TypeDefinition $procTools -Language CSharp 

$puppetApplyRc = [Proc.Tools.exec]::runCommand("ruby", "-S -- puppet apply --test --color false ./manifests/site.pp", "C:\ProgramData\PuppetLabs\code\environments\production"); 

if ($puppetApplyRc -eq 0) { 
    Write-Host "The run succeeded with no changes or failures; the system was already in the desired state." 
} elseif ($puppetApplyRc -eq 1) { 
    throw "The run failed; halt" 
} elseif ($puppetApplyRc -eq 2) { 
    Write-Host "The run succeeded, and some resources were changed." 
} elseif ($puppetApplyRc -eq 4) { 
    Write-Warning "WARNING: The run succeeded, and some resources failed." 
} elseif ($puppetApplyRc -eq 6) { 
    Write-Warning "WARNING: The run succeeded, and included both changes and failures." 
} else { 
    throw "Un-recognised return code RC: $puppetApplyRc" 
} 

幸得T30Stefan Goßner

0

这里的例子都是有用的,但并不能完全满足我的使用情况。我不想调用该命令并退出。我想打开命令提示符,发送输入,读取输出,然后重复。这是我的解决方案。

创建Utils.CmdManager.cs

using System; 
using System.Diagnostics; 
using System.Text; 
using System.Threading; 

namespace Utils 
{ 
    public class CmdManager : IDisposable 
    { 
     const int DEFAULT_WAIT_CHECK_TIME = 100; 
     const int DEFAULT_COMMAND_TIMEOUT = 3000; 

     public int WaitTime { get; set; } 
     public int CommandTimeout { get; set; } 

     Process _process; 
     StringBuilder output; 

     public CmdManager() : this("cmd.exe", null, null) { } 
     public CmdManager(string filename) : this(filename, null, null) { } 
     public CmdManager(string filename, string arguments) : this(filename, arguments, null) { } 

     public CmdManager(string filename, string arguments, string verb) 
     { 
      WaitTime = DEFAULT_WAIT_CHECK_TIME; 
      CommandTimeout = DEFAULT_COMMAND_TIMEOUT; 

      output = new StringBuilder(); 

      _process = new Process(); 
      _process.StartInfo.FileName = filename; 
      _process.StartInfo.RedirectStandardInput = true; 
      _process.StartInfo.RedirectStandardOutput = true; 
      _process.StartInfo.RedirectStandardError = true; 
      _process.StartInfo.CreateNoWindow = true; 
      _process.StartInfo.UseShellExecute = false; 
      _process.StartInfo.ErrorDialog = false; 
      _process.StartInfo.Arguments = arguments != null ? arguments : null; 
      _process.StartInfo.Verb = verb != null ? verb : null; 

      _process.EnableRaisingEvents = true; 
      _process.OutputDataReceived += (s, e) => 
      { 
       lock (output) 
       { 
        output.AppendLine(e.Data); 
       }; 
      }; 
      _process.ErrorDataReceived += (s, e) => 
      { 
       lock (output) 
       { 
        output.AppendLine(e.Data); 
       }; 
      }; 

      _process.Start(); 
      _process.BeginOutputReadLine(); 
      _process.BeginErrorReadLine(); 
      _process.StandardInput.AutoFlush = true; 
     } 

     public void RunCommand(string command) 
     { 
      _process.StandardInput.WriteLine(command); 
     } 

     public string GetOutput() 
     { 
      return GetOutput(null, CommandTimeout, WaitTime); 
     } 

     public string GetOutput(string endingOutput) 
     { 
      return GetOutput(endingOutput, CommandTimeout, WaitTime); 
     } 

     public string GetOutput(string endingOutput, int commandTimeout) 
     { 
      return GetOutput(endingOutput, commandTimeout, WaitTime); 
     } 

     public string GetOutput(string endingOutput, int commandTimeout, int waitTime) 
     { 
      string tempOutput = ""; 
      int tempOutputLength = 0; 
      int amountOfTimeSlept = 0; 

      // Loop until 
      // a) command timeout is reached 
      // b) some output is seen 
      while (output.ToString() == "") 
      { 
       if (amountOfTimeSlept >= commandTimeout) 
       { 
        break; 
       } 

       Thread.Sleep(waitTime); 
       amountOfTimeSlept += waitTime; 
      } 

      // Loop until: 
      // a) command timeout is reached 
      // b) endingOutput is found 
      // c) OR endingOutput is null and there is no new output for at least waitTime 
      while (amountOfTimeSlept < commandTimeout) 
      { 
       if (endingOutput != null && output.ToString().Contains(endingOutput)) 
       { 
        break; 
       } 
       else if(endingOutput == null && tempOutputLength == output.ToString().Length) 
       { 
        break; 
       } 

       tempOutputLength = output.ToString().Length; 

       Thread.Sleep(waitTime); 
       amountOfTimeSlept += waitTime; 
      } 

      // Return the output and clear the buffer 
      lock (output) 
      { 
       tempOutput = output.ToString(); 
       output.Clear(); 
       return tempOutput.TrimEnd(); 
      } 
     } 

     public void Dispose() 
     { 
      _process.Kill(); 
     } 
    } 
} 

然后从PowerShell中添加的类别,并使用它。

Add-Type -Path ".\Utils.CmdManager.cs" 

$cmd = new-object Utils.CmdManager 
$cmd.GetOutput() | Out-Null 

$cmd.RunCommand("whoami") 
$cmd.GetOutput() 

$cmd.RunCommand("cd") 
$cmd.GetOutput() 

$cmd.RunCommand("dir") 
$cmd.GetOutput() 

$cmd.RunCommand("cd Desktop") 
$cmd.GetOutput() 

$cmd.RunCommand("cd") 
$cmd.GetOutput() 

$cmd.RunCommand("dir") 
$cmd.GetOutput() 

$cmd.Dispose() 

不要忘记调用Dispose()功能在最后清理是在后台运行的进程。或者,您可以通过运行如$cmd.RunCommand("exit")