我有以下代码:

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?如果是,我该怎么办?如果不是,我做错了什么?


当前回答

我认为使用async,可以有一个更优雅的解决方案,即使同时使用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;

    process.Start();

    var tStandardOutput = process.StandardOutput.ReadToEndAsync();
    var tStandardError = process.StandardError.ReadToEndAsync();

    if (process.WaitForExit(timeout))
    {
        string output = await tStandardOutput;
        string errors = await tStandardError;

        // Process completed. Check process.ExitCode here.
    }
    else
    {
        // Timed out.
    }
}

这是基于Mark Byers的回答。 如果你不在异步方法中,你可以使用string output = tStandardOutput.result;而不是等待

其他回答

让我们将这里发布的示例代码称为重定向程序,将另一个程序称为重定向程序。如果是我,我可能会写一个测试重定向程序,可以用来复制这个问题。

于是我照做了。对于测试数据,我使用ECMA-334 c#语言规范v PDF;大约5MB。下面是其中的重要部分。

StreamReader stream = null;
try { stream = new StreamReader(Path); }
catch (Exception ex)
{
    Console.Error.WriteLine("Input open error: " + ex.Message);
    return;
}
Console.SetIn(stream);
int datasize = 0;
try
{
    string record = Console.ReadLine();
    while (record != null)
    {
        datasize += record.Length + 2;
        record = Console.ReadLine();
        Console.WriteLine(record);
    }
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return;
}

datasize值与实际文件大小不匹配,但这无关紧要。目前还不清楚PDF文件是否总是在行尾同时使用CR和LF,但这并不重要。您可以使用任何其他大型文本文件进行测试。

使用这种方法,当我写入大量数据时,示例重定向器代码挂起,而当我写入少量数据时则不会挂起。

我试图以某种方式跟踪该代码的执行,但我做不到。我注释掉了重定向程序中禁用为重定向程序创建控制台的行,以尝试获得单独的控制台窗口,但我不能。

然后我发现了如何在新窗口、父窗口或没有窗口中启动控制台应用程序。所以很明显,当一个控制台程序在没有ShellExecute的情况下启动另一个控制台程序时,我们不能(很容易)拥有一个单独的控制台,因为ShellExecute不支持重定向,我们必须共享一个控制台,即使我们没有为其他进程指定窗口。

我假设,如果重定向程序在某个地方填满了缓冲区,那么它必须等待数据被读取,如果此时重定向程序没有读取数据,那么它就是死锁。

解决方案是不使用ReadToEnd,而是在写入数据时读取数据,但没有必要使用异步读取。解决方法很简单。下面是5 MB的PDF文件。

ProcessStartInfo info = new ProcessStartInfo(TheProgram);
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
Process p = Process.Start(info);
string record = p.StandardOutput.ReadLine();
while (record != null)
{
    Console.WriteLine(record);
    record = p.StandardOutput.ReadLine();
}
p.WaitForExit();

另一种可能是使用GUI程序进行重定向。上面的代码在WPF应用程序中工作,除非进行了明显的修改。

问题是,如果你重定向StandardOutput和/或StandardError,内部缓冲区可能会满。不管你用什么顺序,都会有一个问题:

如果在读取StandardOutput之前等待进程退出,进程可能会阻塞尝试写入它,因此进程永远不会结束。 如果你使用ReadToEnd从StandardOutput中读取,那么如果进程从未关闭StandardOutput(例如,如果它从未终止,或者如果它被阻塞写入到standderror),那么你的进程就会阻塞。

解决方案是使用异步读取来确保缓冲区不会被填满。为了避免死锁并从StandardOutput和standderror收集所有输出,你可以这样做:

编辑:请参阅下面的回答,了解如何在超时发生时避免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.
        }
    }
}

我最终使用的解决方案来避免所有的复杂性:

var outputFile = Path.GetTempFileName();
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args) + " > " + outputFile + " 2>&1");
info.CreateNoWindow = true;
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
info.UseShellExecute = false;
System.Diagnostics.Process p = System.Diagnostics.Process.Start(info);
p.WaitForExit();
Console.WriteLine(File.ReadAllText(outputFile)); //need the StandardOutput contents

所以我创建了一个临时文件,通过使用> outputfile > 2>&1将输出和错误重定向到它,然后在进程完成后读取文件。

其他解决方案适用于希望对输出执行其他操作的场景,但对于简单的操作,这可以避免很多复杂性。

https://stackoverflow.com/a/17600012/4151626的信用归于EM0

我的应用程序的其他解决方案(包括EM0)仍然处于死锁状态,这是由于内部超时以及派生应用程序同时使用StandardOutput和standderror。以下是对我有效的方法:

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代码示例

我读了很多答案,并给出了自己的答案。不确定这个在任何情况下都会修复,但它在我的环境中修复了。我没有使用WaitForExit,而是使用WaitHandle。在输出和错误端信号上等待所有。如果有人能发现可能存在的问题,我会很高兴。或者它是否能帮助别人。对我来说更好,因为不用暂停。

private static int DoProcess(string workingDir, string fileName, string arguments)
{
    int exitCode;
    using (var process = new Process
    {
        StartInfo =
        {
            WorkingDirectory = workingDir,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = fileName,
            Arguments = arguments,
            RedirectStandardError = true,
            RedirectStandardOutput = true
        },
        EnableRaisingEvents = true
    })
    {
        using (var outputWaitHandle = new AutoResetEvent(false))
        using (var errorWaitHandle = new AutoResetEvent(false))
        {
            process.OutputDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.Log(args.Data);
                else outputWaitHandle.Set();
            };
            process.ErrorDataReceived += (sender, args) =>
            {
                // ReSharper disable once AccessToDisposedClosure
                if (args.Data != null) Debug.LogError(args.Data);
                else errorWaitHandle.Set();
            };

            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();

            WaitHandle.WaitAll(new WaitHandle[] { outputWaitHandle, errorWaitHandle });

            exitCode = process.ExitCode;
        }
    }
    return exitCode;
}