Захват процесса stdout и stderr в правильном порядке

Я запускаю процесс из С# следующим образом:

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?

Я думал использовать временную метку, но я не думаю, что это сработает из-за упреждающего характера потоков.

Обновление: подтверждено, что использование метки времени для данных не используется.

Заключительное обновление: принятый ответ решает эту проблему - однако у него есть один недостаток, когда потоки объединены, нет способа узнать, к какому потоку было написано. Следовательно, если вам требуется логика write to stderr == failure, а не код выхода приложения, вы все равно можете быть ввернуты.

Ответ 1

Насколько я понимаю, вы хотите сохранить порядок сообщений stdout/stderr. Я не вижу никакого способа DECENT сделать это с помощью управляемого С# процесса (отражение - да, неприятный взлом подкласса - да). Кажется, что он довольно жестко закодирован.

Эта функциональность не зависит от самих потоков. Если вы хотите сохранить порядок, STDOUT и STDERROR должны использовать тот же дескриптор (буфер). Если они используют один и тот же буфер, он будет синхронизирован.

Вот фрагмент файла 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, а не С#, но достаточно, чтобы понять алгоритм:

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();

Ответ 2

Пока я понимаю ответ Эрт-Криса (что это, Паскаль?), я думал, что другие могут предпочесть ответ на управляемом языке. Кроме того, к недоброжелателям, которые говорят, что "вы не должны этого делать", потому что STDOUT и STDERR не гарантируют сохранения порядка: да, я понимаю, но иногда нам приходится взаимодействовать с программами (которые мы не писали), что ожидайте, что мы это сделаем, правильная семантика будет проклята.

Здесь версия в С#. Вместо того, чтобы обойти управляемый API Process, вызывая CreateProcess, он использует альтернативный подход, который перенаправляет STDERR на поток STDOUT в оболочке Windows. Поскольку UseShellExecute = true фактически не использует оболочку cmd.exe (сюрприз!), Вы обычно не можете использовать перенаправления оболочки. Обходной путь состоит в том, чтобы запустить оболочку cmd.exe самостоятельно, вручную загружая нашу настоящую программу оболочки и аргументы.

Обратите внимание, что следующее решение предполагает, что ваш массив 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");
            }
        }
    }
}