从Process.StandardOutput中捕获二进制输出
在C#中(在SuSE上运行在Mono 2.8下的.NET 4.0),我想运行一个外部批处理命令并以二进制forms捕获它的输出。 我使用的外部工具称为“samtools”(samtools.sourceforge.net),除此之外,它可以从一个名为BAM的索引二进制文件格式返回logging。
我使用Process.Start来运行外部命令,我知道我可以通过redirectProcess.StandardOutput来捕获它的输出。 问题是,这是一个带有编码的文本stream,所以它不能访问输出的原始字节。 我find的几乎可行的解决scheme是访问基础stream。
这是我的代码:
Process cmdProcess = new Process(); ProcessStartInfo cmdStartInfo = new ProcessStartInfo(); cmdStartInfo.FileName = "samtools"; cmdStartInfo.RedirectStandardError = true; cmdStartInfo.RedirectStandardOutput = true; cmdStartInfo.RedirectStandardInput = false; cmdStartInfo.UseShellExecute = false; cmdStartInfo.CreateNoWindow = true; cmdStartInfo.Arguments = "view -u " + BamFileName + " " + chromosome + ":" + start + "-" + end; cmdProcess.EnableRaisingEvents = true; cmdProcess.StartInfo = cmdStartInfo; cmdProcess.Start(); // Prepare to read each alignment (binary) var br = new BinaryReader(cmdProcess.StandardOutput.BaseStream); while (!cmdProcess.StandardOutput.EndOfStream) { // Consume the initial, undocumented BAM data br.ReadBytes(23);
// …更多parsing如下
但是当我运行这个时,我读到的前23字节不是输出中的前23个字节,而是下游的数百或千字节。 我假设StreamReader做了一些缓冲,所以底层stream已经提前说4K输出。 底层的stream不支持回到起点。
而我卡在这里。 有没有人有一个工作的解决scheme来运行一个外部的命令,并以二进制forms捕获它的标准输出? 输出可能非常大,所以我想stream。
任何帮助赞赏。
顺便说一下,我目前的解决方法是让samtools以文本格式返回logging,然后parsing这些logging,但这很慢,我希望通过直接使用二进制格式来加快速度。
使用StandardOutput.BaseStream
是正确的方法,但不能使用cmdProcess.StandardOutput
任何其他属性或方法。 例如,访问cmdProcess.StandardOutput.EndOfStream
将导致StreamReader
for StandardOutput
读取部分stream,删除要访问的数据。
相反,只需从br
读取和parsing数据(假设您知道如何parsing数据,并且不会读取stream结束,或者愿意捕获EndOfStreamException
)。 或者,如果您不知道数据有多大,请使用Stream.CopyTo
将整个标准输出stream复制到新文件或内存stream。
由于您明确指定了在Suse linux和mono上运行,因此可以使用本地unix调用来创buildredirect并从stream中读取,从而解决此问题。 如:
using System; using System.Diagnostics; using System.IO; using Mono.Unix; class Test { public static void Main() { int reading, writing; Mono.Unix.Native.Syscall.pipe(out reading, out writing); int stdout = Mono.Unix.Native.Syscall.dup(1); Mono.Unix.Native.Syscall.dup2(writing, 1); Mono.Unix.Native.Syscall.close(writing); Process cmdProcess = new Process(); ProcessStartInfo cmdStartInfo = new ProcessStartInfo(); cmdStartInfo.FileName = "cat"; cmdStartInfo.CreateNoWindow = true; cmdStartInfo.Arguments = "test.exe"; cmdProcess.StartInfo = cmdStartInfo; cmdProcess.Start(); Mono.Unix.Native.Syscall.dup2(stdout, 1); Mono.Unix.Native.Syscall.close(stdout); Stream s = new UnixStream(reading); byte[] buf = new byte[1024]; int bytes = 0; int current; while((current = s.Read(buf, 0, buf.Length)) > 0) { bytes += current; } Mono.Unix.Native.Syscall.close(reading); Console.WriteLine("{0} bytes read", bytes); } }
在Unix下,文件描述符被subprocessinheritance,除非另有标记( 在exec上closures )。 所以,为了redirect一个孩子的stdout
,你只需要在调用exec
之前改变父进程中的文件描述符#1。 Unix还提供了一个方便的东西叫做pipe道 ,它是一个单向的通信通道,两个文件描述符代表两个端点。 对于复制文件描述符,可以使用dup
或dup2
创build描述符的等价副本,但是dup
返回由系统分配的新描述符,而dup2
将该副本放置在特定的目标(如果需要closures它)。 上面的代码做了什么,然后:
- 用端点
reading
创build一个pipe道 - 保存当前
stdout
描述符的副本 - 将pipe道的写入端点分配给
stdout
并closures原始文件 - 启动subprocess,以便inheritance连接到pipe道的写端点的
stdout
- 恢复保存的
stdout
- 通过将其包装在
UnixStream
中从pipe道的reading
端点读取
注意,在本机代码中,一个进程通常由一个fork
+ exec
对开始,所以文件描述符可以在subprocess本身中被修改,但在新程序被加载之前。 这个pipe理版本不是线程安全的,因为它必须临时修改父进程的stdout
。
由于代码在没有托piperedirect的情况下启动subprocess,.NET运行时不会更改任何描述符或创build任何stream。 所以,孩子输出的唯一读者将是用户代码,它使用UnixStream
来解决StreamReader
的编码问题,
我检查了reflection器发生了什么事。 在我看来,StreamReader不读取,直到你打电话给它阅读。 但它是用0x1000的缓冲区大小创build的,所以也许它是。 但幸运的是,直到你真正的读取它,你可以安全地获取缓冲数据:它有一个私有字段byte [] byteBuffer和两个整数字段byteLen和bytePos,第一个意味着缓冲区中有多less字节,第二个意思是你消耗了多less,应该是零。 所以首先用reflection读取这个缓冲区,然后创buildBinaryReader。