ProcessStartInfo在“WaitForExit”上挂起?为什么?

228

我有以下代码:

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,导致了这种情况。如果是这样,我应该做什么?如果不是,那我做错了什么?


有没有关于此的完整源代码的最终解决方案? - Kiquenet
2
我遇到了同样的问题,这是我解决它的方法。https://dev59.com/okzSa4cB1Zd3GeqPjAyW#12848337 - Bedasso
6
好的,最终解决方案是:交换最后两行。这在手册中有说明。 - Amit Naidu
4
该代码示例在调用p.WaitForExit之前调用p.StandardOutput.ReadToEnd,避免了死锁情况。如果父进程在调用p.StandardOutput.ReadToEnd之前调用p.WaitForExit,并且子进程写入足够的文本以填满重定向流,则可能导致死锁条件。父进程将无限期地等待子进程退出。子进程将无限期地等待父进程从完整的StandardOutput流中读取。 - Carlos Liu
做这件事情正确的方法非常复杂,有点让人烦恼。很高兴能够通过简单的命令行重定向 > 输出文件来解决它 :) - eglasius
请查看 https://github.com/Tyrrrz/CliWrap - Stefan Ollinger
23个回答

470
问题在于,如果你重定向了StandardOutput和/或StandardError,内部缓冲区可能会变满。无论使用什么顺序,都可能会出现问题:
- 如果在读取StandardOutput之前等待进程退出,则该进程可能会阻塞尝试向其写入,因此进程永远不会结束。 - 如果使用ReadToEnd从StandardOutput中读取数据,则您的进程可能会在进程未关闭StandardOutput(例如,如果它从未终止,或者如果它被阻塞写入StandardError)时阻塞。
解决方案是使用异步读取以确保缓冲区不会变满。为了避免任何死锁并收集来自StandardOutputStandardError的所有输出,您可以这样做:
编辑:请参见下面的答案,了解如何避免如果超时发生而导致的。
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.
        }
    }
}

21
完全没有想到重定向输出引起了这个问题,但确实是这样。我花了4个小时苦思冥想,但在阅读了你的帖子后,在5分钟内解决了它。干得好! - Ben Gripka
6
每次命令提示符关闭时,都会出现以下内容:出现了一个未处理的类型为“System.ObjectDisposed”的异常,位于mscorlib.dll中。其他信息:安全句柄已关闭。 - user1663380
3
我们遇到了和@user1663380描述的类似问题。您认为事件处理程序的using语句需要在进程本身的using语句之上,这样做可能是可行的吗? - Dan Forbes
8
我认为等待句柄是不必要的。根据MSDN文档,只需使用非超时版本的WaitForExit()方法即可: 当标准输出已重定向到异步事件处理程序时,当此方法返回时,可能尚未完成输出处理。为确保异步事件处理已完成,请在从此重载接收到true后调用不带参数的WaitForExit()重载。 - Patrick
2
我们怎么知道 e.Data 的空值表示流结束?我找不到任何文档记录这一点。 - alx9r
显示剩余25条评论

115

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

17
我不确定这是否只是我的环境问题,但是我发现如果你设置了 RedirectStandardOutput = true; 却不使用 p.StandardOutput.ReadToEnd();,会导致死锁/挂起。 - Chris S
4
没错。我曾经遇到过类似的情况。在使用ffmpeg进行转换时,我无缘无故地重定向了StandardError流,结果它写入了足够多的内容,导致了死锁。 - Léon Pelletier
1
即使重定向和读取标准输出,这个问题仍然困扰着我。 - user3791372
2
@user3791372 我猜这只适用于StandardOutput后面的缓冲区没有完全填充的情况。这里MSDN没有做到它应该有的作用。我建议你阅读一篇很棒的文章,链接为:https://dzone.com/articles/async-io-and-threadpool - Cary

32

这是一个基于Task Parallel Library(TPL)的更现代的等待解决方案,适用于.NET 4.5及更高版本。

使用示例

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
        }
    })
    {
        var cancellationTokenSource = timeout.HasValue ?
            new CancellationTokenSource(timeout.Value) :
            new CancellationTokenSource();

        process.Start();

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

2
迄今为止最好、最完整的答案 - TermoTux
1
由于某种原因,这是唯一对我有效的解决方案,应用程序停止了挂起。 - Jack
2
看起来你没有处理这种情况:进程在启动后但附加Exited事件之前就结束了。我的建议是在所有注册完成后再启动进程。 - Stas Boyarincev
1
@MuhammadRehanSaeed 另一件事 - 看起来不允许在 process.Start 之前调用 process.BeginOutputReadLine() 或者 process.BeginErrorReadLine()。在这种情况下,我会收到错误消息:StandardOut 未被重定向或进程尚未启动。 - Stas Boyarincev
1
只有在我没有将过程包装在“using”中时,这才对我有效。如果我这样做,它不会触发退出处理程序。请参见此答案:https://dev59.com/JW855IYBdhLWcg3wJg5y#56582093 - grumble
显示剩余3条评论

25

Mark Byers的回答非常出色,但我想要补充以下内容:

OutputDataReceivedErrorDataReceived代理必须在outputWaitHandleerrorWaitHandle被处理之前被移除。 如果进程在超时后继续输出数据然后终止,则会在处理后访问outputWaitHandleerrorWaitHandle变量。

(FYI,由于我无法在他的帖子上发表评论,因此我必须将此警告添加为回答。)


2
也许更好的做法是调用 CancelOutputRead - Mark Byers
1
将Mark编辑过的代码添加到这个答案中会非常棒!我现在正好遇到了完全相同的问题。 - ianbailey
12
最简单的解决方法是将 using(Process p...) 放在 using(AutoResetEvent errorWaitHandle...) 内部。 - Didier A.

19
处理不当的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 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
                    {
                        errorBuilder.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);
        }
    }
}

1
为了完整起见,这里缺少将重定向设置为true的步骤。 - knocte
我已经在我的代码中移除了超时设置,因为程序可能会要求用户输入(例如输入一些内容),所以我不想要求用户必须快速响应。 - knocte
为什么你把 outputerror 改成了 outputBuilder?有人能提供一个完整可行的答案吗? - Marko Avlijaš
System.ObjectDisposedException: 安全句柄已关闭,在我的版本中也发生了这种情况。 - Matt

11

Rob回答了我的问题,帮我节省了几个小时的尝试。在等待之前,请先阅读输出/错误缓冲区:

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

2
但是如果在调用WaitForExit()之后还有更多的数据呢? - knocte
根据我的测试,ReadToEnd或类似的方法(如StandardOutput.BaseStream.CopyTo)将在读取所有数据后返回。之后不会再有任何数据。 - S.Serpooshan
你是在说ReadToEnd()也会等待进程退出? - knocte
2
@knocte,你在试图理解微软创建的API吗? - aaaaaa
相应的MSDN页面存在的问题是,它没有解释StandardOutput后面的缓冲区可能会变满,在这种情况下,子进程必须停止写入并等待直到缓冲区被排空(父进程读取缓冲区中的数据)。ReadToEnd()只能同步读取,直到缓冲区关闭或缓冲区变满,或者子进程退出且缓冲区未满。这是我的理解。 - Cary
@knocte: ReadToEnd() 等待管道的写入端被关闭。这将在进程退出时发生,也可以在程序有意关闭其标准输出句柄时发生(但这也保证不会再有更多数据)。 - Ben Voigt

8
我们也遇到了这个问题(或者类似的问题)。
请尝试以下方法:
1)在 p.WaitForExit(nnnn); 中增加超时时间,其中 nnnn 表示毫秒数。
2)在 WaitForExit 调用之前进行 ReadToEnd 调用。这是微软建议的方法。

6

感谢EM0https://dev59.com/rGHVa4cB1Zd3GeqPjRaT#17600012做出的贡献。

其他解决方案(包括EM0的)对我的应用程序仍然存在死锁问题,原因是由于产生的应用程序同时使用StandardOutput和StandardError并且具有内部超时。下面是对我有效的解决方案:

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

编辑:代码示例中添加了StartInfo的初始化


这是我使用的东西,再也没有死锁的问题了。 - Roemer

4

我是这样解决的:

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

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


2
Elina:谢谢你的回答。在这个MSDN文档(https://msdn.microsoft.com/en-us/library/system.diagnostics.processstartinfo.redirectstandardoutput(v=vs.110).aspx)的底部有一些注释,警告可能会出现死锁,如果您同步读取了重定向的stdout和stderr流的末尾。很难确定您的解决方案是否容易受到此问题的影响。此外,似乎您正在将进程的stdout / stderr输出直接作为输入发送回来。为什么? :) - Matthew Piatt

3
我尝试创建一个类,使用异步流读取解决您的问题,考虑了Mark Byers、Rob和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”的设计方式下,无法实现对进程输出流的安全异步读取。

对于您的情况,您可能最好使用其他用户建议的异步读取。但是您应该意识到,由于竞态条件,您可能会错过一些信息。


“在将输出流设置为异步之前,它可以接收数据”,这些数据会存储在管道缓冲区中,当您调用BeginOutputReadLine()时,您将看到它。没有任何数据会丢失。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接