2010-11-10 111 views
22

在C#中(在SuSE上运行在Mono 2.8下的.NET 4.0),我想运行一个外部批处理命令并以二进制形式捕获它的输出。我使用的外部工具称为“samtools”(samtools.sourceforge.net),除此之外它还可以从名为BAM的索引二进制文件格式返回记录。从Process.StandardOutput捕获二进制输出

我使用Process.Start来运行外部命令,我知道我可以通过重定向Process.StandardOutput来捕获它的输出。问题是,这是一个带有编码的文本流,所以它不允许我访问输出的原始字节。我找到的几乎可行的解决方案是访问基础流。

这里是我的代码:

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

// ...详细解析如下

但是当我运行这一点,我读的第一23bytes不在输出中的第一个23个字节,但而是下游数百或千字节的某处。我假设StreamReader做了一些缓冲,所以底层流已经提前说4K输出。底层的流不支持回到起点。

而我卡在这里。有没有人有工作的解决方案来运行外部命令并以二进制形式捕获它的stdout?输出可能非常大,所以我想流式传输。

任何帮助表示赞赏。顺便说一句,我目前的解决方法是让samtools以文本格式返回记录,然后解析这些记录,但这很慢,我希望通过直接使用二进制格式来加快速度。

+0

我能想到的唯一一件事情就是将所需的编码设置为Unicode,然后将StreamReader中的每个字符分隔为两个字节。这将是一个可怕的黑客攻击,如果输出的奇数字节可能会惨败。解决方法是实现自己的编码,将字节直接映射到它们各自的char值,如ASCII,但不将上面的集合转换为'?'。但我会让其他人拿出正确的答案。 :) – cdhowie 2010-11-10 18:17:30

回答

24

使用StandardOutput.BaseStream是正确的做法,但你不能使用任何其他属性或方法的cmdProcess.StandardOutput。例如,访问cmdProcess.StandardOutput.EndOfStream将导致StreamReaderStandardOutput读取流的一部分,删除要访问的数据。

取而代之,只需读取并解析来自br的数据(假设您知道如何解析数据,并且不会读过流末尾,或者愿意赶上EndOfStreamException)。或者,如果您不知道数据有多大,请使用Stream.CopyTo将整个标准输出流复制到新文件或内存流。

+2

Stream.CopyTo应该被调用来处理可能非常巨大的整个输出? – SerG 2014-02-26 13:17:03

7

由于您明确指定了在Suse linux和mono上运行,因此可以使用本机unix调用来创建重定向并从流中读取,从而解决此问题。如:

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下,文件描述符由子进程继承,除非另有标注(收盘EXEC)。因此,要重定向孩子的stdout,您只需在调用exec之前更改父进程中的文件描述符#1即可。 Unix还提供了一个方便的东西,叫做pipe这是一个单向通信通道,有两个文件描述符代表两个端点。对于复制文件描述符,可以使用dupdup2,它们都创建描述符的等效副本,但dup返回由系统分配的新描述符,dup2将副本放入特定目标(如果需要,关闭它)。什么上面的代码的话,那么:

  1. 创建与端点readingwriting
  2. 保存当前stdout描述
  3. 分配管的写端点的副本stdout,并关闭原有
  4. 启动子进程,因此它继承连接到管道的写端点的stdout
  5. 恢复保存的stdout
  6. 通过在UnixStream

注包裹它从reading端点管的读取,在本机代码,该方法通常由fork + exec对启动的,所以该文件描述符可以在被修改子进程本身,但在新程序加载之前。此管理版本不是线程安全的,因为它必须临时修改父进程的stdout

由于代码在没有托管重定向的情况下启动子进程,因此.NET运行时不会更改任何描述符或创建任何流。所以,孩子的输出的唯一读者将用户代码,它采用了UnixStream来解决StreamReader的编码问题,

+0

您可以评论(1)pipe是如何连接到新进程的stdout的;(2)这是如何解决StreamReader在创建时缓存一些字节的问题的? – cdhowie 2010-12-23 02:56:12

+0

我已经更新了答案。 – Jester 2010-12-23 13:33:42

1

我检查了反射器发生了什么。在我看来,StreamReader不会阅读,直到您致电阅读。但是它创建的缓冲区大小为0x1000,所以也许它。但是幸运的是,直到您真正读取它为止,您可以安全地从中获取缓冲数据:它具有一个专用字段byte [] byteBuffer和两个整数字段byteLen和bytePos,第一个字段表示缓冲区中有多少字节,第二种意味着你消费了多少,应该是零。所以首先用反射读取这个缓冲区,然后创建BinaryReader。

+0

哦,现在我看到了,你调用了EndOfStream,这真的会导致缓冲读取。就像布拉德利所说的那样,不要那样做,而且你会没事地干扰私人领域。 – fejesjoco 2010-12-27 07:54:48