捕获进程的标准输出和错误输出并按正确顺序排列

17

我从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 == 失败的逻辑而不是应用程序退出代码,则可能仍然会失败。


只是建议,您尝试过更改 BeginErrorReadLineBeginOutputReadLine 调用的顺序吗? - Tony
看看被接受的答案,那根本没有帮助。 - paulm
2个回答

14
据我所知,您想保留标准输出/错误消息的顺序。在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;

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

是的,我同意这个答案。今天下午我对这个问题进行了大量研究,我认为这是正确的方法。即使您要使用进程提供的底层StreamReaders来处理stdout/err,听起来Peek方法也会阻塞,因为它在内部没有使用PeekNamedPipe。话虽如此,我一直在想你需要做什么。如果你只关心以正确的顺序捕获stdout/stderr,而不关心哪个是哪个,你可以潜在地创建一个批处理文件,使用2>&1技巧将所有内容推入stdout。那样行吗? - J Trana
1
理想情况下,我希望能够同时获取stdout(用于输出处理)和stdout+stderr(用于错误报告)。也许,以某种方式使用公共锁来挂钩写入这两个句柄... - ivan_pozdeev
在我的情况下,使用带有2>&1的批处理文件也以错误的顺序捕获它,我只是认为这根本不可能。 - paulm
如果这个方法可行,那么你如何知道应用程序是否写入了stderr以检测是否存在问题?我相信这就是.NET进程类具有两个管道的原因 - 与cmd.exe相同? - paulm
2
STDOUT/STDERR从来没有被设计为有序的。就我目前所看到的情况而言:你要么只得到一个,要么只得到另一个。如果你可以运行同一个进程两次(一次用于stdout,另一次用于stdout/stderr),那么你就没问题了。你可以做什么:下载ApiMonitor并查看控制台写入是如何在底层实现的(WriteOut)。你可以进行进程劫持,这将允许你拦截正在进行的任何调用-从而允许你做任何你想做的事情。在Google中查找IAT API hooking - 这不是一种可爱的方式,但它会起作用。 - Erti-Chris Eelmaa
显示剩余3条评论

9
虽然我欣赏Erti-Chris的回答(那是什么,Pascal?),但我认为其他人可能更喜欢用托管语言的答案。此外,对于那些说“你不应该这样做”的反对者,因为STDOUT和STDERR不能保证保留排序:是的,我理解,但有时我们必须与期望我们这样做的程序(我们没有编写)进行交互,正确的语义被忽略。
以下是C#版本。它不是通过调用CreateProcess来规避托管Process API,而是使用一种替代方法,将STDERR重定向到Windows shell中的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");
            }
        }
    }
}

谢谢,这实际上是一个非常简单的方法。在我的情况下,我的进程本来就是批处理文件,所以这确实不会增加额外的开销或复杂性。 - StayOnTarget
2
为了澄清,AutoResetEvente.Data == null的存在是因为p.WaitForExit可能会在最后一个OutputDataReceived事件之前发生。但是当输出流关闭时,将发送一个带有e.Data == null的最终事件。来源:https://msdn.microsoft.com/en-us/library/system.diagnostics.datareceivedeventhandler%28v=vs.110%29.aspx - Kjell Rilbe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接