【问题标题】:Buffering in redirected STDOUT for a child process在重定向的 STDOUT 中为子进程缓冲
【发布时间】:2021-06-01 11:11:28
【问题描述】:

这样的代码可以托管一个控制台应用程序并监听其输出到 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 编写的。我对该代码没有可见性。我已经向作者建议她可能需要刷新她的输出流或可能使用 FORTRAN 等效于 Thread.Sleep(10);

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

什么控制着捕获的流的行为?

  • 它们被缓冲了吗?
  • 有什么方法可以影响这个吗?

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

似乎(见 cmets)这是一个老问题。我将托管代码提取到控制台应用程序中,问题也很明显。


Codiçil

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

【问题讨论】:

  • 这是一个老问题,有老套的变通方法。 codeproject.com/Articles/16163/…
  • 嗯,也许这个老问题有新的解决方案devblogs.microsoft.com/commandline/…
  • @JeremyLakeman 我在这个问题上找不到任何关于 SO 的信息。我相信你是对的。你为什么不写一个简短的答案让我接受?只需几句话解释重定向管道的处理方式不同,您要么必须伪造控制台,要么在源程序中刷新缓冲区,并提供更多信息的链接。

标签: c# process io-redirection


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 您的意思是批发替代System.Diagnostics.Process 和朋友吗?还是像 RTconsole 这样的 shim,但要调整旧版控制台应用程序以使用 ConPTY?
  • 上面的代码项目链接包含一个控制台进程,您可以将其放入其中;打开一个离屏控制台,启动一个附加到该控制台的子进程,监视控制台缓冲区的写入,将写入的字节复制到它自己的标准输出中而不进行缓冲。可以使用上面链接的新 api 编写一个新的 shim 控制台进程来创建一个伪控制台而不是屏幕外控制台。因此,您将从 C# 启动该控制台程序,它将启动您的 fortran 程序。 fortran 程序不会知道它的输出被重定向。
  • 但是如果你的 fortran 程序缓冲了标准错误,你就完蛋了。因为您要么必须合并标准输出和标准错误,要么处理其中一个被缓冲。
  • 是的,我明白了,我已经考虑到了这一点。但它在 VC6 中,我什至无法加载项目,老板对我有一个有趣的副项目的前景并不满意,而没有直接付费的客户。幸运的是,我们有 FORTRAN 的源代码,甚至还有原作者。可以添加冲洗。
  • 从子进程中刷新无疑是最简单的解决方案。
猜你喜欢
  • 1970-01-01
  • 2015-08-18
  • 2012-02-11
  • 2012-07-14
  • 2011-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多