2014-06-23 46 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")