2014-06-23 5 views
11

Ich möchte stdout und stderr von einem Prozess erfassen, den ich in einem Powershell-Skript starte und asynchron zur Konsole anzeigen. Ich habe eine Dokumentation über MSDN und other blogs gefunden.Wie wird die Prozessausgabe asynchron in der Powershell erfasst?

Nachdem ich das folgende Beispiel erstellt und ausgeführt habe, kann ich scheinbar keine Ausgaben asynchron anzeigen lassen. Die gesamte Ausgabe wird nur angezeigt, wenn der Prozess beendet wird.

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

In diesem Beispiel erwarte ich die Ausgabe von „hallo“ auf der Kommandozeile vor dem Ende der Programmausführung zu sehen, weil das OutputDataReceived Ereignis ausgelöst werden soll.

Ich habe dies mit anderen ausführbaren Dateien versucht - java.exe, git.exe, etc. Alle von ihnen haben den gleichen Effekt, so dass ich denke, dass es etwas einfaches ist, dass ich nicht verstehe oder verpasst haben. Was muss noch getan werden, um stdout asynchron zu lesen?

Antwort

18

Leider ist asynchrones Lesen nicht so einfach, wenn Sie es richtig machen wollen. Wenn Sie WaitForExit() ohne Timeout nennen könnte man so etwas für diese Funktion I (auf Basis von C# -Code) schrieb:

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 
} 

Es stdout erfasst, stderr und Exit-Code. Beispiel Nutzung:

Für mehr Informationen und alternative Implementierungen (in C#) lesen this blog post.

+0

schließen können, ich bekomme keine stdout oder stderr nach diesem Code ausgeführt wird. – Ci3

+0

@ChrisHarris Wieder getestet (in PS 2.0) und es funktioniert für mich. Hast du irgendeine Ausnahme? Erhalten Sie eine Ausgabe, wenn Sie denselben Befehl direkt ausführen? –

+0

Ich bekomme das Objekt mit Null-Werten für StdOut, StdErr zurückgegeben. Der Beendigungscode ist "0". Ich erwartete die Ausgabe von ping.exe mit einer Antwort, die Bytes, die Zeit, etc. Ist das richtig? Ich habe es genau so ausgeführt, wie du es hier hast. Ich betreibe Powershell 4. Ah, lief es einfach auf Powershell 2, und es funktioniert wie erwartet! – Ci3

6

Basierend auf Alexander Obersht's answer Ich habe eine Funktion erstellt, die Timeout und asynchrone Task-Klassen anstelle von Event-Handlern verwendet. Nach Mike Adelson

Leider ist diese Methode (Event-Handler) bietet keine Möglichkeit, zu wissen, wann das letzte Bit von Daten empfangen wurde. Da alles asynchron ist, ist es möglich (und ich habe dies beobachtet) für Ereignisse zu Feuer nach WaitForExit() zurückgegeben hat.

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

Dank für die gemeinsame Nutzung Aufgaben anstelle von Event-Handler verwenden! Die Verwendung von Millisekunden für die Zeitüberschreitung in einem PowerShell-Skript ist wahrscheinlich übertrieben. Ich kann mir ein Skript nicht vorstellen, bei dem eine solche Präzision erforderlich wäre und selbst wenn ich könnte, bin ich mir nicht sicher, ob PS dieser Aufgabe gewachsen ist. Sonst ist es in der Tat ein besserer Ansatz. Ich schrieb meine Funktion, bevor ich in C# tief genug getaucht, um zu verstehen, wie async in .NET gearbeitet, aber jetzt ist es Zeit zu überprüfen, und nehmen Sie eine Kerbe. –

+0

Sie wissen, wie Sie den Stream teilen können? Ich möchte entweder Schreib- und/oder Capture-Möglichkeiten haben. Auf diese Weise Fortschritte konnten in die Konsole geschrieben werden, so dass der Benutzer, was los Live sehen kann, und der Ausgang eingefangen werden, so dass andere Stationen entlang der Pipeline es verarbeiten können. – Lucas

+0

@Lucas, versuchen ConsoleCopy Klasse http://stackoverflow.com/a/6927051/52277 –

2

Ich konnte auch nicht von diesen Beispielen erhalten mit PS 4.0 zu arbeiten.

Ich wollte puppet apply von einem Octopus Deploy-Paket (über Deploy.ps1) und sehen Sie die Ausgabe in „Echtzeit“ anstatt warten, für das Verfahren (eine Stunde später) beenden laufen, so kam ich mit der Follow-up:

# 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" 
} 

Kredit geht an T30 und Stefan Goßner

0

Die hier Beispiele alle nützlich sind, aber mein Anwendungsfall nicht vollständig gerecht wird. Ich wollte den Befehl nicht aufrufen und beenden. Ich wollte eine Eingabeaufforderung öffnen, Eingabe senden, die Ausgabe lesen und wiederholen. Hier ist meine Lösung dafür.

erstellen 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(); 
     } 
    } 
} 

dann von Powershell die Klasse hinzuzufügen und zu nutzen.

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

Vergessen Sie nicht, die Dispose() Funktion am Ende des Prozesses zu bereinigen, zu nennen, die im Hintergrund ausgeführt wird. Alternativ können Sie auch diesen Prozess Leider indem Sie so etwas wie $cmd.RunCommand("exit")

Verwandte Themen