按照正确的顺序捕获进程stdout和stderr

本文关键字:进程 stdout stderr 顺序 | 更新日期: 2023-09-27 18:29:33

我从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

据我所知,您希望保留stdout/stderr消息的顺序。我看不到用C#管理的进程来做这件事的任何DECENT方法(反射-是的,讨厌的子类化黑客-是的)。看起来它几乎是硬编码的。

此功能不依赖于线程本身。若要保持订单,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;

来源:如何重定向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();

虽然我很欣赏Erti Chris的回答(那是什么,Pascal?),但我认为其他人可能更喜欢用托管语言回答。此外,对于那些说"你不应该这样做"的批评者,因为STDOUT和STDERR不能保证保持顺序:是的,我理解,但有时我们必须与期望我们这样做的程序(我们没有写)进行互操作,正确的语义是该死的。

这是C#的一个版本。它没有通过调用CreateProcess来绕过托管Process API,而是使用了一种替代方法,将STDERR重定向到Windows外壳中的STDOUT流。因为UseShellExecute = true实际上并没有使用cmd.exe shell(令人惊讶!),所以您通常不能使用shell重定向。解决方法是自己启动cmd.exe shell,手动向其提供真实的shell程序和参数。

请注意,以下解决方案假设您的args数组已经正确转义。我喜欢使用内核的GetShortPathName调用的暴力解决方案,但你应该知道,它并不总是适合使用(比如如果你不在NTFS上)。此外,确实希望异步读取STDOUT缓冲区(如下所述),因为如果不这样做,程序可能会死锁。

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
public static string runCommand(string cpath, string[] args)
{
    using (var p = new Process())
    {
        // notice that we're using the Windows shell here and the unix-y 2>&1
        p.StartInfo.FileName = @"c:'windows'system32'cmd.exe";
        p.StartInfo.Arguments = "/c '"" + cpath + " " + String.Join(" ", args) + "'" 2>&1";
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.RedirectStandardError = true;
        var output = new StringBuilder();
        using (var outputWaitHandle = new AutoResetEvent(false))
        {
            p.OutputDataReceived += (sender, e) =>
            {
                // attach event handler
                if (e.Data == null)
                {
                    outputWaitHandle.Set();
                }
                else
                {
                    output.AppendLine(e.Data);
                }
            };
            // start process
            p.Start();
            // begin async read
            p.BeginOutputReadLine();
            // wait for process to terminate
            p.WaitForExit();
            // wait on handle
            outputWaitHandle.WaitOne();
            // check exit code
            if (p.ExitCode == 0)
            {
                return output.ToString();
            }
            else
            {
                throw new Exception("Something bad happened");
            }
        }
    }
}