ProcessStartInfo si blocca su “WaitForExit”? Perché?

Ho il codice seguente:

info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args)); info.CreateNoWindow = true; info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; info.RedirectStandardOutput = true; info.UseShellExecute = false; System.Diagnostics.Process p = System.Diagnostics.Process.Start(info); p.WaitForExit(); Console.WriteLine(p.StandardOutput.ReadToEnd()); //need the StandardOutput contents 

So che l’output del processo che sto iniziando è lungo circa 7 MB. L’esecuzione nella console di Windows funziona correttamente. Sfortunatamente a livello di codice questo si blocca indefinitamente a WaitForExit. Nota anche che il codice NON si blocca per uscite più piccole (come 3 KB).

È ansible che StandardOutput interno in ProcessStartInfo non possa bufferizzare 7 MB? Se sì, cosa dovrei fare invece? In caso contrario, cosa sto sbagliando?

    Il problema è che se si reindirizza StandardOutput e / o StandardError il buffer interno può riempirsi. Qualunque sia l’ordine che usi, può esserci un problema:

    • Se si attende l’uscita del processo prima di leggere StandardOutput il processo può bloccare il tentativo di scrittura su di esso, quindi il processo non termina mai.
    • Se si legge da StandardOutput utilizzando ReadToEnd, il processo può bloccarsi se il processo non chiude mai StandardOutput (ad esempio se non termina mai, o se è bloccato a scrivere su StandardError ).

    La soluzione consiste nell’utilizzare letture asincrone per garantire che il buffer non si riempia. Per evitare qualsiasi deadlock e raccogliere tutti gli output da StandardOutput e StandardError puoi farlo:

    EDIT: vedere le risposte di seguito per come evitare una ObjectDisposedException se si verifica il timeout.

     using (Process process = new Process()) { process.StartInfo.FileName = filename; process.StartInfo.Arguments = arguments; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; StringBuilder output = new StringBuilder(); StringBuilder error = new StringBuilder(); using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) { process.OutputDataReceived += (sender, e) => { if (e.Data == null) { outputWaitHandle.Set(); } else { output.AppendLine(e.Data); } }; process.ErrorDataReceived += (sender, e) => { if (e.Data == null) { errorWaitHandle.Set(); } else { error.AppendLine(e.Data); } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); if (process.WaitForExit(timeout) && outputWaitHandle.WaitOne(timeout) && errorWaitHandle.WaitOne(timeout)) { // Process completed. Check process.ExitCode here. } else { // Timed out. } } } 

    La documentazione per Process.StandardOutput dice di leggere prima di aspettare altrimenti puoi deadlock, snippet copiato di seguito:

      // Start the child process. Process p = new Process(); // Redirect the output stream of the child process. p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.FileName = "Write500Lines.exe"; p.Start(); // Do not wait for the child process to exit before // reading to the end of its redirected stream. // p.WaitForExit(); // Read the output stream first and then wait. string output = p.StandardOutput.ReadToEnd(); p.WaitForExit(); 

    La risposta di Mark Byers è eccellente, ma vorrei semplicemente aggiungere quanto segue: i delegati OutputDataReceived e ErrorDataReceived devono essere rimossi prima che l’outputWaitHandle e errorWaitHandle vengano eliminati. Se il processo continua a emettere dati dopo che il timeout è stato superato e quindi termina, le variabili outputWaitHandle e errorWaitHandle saranno accessibili dopo essere state eliminate.

    (FYI ho dovuto aggiungere questo avvertimento come una risposta in quanto non ho potuto commentare il suo post.)

    Il problema con ObjectDisposedException non gestito si verifica quando il processo è scaduto. In tal caso le altre parti della condizione:

     if (process.WaitForExit(timeout) && outputWaitHandle.WaitOne(timeout) && errorWaitHandle.WaitOne(timeout)) 

    non sono eseguiti Ho risolto questo problema in un modo seguente:

     using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) { using (process = new Process()) { // preparing ProcessStartInfo try { process.OutputDataReceived += (sender, e) => { if (e.Data == null) { outputWaitHandle.Set(); } else { outputBuilder.AppendLine(e.Data); } }; process.ErrorDataReceived += (sender, e) => { if (e.Data == null) { errorWaitHandle.Set(); } else { outputBuilder.AppendLine(e.Data); } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); if (process.WaitForExit(timeout)) { exitCode = process.ExitCode; } else { // timed out } output = outputBuilder.ToString(); } finally { outputWaitHandle.WaitOne(timeout); errorWaitHandle.WaitOne(timeout); } } } 

    Rob ha risposto e mi ha salvato poche ore in più di prove. Leggi il buffer di output / errore prima di attendere:

     // Read the output stream first and then wait. string output = p.StandardOutput.ReadToEnd(); p.WaitForExit(); 

    Abbiamo anche questo problema (o una variante).

    Prova quanto segue:

    1) Aggiungi un timeout a p.WaitForExit (nnnn); dove nnnn è in millisecondi.

    2) Metti la chiamata ReadToEnd prima della chiamata WaitForExit. Questo è ciò che abbiamo visto raccomandare MS.

    Questa è una soluzione più moderna e attendibile, basata su Task Parallel Library (TPL) per .NET 4.5 e versioni successive.

    Esempio di utilizzo

     try { var exitCode = await StartProcess( "dotnet", "--version", @"C:\", 10000, Console.Out, Console.Out); Console.WriteLine($"Process Exited with Exit Code {exitCode}!"); } catch (TaskCanceledException) { Console.WriteLine("Process Timed Out!"); } 

    Implementazione

     public static async Task StartProcess( string filename, string arguments, string workingDirectory= null, int? timeout = null, TextWriter outputTextWriter = null, TextWriter errorTextWriter = null) { using (var process = new Process() { StartInfo = new ProcessStartInfo() { CreateNoWindow = true, Arguments = arguments, FileName = filename, RedirectStandardOutput = outputTextWriter != null, RedirectStandardError = errorTextWriter != null, UseShellExecute = false, WorkingDirectory = workingDirectory } }) { process.Start(); var cancellationTokenSource = timeout.HasValue ? new CancellationTokenSource(timeout.Value) : new CancellationTokenSource(); var tasks = new List(3) { process.WaitForExitAsync(cancellationTokenSource.Token) }; if (outputTextWriter != null) { tasks.Add(ReadAsync( x => { process.OutputDataReceived += x; process.BeginOutputReadLine(); }, x => process.OutputDataReceived -= x, outputTextWriter, cancellationTokenSource.Token)); } if (errorTextWriter != null) { tasks.Add(ReadAsync( x => { process.ErrorDataReceived += x; process.BeginErrorReadLine(); }, x => process.ErrorDataReceived -= x, errorTextWriter, cancellationTokenSource.Token)); } await Task.WhenAll(tasks); return process.ExitCode; } } ///  /// Waits asynchronously for the process to exit. ///  /// The process to wait for cancellation. /// A cancellation token. If invoked, the task will return /// immediately as cancelled. /// A Task representing waiting for the process to end. public static Task WaitForExitAsync( this Process process, CancellationToken cancellationToken = default(CancellationToken)) { process.EnableRaisingEvents = true; var taskCompletionSource = new TaskCompletionSource(); EventHandler handler = null; handler = (sender, args) => { process.Exited -= handler; taskCompletionSource.TrySetResult(null); }; process.Exited += handler; if (cancellationToken != default(CancellationToken)) { cancellationToken.Register( () => { process.Exited -= handler; taskCompletionSource.TrySetCanceled(); }); } return taskCompletionSource.Task; } ///  /// Reads the data from the specified data recieved event and writes it to the /// . ///  /// Adds the event handler. /// Removes the event handler. /// The text writer. /// The cancellation token. /// A task representing the asynchronous operation. public static Task ReadAsync( this Action addHandler, Action removeHandler, TextWriter textWriter, CancellationToken cancellationToken = default(CancellationToken)) { var taskCompletionSource = new TaskCompletionSource(); DataReceivedEventHandler handler = null; handler = new DataReceivedEventHandler( (sender, e) => { if (e.Data == null) { removeHandler(handler); taskCompletionSource.TrySetResult(null); } else { textWriter.WriteLine(e.Data); } }); addHandler(handler); if (cancellationToken != default(CancellationToken)) { cancellationToken.Register( () => { removeHandler(handler); taskCompletionSource.TrySetCanceled(); }); } return taskCompletionSource.Task; } 

    Credito a EM0 per https://stackoverflow.com/a/17600012/4151626

    Le altre soluzioni (comprese quelle di EM0) sono ancora deadlock per la mia applicazione, a causa di timeout interni e dell’utilizzo di StandardOutput e StandardError dall’applicazione generata. Ecco cosa ha funzionato per me:

     Process p = new Process() { StartInfo = new ProcessStartInfo() { FileName = exe, Arguments = args, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true } }; p.Start(); string cv_error = null; Thread et = new Thread(() => { cv_error = p.StandardError.ReadToEnd(); }); et.Start(); string cv_out = null; Thread ot = new Thread(() => { cv_out = p.StandardOutput.ReadToEnd(); }); ot.Start(); p.WaitForExit(); ot.Join(); et.Join(); 

    Modifica: aggiunta inizializzazione di StartInfo al codice di esempio

    L’ho risolto in questo modo:

      Process proc = new Process(); proc.StartInfo.FileName = batchFile; proc.StartInfo.UseShellExecute = false; proc.StartInfo.CreateNoWindow = true; proc.StartInfo.RedirectStandardError = true; proc.StartInfo.RedirectStandardInput = true; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; proc.Start(); StreamWriter streamWriter = proc.StandardInput; StreamReader outputReader = proc.StandardOutput; StreamReader errorReader = proc.StandardError; while (!outputReader.EndOfStream) { string text = outputReader.ReadLine(); streamWriter.WriteLine(text); } while (!errorReader.EndOfStream) { string text = errorReader.ReadLine(); streamWriter.WriteLine(text); } streamWriter.Close(); proc.WaitForExit(); 

    Ho reindirizzato sia l’input, l’output e l’errore e gestito la lettura dai flussi di output e di errore. Questa soluzione funziona per SDK 7- 8.1, sia per Windows 7 che Windows 8

    Ho provato a creare una class che risolvesse il problema utilizzando la lettura del stream asincrono, prendendo in considerazione le risposte Mark Byers, Rob, stevejay. In questo modo ho capito che c’è un bug relativo alla lettura del stream di output del processo asincrono.

    Ho segnalato questo bug a Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/3119134

    Sommario:

    Non puoi farlo:

    process.BeginOutputReadLine (); Process.Start ();

    Riceverai System.InvalidOperationException: StandardOut non è stato reindirizzato o il processo non è ancora iniziato.

    ================================================== ================================================== ========================

    Quindi è necessario avviare la lettura dell’uscita asincrona dopo l’avvio del processo:

    Process.Start (); process.BeginOutputReadLine ();

    In questo modo, crea una condizione di competizione perché il stream di output può ricevere dati prima di impostarlo su asincrono:

     process.Start(); // Here the operating system could give the cpu to another thread. // For example, the newly created thread (Process) and it could start writing to the output // immediately before next line would execute. // That create a race condition. process.BeginOutputReadLine(); 

    ================================================== ================================================== ========================

    Quindi alcune persone potrebbero dire che devi solo leggere lo stream prima di impostarlo in modo asincrono. Ma lo stesso problema si verifica. Ci sarà una condizione di competizione tra la lettura sincrona e lo streaming in modalità asincrona.

    ================================================== ================================================== ========================

    Non esiste alcun modo per ottenere una lettura asincrona sicura di un stream di output di un processo nel modo in cui “Process” e “ProcessStartInfo” sono stati progettati.

    Probabilmente stai meglio usando la lettura asincrona come suggerito da altri utenti per il tuo caso. Ma dovresti sapere che potresti perdere alcune informazioni a causa delle condizioni della gara.

    Nessuna delle risposte di cui sopra sta facendo il lavoro.

    La soluzione di Rob si blocca e la soluzione “Mark Byers” ottiene l’eccezione disponibile (ho provato le “soluzioni” delle altre risposte).

    Così ho deciso di suggerire un’altra soluzione:

     public void GetProcessOutputWithTimeout(Process process, int timeoutSec, CancellationToken token, out string output, out int exitCode) { string outputLocal = ""; int localExitCode = -1; var task = System.Threading.Tasks.Task.Factory.StartNew(() => { outputLocal = process.StandardOutput.ReadToEnd(); process.WaitForExit(); localExitCode = process.ExitCode; }, token); if (task.Wait(timeoutSec, token)) { output = outputLocal; exitCode = localExitCode; } else { exitCode = -1; output = ""; } } using (var process = new Process()) { process.StartInfo = ...; process.Start(); string outputUnicode; int exitCode; GetProcessOutputWithTimeout(process, PROCESS_TIMEOUT, out outputUnicode, out exitCode); } 

    Questo codice ha il debug e funziona perfettamente.

    introduzione

    La risposta attualmente accettata non funziona (genera eccezioni) e ci sono troppe soluzioni alternative ma nessun codice completo. Questo ovviamente sta sprecando un sacco di tempo perché questa è una domanda popolare.

    Combinando la risposta di Mark Byers e la risposta di Karol Tyl, ho scritto un codice completo basato su come voglio usare il metodo Process.Start.

    uso

    L’ho usato per creare una finestra di dialogo di avanzamento attorno ai comandi git. Questo è il modo in cui l’ho usato:

      private bool Run(string fullCommand) { Error = ""; int timeout = 5000; var result = ProcessNoBS.Start( filename: @"C:\Program Files\Git\cmd\git.exe", arguments: fullCommand, timeoutInMs: timeout, workingDir: @"C:\test"); if (result.hasTimedOut) { Error = String.Format("Timeout ({0} sec)", timeout/1000); return false; } if (result.ExitCode != 0) { Error = (String.IsNullOrWhiteSpace(result.stderr)) ? result.stdout : result.stderr; return false; } return true; } 

    In teoria puoi anche combinare stdout e stderr, ma non l’ho provato.

    Codice

     public struct ProcessResult { public string stdout; public string stderr; public bool hasTimedOut; private int? exitCode; public ProcessResult(bool hasTimedOut = true) { this.hasTimedOut = hasTimedOut; stdout = null; stderr = null; exitCode = null; } public int ExitCode { get { if (hasTimedOut) throw new InvalidOperationException( "There was no exit code - process has timed out."); return (int)exitCode; } set { exitCode = value; } } } public class ProcessNoBS { public static ProcessResult Start(string filename, string arguments, string workingDir = null, int timeoutInMs = 5000, bool combineStdoutAndStderr = false) { using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) { using (var process = new Process()) { var info = new ProcessStartInfo(); info.CreateNoWindow = true; info.FileName = filename; info.Arguments = arguments; info.UseShellExecute = false; info.RedirectStandardOutput = true; info.RedirectStandardError = true; if (workingDir != null) info.WorkingDirectory = workingDir; process.StartInfo = info; StringBuilder stdout = new StringBuilder(); StringBuilder stderr = combineStdoutAndStderr ? stdout : new StringBuilder(); var result = new ProcessResult(); try { process.OutputDataReceived += (sender, e) => { if (e.Data == null) outputWaitHandle.Set(); else stdout.AppendLine(e.Data); }; process.ErrorDataReceived += (sender, e) => { if (e.Data == null) errorWaitHandle.Set(); else stderr.AppendLine(e.Data); }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); if (process.WaitForExit(timeoutInMs)) result.ExitCode = process.ExitCode; // else process has timed out // but that's already default ProcessResult result.stdout = stdout.ToString(); if (combineStdoutAndStderr) result.stderr = null; else result.stderr = stderr.ToString(); return result; } finally { outputWaitHandle.WaitOne(timeoutInMs); errorWaitHandle.WaitOne(timeoutInMs); } } } } } 

    So che questa cena è vecchia ma, dopo aver letto tutta questa pagina, nessuna delle soluzioni ha funzionato per me, anche se non ho provato Muhammad Rehan perché il codice era un po ‘difficile da seguire, anche se penso che fosse sulla buona strada . Quando dico che non ha funzionato non è del tutto vero, a volte funzionerebbe bene, immagino che abbia a che fare con la lunghezza dell’output prima di un marchio EOF.

    Ad ogni modo, la soluzione che ha funzionato per me è stata l’utilizzo di thread diversi per leggere StandardOutput e StandardError e scrivere i messaggi.

      StreamWriter sw = null; var queue = new ConcurrentQueue(); var flushTask = new System.Timers.Timer(50); flushTask.Elapsed += (s, e) => { while (!queue.IsEmpty) { string line = null; if (queue.TryDequeue(out line)) sw.WriteLine(line); } sw.FlushAsync(); }; flushTask.Start(); using (var process = new Process()) { try { process.StartInfo.FileName = @"..."; process.StartInfo.Arguments = $"..."; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); var outputRead = Task.Run(() => { while (!process.StandardOutput.EndOfStream) { queue.Enqueue(process.StandardOutput.ReadLine()); } }); var errorRead = Task.Run(() => { while (!process.StandardError.EndOfStream) { queue.Enqueue(process.StandardError.ReadLine()); } }); var timeout = new TimeSpan(hours: 0, minutes: 10, seconds: 0); if (Task.WaitAll(new[] { outputRead, errorRead }, timeout) && process.WaitForExit((int)timeout.TotalMilliseconds)) { if (process.ExitCode != 0) { throw new Exception($"Failed run... blah blah"); } } else { throw new Exception($"process timed out after waiting {timeout}"); } } catch (Exception e) { throw new Exception($"Failed to succesfully run the process.....", e); } } } 

    Spero che questo aiuti qualcuno, che ha pensato che potesse essere così difficile!

    Dopo aver letto tutti i post qui, ho optato per la soluzione consolidata di Marko Avlijaš. Tuttavia , non ha risolto tutti i miei problemi.

    Nel nostro ambiente abbiamo un servizio di Windows che è in programma per eseguire centinaia di diversi file .bat .cmd .exe, … ecc. Che si sono accumulati nel corso degli anni e sono stati scritti da molte persone diverse e in stili diversi. Non abbiamo alcun controllo sulla scrittura di programmi e script, siamo solo responsabili della programmazione, dell’esecuzione e della segnalazione di successo / fallimento.

    Quindi ho provato praticamente tutti i suggerimenti qui con diversi livelli di successo. La risposta di Marko era quasi perfetta, ma quando veniva eseguita come servizio, non catturava sempre lo stdout. Non sono mai arrivato al fondo del perché no.

    L’unica soluzione che abbiamo trovato che funziona in TUTTI i nostri casi è questa: http://csharptest.net/319/using-the-processrunner-class/index.html

    Questo post potrebbe essere obsoleto, ma ho scoperto che la causa principale del suo blocco è dovuta allo stack overflow per redirectStandardoutput o se si utilizza redirectStandarderror.

    Poiché i dati di output oi dati di errore sono grandi, causeranno un tempo di blocco poiché è ancora in elaborazione per una durata indefinita.

    quindi per risolvere questo problema:

     p.StartInfo.RedirectStandardoutput = False p.StartInfo.RedirectStandarderror = False 

    Io questo è un approccio semplice e migliore (non abbiamo bisogno di AutoResetEvent ):

     public static string GGSCIShell(string Path, string Command) { using (Process process = new Process()) { process.StartInfo.WorkingDirectory = Path; process.StartInfo.FileName = Path + @"\ggsci.exe"; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardInput = true; process.StartInfo.UseShellExecute = false; StringBuilder output = new StringBuilder(); process.OutputDataReceived += (sender, e) => { if (e.Data != null) { output.AppendLine(e.Data); } }; process.Start(); process.StandardInput.WriteLine(Command); process.BeginOutputReadLine(); int timeoutParts = 10; int timeoutPart = (int)TIMEOUT / timeoutParts; do { Thread.Sleep(500);//sometimes halv scond is enough to empty output buff (therefore "exit" will be accepted without "timeoutPart" waiting) process.StandardInput.WriteLine("exit"); timeoutParts--; } while (!process.WaitForExit(timeoutPart) && timeoutParts > 0); if (timeoutParts < = 0) { output.AppendLine("------ GGSCIShell TIMEOUT: " + TIMEOUT + "ms ------"); } string result = output.ToString(); return result; } } 

    Stavo avendo lo stesso problema, ma la ragione era diversa. Succederebbe comunque in Windows 8, ma non in Windows 7. La seguente riga sembra aver causato il problema.

     pProcess.StartInfo.UseShellExecute = False 

    La soluzione era NON disabilitare UseShellExecute. Ora ho ricevuto una finestra popup di Shell, che è indesiderata, ma molto meglio del programma in attesa che non succeda nulla di particolare. Quindi ho aggiunto il seguente work-around per questo:

     pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden 

    Ora l’unica cosa che mi infastidisce è il motivo per cui questo sta accadendo sotto Windows 8 in primo luogo.