2014-06-23 6 views
11

Desidero acquisire stdout e stderr da un processo che avvio in uno script PowerShell e visualizzarlo in modo asincrono sulla console. Ho trovato della documentazione su come farlo tramite MSDN e other blogs.Come acquisire l'output del processo in modo asincrono in PowerShell?

Dopo aver creato ed eseguito l'esempio di seguito, non riesco a visualizzare nessun output in modo asincrono. Tutto l'output viene visualizzato solo quando il processo termina.

$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 questo esempio, mi aspettavo di vedere l'uscita di "hi" sulla riga di comando prima della fine dell'esecuzione del programma perché l'evento OutputDataReceived avrebbe dovuto essere innescato.

Ho provato questo usando altri eseguibili - java.exe, git.exe, ecc. Tutti hanno lo stesso effetto, quindi mi viene lasciato pensare che c'è qualcosa di semplice che non capisco o Hanno perso. Che altro si deve fare per leggere lo stdout in modo asincrono?

risposta

18

Sfortunatamente la lettura asincrona non è così facile se si vuole farlo correttamente. Se si chiama WaitForExit() senza timeout si potrebbe usare qualcosa di simile a questa funzione che ho scritto (sulla base di codice 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 
} 

Cattura stdout, stderr e codice di uscita. Esempio di utilizzo:

Per maggiori informazioni e implementazioni alternative (in C#) leggere this blog post.

+0

Sfortunatamente, non ottengo alcun stdout o stderr dopo aver eseguito questo codice. – Ci3

+0

@ChrisHarris Re-testato (in PS 2.0) e funziona per me. Hai qualche eccezione? Ottieni un output quando esegui lo stesso comando direttamente? –

+0

Ottengo l'oggetto restituito con valori null per StdOut, StdErr. Il codice di uscita è "0". Mi aspettavo l'output di ping.exe con una risposta, i byte, tempo, ecc. È giusto? L'ho eseguito esattamente come lo hai tu qui. Sto eseguendo Powershell 4. Ah, l'ho appena eseguito su Powershell 2, e funziona come previsto! – Ci3

6

Basato su Alexander Obersht's answer Ho creato una funzione che utilizza le classi di attività timeout e asincrone invece dei gestori di eventi. Secondo Mike Adelson

Sfortunatamente, questo metodo (gestori di eventi) fornisce alcun modo di sapere quando è stato ricevuto l'ultimo bit di dati. Perché tutto è asincrono, è possibile (e ho osservato questo) per gli eventi a l'incendio dopo che WaitForExit() è tornato.

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

Grazie per aver condiviso! L'utilizzo di millisecondi per il timeout in uno script PowerShell è probabilmente eccessivo. Non riesco a immaginare uno script in cui sia richiesta una tale precisione e, anche se potessi, non sono sicuro che PS sia all'altezza del compito. Altrimenti è davvero un approccio migliore. Ho scritto la mia funzione prima di immergermi in C# abbastanza in profondità per comprendere appieno il funzionamento di async in .NET, ma ora è il momento di rivederlo e portarlo su una tacca. –

+0

Sei a conoscenza di un modo per dividere il flusso? Voglio consentire l'uno o l'altro di scrivere e acquisire. In questo modo i progressi potrebbero essere scritti sulla console in modo che l'utente possa vedere cosa sta succedendo dal vivo, E l'output potrebbe essere catturato in modo che altre fermate lungo la pipeline possano elaborarlo. – Lucas

+0

@Lucas, prova la classe ConsoleCopy http://stackoverflow.com/a/6927051/52277 –

2

non ho potuto ottenere uno di questi esempi per lavorare con PS 4.0.

Volevo correre puppet apply da un pacchetto Octopus Deploy (via Deploy.ps1) e vedere l'output in "tempo reale", piuttosto che attendere che il processo finisca (un'ora più tardi), così mi si avvicinò con il seguente:

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

merito va a T30 e Stefan Goßner

0

Gli esempi qui sono tutti utili, ma non rispondeva completamente il mio caso d'uso. Non volevo invocare il comando ed uscire. Volevo aprire un prompt dei comandi, inviare input, leggere l'output e ripetere. Ecco la mia soluzione per questo.

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

Poi dal PowerShell aggiungere la classe e utilizzarlo.

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

Non dimenticare di chiamare la funzione Dispose() alla fine di ripulire il processo in esecuzione in background. In alternativa, è possibile chiudere tale processo eseguendo qualcosa come $cmd.RunCommand("exit")