c# - 以正确的顺序捕获进程 stdout 和 stderr

标签 c# multithreading stdout stderr

我从 C# 启动一个进程,如下所示:

public bool Execute()
{
    ProcessStartInfo startInfo = new ProcessStartInfo();

    startInfo.Arguments =  "the command";
    startInfo.FileName = "C:\\MyApp.exe";

    startInfo.UseShellExecute = false;
    startInfo.RedirectStandardOutput = true;
    startInfo.RedirectStandardError = true;

    Log.LogMessage("{0} {1}", startInfo.FileName, startInfo.Arguments);

    using (Process myProcess = Process.Start(startInfo))
    {
        StringBuilder output = new StringBuilder();
        myProcess.OutputDataReceived += delegate(object sender, DataReceivedEventArgs e)
        {
            Log.LogMessage(Thread.CurrentThread.ManagedThreadId.ToString() + e.Data);
        };
        myProcess.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs e)
        {
            Log.LogError(Thread.CurrentThread.ManagedThreadId.ToString() +  " " + e.Data);            
        };

        myProcess.BeginErrorReadLine();
        myProcess.BeginOutputReadLine();

        myProcess.WaitForExit();

    }

    return false;
}

但这有一个问题...如果有问题的应用程序按以下顺序写入 std out 和 std err:

std out: msg 1
std err: msg 2
std out: msg 3

然后我从日志中看到的输出是:

msg 2
msg 1
msg 3

这似乎是因为事件处理程序是在另一个线程中执行的。所以我的问题是如何维护写入 std err 和 std out 的进程顺序?

我想过使用时间戳,但由于线程的抢占性质,我认为这行不通..

更新:确认在数据上使用时间戳是没有用的。

最终更新:接受的答案解决了这个问题——但是它确实有一个缺点,当流被合并时,没有办法知道写入了哪个流。因此,如果您需要写入 stderr == failure 的逻辑而不是应用程序退出代码,您可能仍然会被搞砸。

最佳答案

据我所知,您想保留 stdout/stderr 消息的顺序。我没有看到用 C# 托管进程执行此操作的任何体面的方法(反射 - 是的,令人讨厌的子类化黑客 - 是的)。看起来它几乎是硬编码的。

此功能不依赖于线程本身。如果要保持顺序,STDOUTSTDERROR 必须使用相同的句柄(缓冲区)。如果他们使用相同的缓冲区,它将被同步。

这是 Process.cs 的一个片段:

 if (startInfo.RedirectStandardOutput) {
    CreatePipe(out standardOutputReadPipeHandle, 
               out startupInfo.hStdOutput, 
               false);
    } else {
    startupInfo.hStdOutput = new SafeFileHandle(
         NativeMethods.GetStdHandle(
                         NativeMethods.STD_OUTPUT_HANDLE), 
                         false);
}

if (startInfo.RedirectStandardError) {
    CreatePipe(out standardErrorReadPipeHandle, 
               out startupInfo.hStdError, 
               false);
    } else {
    startupInfo.hStdError = new SafeFileHandle(
         NativeMethods.GetStdHandle(
                         NativeMethods.STD_ERROR_HANDLE),
                         false);
}

如您所见,将有两个缓冲区,如果我们有两个缓冲区,我们已经丢失了订单信息。

基本上,您需要创建自己的 Process() 类来处理这种情况。伤心?是的。 好消息是这并不难,看起来很简单。这是从 StackOverflow 获取的代码,不是 C# 但足以理解算法:

function StartProcessWithRedirectedOutput(const ACommandLine: string; const AOutputFile: string;
  AShowWindow: boolean = True; AWaitForFinish: boolean = False): Integer;
var
  CommandLine: string;
  StartupInfo: TStartupInfo;
  ProcessInformation: TProcessInformation;
  StdOutFileHandle: THandle;
begin
  Result := 0;

  StdOutFileHandle := CreateFile(PChar(AOutputFile), GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL, 0);
  Win32Check(StdOutFileHandle <> INVALID_HANDLE_VALUE);
  try
    Win32Check(SetHandleInformation(StdOutFileHandle, HANDLE_FLAG_INHERIT, 1));
    FillChar(StartupInfo, SizeOf(TStartupInfo), 0);
    FillChar(ProcessInformation, SizeOf(TProcessInformation), 0);

    StartupInfo.cb := SizeOf(TStartupInfo);
    StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESTDHANDLES;
    StartupInfo.hStdInput := GetStdHandle(STD_INPUT_HANDLE);
    StartupInfo.hStdOutput := StdOutFileHandle;
    StartupInfo.hStdError := StdOutFileHandle;

    if not(AShowWindow) then
    begin
      StartupInfo.dwFlags := StartupInfo.dwFlags or STARTF_USESHOWWINDOW;
      StartupInfo.wShowWindow := SW_HIDE;
    end;

    CommandLine := ACommandLine;
    UniqueString(CommandLine);

    Win32Check(CreateProcess(nil, PChar(CommandLine), nil, nil, True,
      CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInformation));

    try
      Result := ProcessInformation.dwProcessId;

      if AWaitForFinish then
        WaitForSingleObject(ProcessInformation.hProcess, INFINITE);

    finally
      CloseHandle(ProcessInformation.hProcess);
      CloseHandle(ProcessInformation.hThread);
    end;

  finally
    CloseHandle(StdOutFileHandle);
  end;
end;

来源:How to redirect large amount of output from command executed by CreateProcess?

您想要使用 CreatePipe 而不是文件。从管道中,您可以像这样异步读取:

standardOutput = new StreamReader(new FileStream(
                       standardOutputReadPipeHandle, 
                       FileAccess.Read, 
                       4096, 
                       false),
                 enc, 
                 true, 
                 4096);

和 BeginReadOutput()

  if (output == null) {
        Stream s = standardOutput.BaseStream;
        output = new AsyncStreamReader(this, s, 
          new UserCallBack(this.OutputReadNotifyUser), 
             standardOutput.CurrentEncoding);
    }
    output.BeginReadLine();

关于c# - 以正确的顺序捕获进程 stdout 和 stderr,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18529662/

相关文章:

c# - 将对象的克隆添加到 c# 中的列表(防止从外部修改)

c# - 使用 ASP.NET 清除缓存的最有效方法

multithreading - 是否可以在不使用偏移量的情况下将指针存储在共享内存中?

c++ - 并行但无锁地将数据刷新到磁盘?

python - 检查 python 中正在运行的子进程的标准输出

python 调用外部cmd并将stdout重定向到文件

Python - 在对齐的列中打印 CSV 字符串列表

c# - Silverlight 数据网格 : changing the color of certain cells

c# - 查找整数列表中是否存在整数

c++ - 在线程之间重定向标准输入/输出