在子进程的重定向 STDOUT 中缓冲

Buffering in redirected STDOUT for a child process

像这样的代码可以托管一个控制台应用程序并监听它对 STDOUT 和 STDERR 的输出

      Process process = new Process();
      process.StartInfo.FileName = exePath;
      process.StartInfo.UseShellExecute = false;
      process.StartInfo.WorkingDirectory = context.WorkingDirectory;
      process.StartInfo.RedirectStandardOutput = true;
      process.StartInfo.RedirectStandardError = true;
      process.StartInfo.RedirectStandardInput = true; // if you don't and it reads, no more events
      process.StartInfo.UseShellExecute = false;
      process.StartInfo.CreateNoWindow = false;

      process.EnableRaisingEvents = true;
      process.ErrorDataReceived += (sender, dataReceivedEventArgs) =>
      {
        lastbeat = DateTime.UtcNow;
        if (dataReceivedEventArgs.Data != null)
        {
          if (dataReceivedEventArgs.Data.EndsWith("%"))
          {
            context.Logger.Information($"  PROGRESS: {dataReceivedEventArgs.Data}");
          }
          else
          {
            msg.Append(" STDERR (UNHANDLED EXCEPTION): ");
            msg.AppendLine(dataReceivedEventArgs.Data);
            success = false;
          }
        }
      };
      process.OutputDataReceived += (sender, dataReceivedEventArgs) =>
      {
        lastbeat = DateTime.UtcNow;
        if (dataReceivedEventArgs.Data != null)
        {
          if (dataReceivedEventArgs.Data.EndsWith("%"))
          {
            context.Logger.Information($" PROGRESS: {dataReceivedEventArgs.Data}");
          }
          else
          {
            context.Logger.Information($" STDOUT: {dataReceivedEventArgs.Data}");
          }
        }
      };

      lastbeat = DateTime.UtcNow;
      process.Start();
      process.BeginErrorReadLine();
      process.BeginOutputReadLine();
      // wait for the child process, kill it if hearbeats are too slow
      while (!process.HasExited)
      {
        Thread.Sleep(100);
        var elapsed = DateTime.UtcNow - lastbeat;
        if (elapsed.TotalSeconds > heartbeatIntervalSeconds * 3)
        {
          success = false;
          msg.AppendLine("MODULE HEARTBEAT STOPPED, TERMINATING.");
          try
          {
            process.Kill(entireProcessTree: true); // ...and your children's children
          }
          catch (Exception ek)
          {
            msg.AppendLine(ek.Message);
          }
        }
      }

      if (success)
      {
        process.Dispose();
        context.Logger.Debug("MODULE COMPLETED");
        return JobStepResult.Success;
      }
      else
      {
        process.Dispose();
        context.Logger.Debug("MODULE ABORTED");
        throw new Exception(msg.ToString());
      }

托管进程可能很长运行,所以我们发明了心跳机制。这里有一个约定,STDERR 用于带外通信,这样 STDOUT 就不会被心跳消息污染。写入 STDERR 的任何以百分号结尾的文本行都被视为心跳,其他一切都是正常的错误消息。

我们有两个托管模块,其中一个与及时收到的心跳完美配合,但另一个似乎挂起,直到它在 STDERR 和 STDOUT 上的所有输出都大量到达。

托管模块是用 Lahey FORTRAN 编写的。我看不到该代码。我已经向作者建议她可能需要刷新她的输出流或者可能使用任何相当于 Thread.Sleep(10);

的 FORTRAN 来产生

不过,问题出在我这边也不是没有可能。当模块在控制台中手动执行时,它们的输出以稳定的速度显示,并及时显示心跳消息。

什么控制捕获流的行为?


这可能是相关的。 Get Live output from Process

看来(看评论)这是个老问题了。我将我的托管代码提取到控制台应用程序中,问题在那里也很明显。


编码

当托管控制台应用程序是 dotnet 核心应用程序时,不会发生这种情况。大概 dotnet 核心应用程序使用 ConPTY,因为这样它们可以跨平台工作。

这是一个老问题。应用程序可以检测它们是否在控制台中 运行,如果不是,则选择缓冲它们的输出。例如,这是 Microsoft C 运行时在您调用 printf.

时故意做的事情

应用程序不应该缓冲对 stderr 的写入,因为错误应该在您的程序有机会崩溃和擦除任何缓冲区之前可用。但是,没有任何东西可以强制执行该规则。

这个问题有旧的解决方案。通过创建屏幕外控制台,您可以检测输出何时写入控制台缓冲区。 Code Project 上有一篇文章更详细地讨论了这个问题和解决方案。

最近,Microsoft 对其控制台基础结构进行了现代化改造。如果您写入现代控制台,您的输出将转换为带有嵌入式 VT 转义序列的 UTF-8 流。世界其他地区已经使用了几十年的标准。有关更多详细信息,您可以阅读他们关于该作品的博客系列 here

我相信应该可以构建一个新的现代解决方法。一个存根进程,类似于上面的代码项目 link,它使用这些新的 pseudo console API 来启动子进程,捕获控制台 I/O 和输出无缓冲的管道到它自己的 stdio 句柄。如果这样的存根进程在 windows 中分发,或者由其他一些第 3 方分发,我会很好,但我还没有找到。

github.com/microsoft/terminal 中提供了几个示例,如果您想创建自己的存根,这将是一个很好的起点。

不过,我相信使用此解决方案或旧的解决方法仍会将输出流和错误流合并在一起。