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

10 浏览
0 Comments

为什么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()); //需要StandardOutput的内容

我知道我启动的进程的输出大约为7MB。在Windows控制台中运行正常。不幸的是,在程序中,这个代码在WaitForExit处无限期地挂起。请注意,对于较小的输出(如3KB),此代码不会挂起。

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

0
0 Comments

ProcessStartInfo hanging on "WaitForExit"? Why?

在使用ProcessStartInfo的过程中,如果不按照文档中的建议,在等待之前先读取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();

在某些环境下,如果设置了`RedirectStandardOutput = true;`,并且没有使用`p.StandardOutput.ReadToEnd();`,就会发生死锁。

即使已经重定向并读取了标准输出,这个问题仍然会发生。

我猜这只适用于StandardOutput的缓冲区没有完全填满的情况。这里MSDN的解释不够详细。我建议阅读一篇很好的文章:dzone.com/articles/async-io-and-threadpool

解决方法暂时没有给出。

0
0 Comments

ProcessStartInfo中的WaitForExit问题的原因是,如果重定向了StandardOutput和/或StandardError,内部缓冲区可能会变满。无论使用什么顺序,都可能会出现问题:

- 如果在读取StandardOutput之前等待进程退出,进程可能会因为尝试写入StandardOutput而阻塞,从而进程永远不会结束。

- 如果使用ReadToEnd从StandardOutput中读取内容,如果进程永远不关闭StandardOutput(例如,如果它永远不终止或者在写入StandardError时被阻塞),那么你的进程可能会被阻塞。

解决方法是使用异步读取来确保缓冲区不会变满。为了避免任何死锁并收集来自StandardOutput和StandardError的所有输出,可以按照以下步骤进行操作:

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

以上就是解决ProcessStartInfo中WaitForExit问题的方法,通过使用异步读取来避免缓冲区满的问题。这个方法已经得到了广泛的应用并解决了许多人的问题。

0
0 Comments

问题:为什么ProcessStartInfo在"WaitForExit"上挂起?解决方法是什么?

在使用ProcessStartInfo启动进程并等待其退出时,可能会出现挂起的问题。问题出现的原因是,在进程开始后但退出事件附加之前,进程可能已经结束。解决方法是在所有注册操作完成后再开始进程。

具体实现的代码如下:

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

以上是截至目前为止最好和最完整的解决方案。这是唯一有效的解决方案,能够防止应用程序挂起。原因是在进程启动后但退出事件附加之前,进程可能已经结束。解决方法是在所有注册操作完成后再开始进程。

另外,还需注意的是,在调用process.BeginOutputReadLine()或process.BeginErrorReadLine()之前,必须先调用process.Start()。否则会出现错误:"StandardOut has not been redirected or the process hasn't started yet"。

有用户还指出,在事件处理程序中移除事件处理程序本身是一种不太优雅的做法。希望能找到更好的解决方法。这也是唯一一个不会在读取输出流时导致挂起的方法。

更多细节请参考[此回答](https://stackoverflow.com/a/56582093/893335)。

0