ProcessStartInfo挂在“WaitForExit”? 为什么?

我有以下代码:

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 

我知道从我开始的过程的输出是大约7MB长。 在Windows控制台中运行它工作正常。 不幸的是,在编程上这无限期地挂在WaitForExit上。 还要注意这个代码不会挂起较小的输出(如3KB)。

ProcessStartInfo中的内部StandardOutput是否有可能无法缓冲7MB? 如果是这样,我该怎么做呢? 如果没有,我做错了什么?

问题是,如果您重定向StandardOutput和/或StandardError ,内部缓冲区可能会变满。 无论你使用什么命令,都可能有一个问题:

  • 如果您在读取StandardOutput之前等待进程退出,进程可能会阻止尝试写入,所以进程永远不会结束。
  • 如果您使用ReadToEnd从StandardOutput读取,那么您的进程可以阻止进程从不关闭StandardOutput (例如,如果它永远不会终止,或者如果它被阻止写入StandardError )。

解决方案是使用异步读取来确保缓冲区未满。 为了避免任何死锁并收集来自StandardOutputStandardError所有输出,您可以这样做:

编辑:请参阅下面的答案如何避免发生超时ObjectDisposedException

 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. } } } 

Process.StandardOutput的文档说在你等待之前阅读,否则你可以死锁,复制代码片段如下:

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

马克·拜尔斯的答案是非常好的,但我只是增加了以下内容:OutputDataReceived和ErrorDataReceived委托需要在outputWaitHandle和errorWaitHandle处理之前被删除。 如果在超时超时后进程继续输出数据,然后终止,outputWaitHandle和errorWaitHandle变量将在处理后被访问。

(仅供参考,我不得不加上这个警告,因为我不能评论他的帖子。)

当进程超时时,发生未处理的ObjectDisposedException问题。 在这种情况下,条件的其他部分:

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

没有执行。 我通过以下方式解决了这个问题:

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

罗布回答了这个问题,为我节省了几个小时的时间。 在等待之前读取输出/错误缓冲区:

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

我们也有这个问题(或变体)。

尝试以下操作:

1)添加超时到p.WaitForExit(nnnn); 其中nnnn以毫秒为单位。

2)在WaitForExit调用之前进行ReadToEnd调用。 这我们看到MS建议。

Mark Byers解决方案谨慎注意:

在这里输入图像描述

这是一个比较现代的,基于.NET 4.5及以上版本的基于任务并行库(TPL)的解决方案。

用法示例

 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!"); } 

履行

 public static async Task<int> 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<Task>(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; } } /// <summary> /// Waits asynchronously for the process to exit. /// </summary> /// <param name="process">The process to wait for cancellation.</param> /// <param name="cancellationToken">A cancellation token. If invoked, the task will return /// immediately as cancelled.</param> /// <returns>A Task representing waiting for the process to end.</returns> public static Task WaitForExitAsync( this Process process, CancellationToken cancellationToken = default(CancellationToken)) { process.EnableRaisingEvents = true; var taskCompletionSource = new TaskCompletionSource<object>(); 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; } /// <summary> /// Reads the data from the specified data recieved event and writes it to the /// <paramref name="textWriter"/>. /// </summary> /// <param name="addHandler">Adds the event handler.</param> /// <param name="removeHandler">Removes the event handler.</param> /// <param name="textWriter">The text writer.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task representing the asynchronous operation.</returns> public static Task ReadAsync( this Action<DataReceivedEventHandler> addHandler, Action<DataReceivedEventHandler> removeHandler, TextWriter textWriter, CancellationToken cancellationToken = default(CancellationToken)) { var taskCompletionSource = new TaskCompletionSource<object>(); 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; } 

我这样解决了它:

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

我重定向了输入,输出和错误,并处理输出和错误流的读取。 此解决方案适用于Windows 7和Windows 8的SDK 7 – 8.1

我觉得这是简单和更好的方法(我们不需要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; } } 

我有同样的问题,但原因是不同的。 它会发生在Windows 8下,但在Windows 7下。下面的行似乎已经造成了这个问题。

 pProcess.StartInfo.UseShellExecute = False 

解决方案是不禁用UseShellExecute。 我现在收到一个不需要的Shell弹出窗口,但是比没有特别的事情发生的程序要好得多。 所以我添加了以下解决方法:

 pProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden 

现在唯一困扰我的是为什么这首先在Windows 8下发生。

我试图做一个类,将解决您的问题使用异步流阅读,通过考虑马克·拜尔斯,罗布,stevejay的答案。 这样做,我意识到有一个与异步进程输出流读取有关的错误。

我在Microsoft报告这个错误: https : //connect.microsoft.com/VisualStudio/feedback/details/3119134

概要:

你不能那样做:

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

您将收到System.InvalidOperationException:StandardOut尚未重定向或进程尚未启动。

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

然后,在启动进程后,您必须启动异步输出读取:

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

这样做,使得竞争条件,因为输出流可以接收数据,然后将其设置为异步:

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

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

然后有些人可能会说,你只需要读取流,然后将其设置为异步。 但同样的问题发生。 同步读取之间将存在争用条件,并将流设置为异步模式。

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

实际上没有办法实现一个进程的输出流的安全异步读取“Process”和“ProcessStartInfo”的设计。

您可能更好地使用异步阅读,如其他用户建议您的情况。 但是你应该知道,由于竞赛状况,你可能会错过一些信息。

上面的答案都没有做这项工作。

罗布解决方案挂起和'马克·拜尔斯'的解决方案得到处置的例外(我尝试了其他答案的“解决方案”)。

所以我决定提出另一个解决方案:

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

这段代码调试完美。

介绍

目前接受的答案不起作用(抛出异常),有太多的解决方法,但没有完整的代码。 这显然是在浪费很多人的时间,因为这是一个普遍的问题。

结合Mark Byers的回答和Karol Tyl的回答,我基于如何使用Process.Start方法编写了完整的代码。

用法

我用它来创建git命令周围的进度对话框。 这是我用它的方式:

  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; } 

理论上你也可以把stdout和stderr结合起来,但是我没有测试过。

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

我知道这是晚餐,但是在阅读整个页面后,没有任何解决方案为我工作,虽然我没有尝试穆罕默德·雷汉,因为代码有点难以遵循,但我猜他是在正确的轨道上。 当我说它没有工作,这不完全正确,有时它会正常工作,我想这是一个EOF标记之前输出的长度。

无论如何,对我来说,解决方案是使用不同的线程读取StandardOutput和StandardError并写入消息。

  StreamWriter sw = null; var queue = new ConcurrentQueue<string>(); 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); } } } 

希望这有助于某人,谁认为这可能是如此艰难!

感谢EM0的https://stackoverflow.com/a/17600012/4151626

其他解决方案(包括EM0的)仍然为我的应用程序死锁,由于内部超时和产生的应用程序使用StandardOutput和StandardError。 这是什么对我有用:

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

这篇文章可能已经过时了,但是我发现它通常挂起的主要原因是由于redirectStandardoutput的堆栈溢出,或者如果你有redirectStandarderror。

由于输出数据或错误数据较大,因此处理时间不确定,会造成暂停时间。

所以要解决这个问题:

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