以正确的顺序捕获进程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的进程顺序?

我想过使用时间戳,但我认为这不会起作用,因为线程具有先发制人的性质。

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

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

据我所知,您希望保留stdout / stderr消息的顺序。 我没有看到任何DECENT方式使用C#managed Process(reflection – 是的,讨厌的子类化黑客 – 是的)。 似乎它几乎是硬编码的。

此function不依赖于线程本身。 如果要保持顺序, 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; 

Source: 如何重定向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 shell中的STDOUT流。 因为UseShellExecute = true实际上并不使用cmd.exe shell(出乎意料!),所以通常不能使用shell重定向。 解决方法是自己启动cmd.exe shell,手动提供真正的shell程序和参数。

请注意,以下解决方案假定您的args数组已正确转义。 我喜欢使用内核的GetShortPathName调用的powershell解决方案,但你应该知道它并不总是适合使用(就像你不在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"); } } } }