C# 按正确顺序捕获进程stdout和stderr
我从C#启动了一个流程,如下所示:C# 按正确顺序捕获进程stdout和stderr,c#,multithreading,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.RedirectStan
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的进程顺序
我曾想过使用时间戳,但由于线程的先发制人特性,我认为这不会起作用
更新:确认在数据上使用时间戳没有用
最终更新:接受的答案解决了这个问题-但是它确实有一个缺点,当流被合并时,无法知道写入了哪个流。因此,如果您需要write to stderr==failure的逻辑,而不是app exit代码,那么您可能仍然会出错。据我所知,您希望保留stdout/stderr消息的顺序。我看不出有什么像样的方法可以用C#管理的流程(反射-是的,讨厌的子类化攻击-是的)做到这一点。看起来它几乎是硬编码的 此功能不依赖于线程本身。如果要保持顺序,
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的代码,不是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;
和BeginReadOutput()
虽然我很欣赏Erti Chris的回答(那是什么,Pascal?),但我认为其他人可能更喜欢用管理语言回答。同样,对于那些说“你不应该这样做”的批评者,因为STDOUT和STDERR不能保证保持顺序:是的,我理解,但有时我们必须与那些期望我们这样做的程序(我们没有编写)进行互操作,正确的语义是该死的 这里有一个C的版本。它没有通过调用
CreateProcess
绕过托管进程
API,而是使用另一种方法将STDERR重定向到Windows shell中的STDOUT流。因为UseShellExecute=true
实际上没有使用cmd.exe
shell(令人惊讶!),所以通常不能使用shell重定向。解决方法是自己启动cmd.exe
shell,手动向它提供真正的shell程序和参数
请注意,以下解决方案假设您的args
数组已经正确转义。我喜欢使用内核的GetShortPathName
调用的蛮力解决方案,但您应该知道它并不总是适合使用(比如如果您不在NTFS上)。另外,您确实希望执行异步读取标准输出缓冲区的额外步骤(如下所述),因为如果您不这样做
是的,我同意这个答案。今天下午我对这个问题做了一系列研究,我认为这是正确的方法。即使您要使用Process for stdout/err中提供的底层StreamReader,它听起来像是Peek方法阻塞,因为它没有在引擎盖下使用PeekNamedPipe。尽管如此,我一直在想你到底需要做什么。如果您只关心以正确的顺序捕获stdout/stderr,而不关心哪个是哪个,那么您可能会创建一个批处理文件,使用2>&1技巧将所有内容都推送到stdout中。这行吗?理想情况下,我希望能够同时获得
stdout
(用于输出处理)和stdout+stderr
(用于错误报告)。也许,hook会以某种方式使用公共锁写入2个句柄……在我的例子中,使用带有2>&1的批处理文件也会以错误的顺序捕获它,我只是不认为这是可能的all@paulm:我认为2>&1不起作用。刚刚在我的.NET应用程序上测试了它,并比较了GetStdHandle(STD_输出)和GetStdHandle(STD_错误)。不确定批处理管道重定向是如何工作的,但如果进行调试,可以看到它不会使两个句柄相同。试着重写Pascal代码,它应该对你有用,paulm.STDOUT/STDERR从来都不意味着不被订购。就我目前所知:你要么得到一个,要么得到另一个。如果可以运行相同的进程两次(一次用于stdout,另一次用于stdout/stderr),那么就可以了。您可以做什么:下载ApiMonitor并查看控制台写入是如何在后台实现的(WriteOut)。您可以执行进程劫持,这将允许您拦截正在进行的任何呼叫,从而允许您做任何您想做的事情。请参阅Google中的IAT API挂钩-这不是一种可爱的方式,但它会起作用。正如一项建议,您是否尝试过更改BeginErrorReadLine
和BeginOutputReadLine
调用的顺序?请参阅公认的答案,这根本没有帮助。谢谢,这实际上是一种非常简单的方法。在我的例子中,我的进程碰巧是一个批处理文件,因此这实际上不会带来额外的开销或复杂性。为了澄清,存在AutoResetEvent
和e.Data==null
,因为p.WaitForExit
可能发生在最后一个OutputDataReceived
事件之前。但当输出流关闭时,将发送带有e.Data==null
的最终事件。资料来源:
standardOutput = new StreamReader(new FileStream(
standardOutputReadPipeHandle,
FileAccess.Read,
4096,
false),
enc,
true,
4096);
if (output == null) {
Stream s = standardOutput.BaseStream;
output = new AsyncStreamReader(this, s,
new UserCallBack(this.OutputReadNotifyUser),
standardOutput.CurrentEncoding);
}
output.BeginReadLine();
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");
}
}
}
}