我有以下代码:

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


当前回答

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

OutputDataReceived和ErrorDataReceived委托需要在outputWaitHandle和errorWaitHandle被释放之前被移除。如果进程在超过超时时间后继续输出数据,然后终止,则outputWaitHandle和errorWaitHandle变量将在被释放后被访问。

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

其他回答

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

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

于是我照做了。对于测试数据,我使用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应用程序中工作,除非进行了明显的修改。

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

OutputDataReceived和ErrorDataReceived委托需要在outputWaitHandle和errorWaitHandle被释放之前被移除。如果进程在超过超时时间后继续输出数据,然后终止,则outputWaitHandle和errorWaitHandle变量将在被释放后被访问。

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

上面的答案没有一个能起作用。

Rob解决方案挂起,“Mark Byers”解决方案获得已处理异常。(我尝试了其他答案的“解决方案”)。

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

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

这段代码经过调试,工作完美。

在阅读了这里所有的帖子之后,我确定了Marko avlijaovic的统一解决方案。 然而,它并没有解决我所有的问题。

在我们的环境中,我们有一个Windows服务,计划运行数百个不同的.bat .cmd .exe,…等等,这些文件积累了多年,由许多不同的人以不同的风格编写。我们无法控制程序和脚本的编写,我们只负责调度、运行和成功/失败的报告。

所以我尝试了这里所有的建议,并取得了不同程度的成功。Marko的回答几乎是完美的,但是当作为服务运行时,它并不总是捕获标准输出。我一直没搞清楚为什么不行。

我们发现在所有情况下都有效的唯一解决方案是:http://csharptest.net/319/using-the-processrunner-class/index.html