2010-11-10 113 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