2010-11-10 13 views
22

In C# (.NET 4.0 in esecuzione su Mono 2.8 su SuSE) Vorrei eseguire un comando batch esterno e acquisire la sua uscita in formato binario. Lo strumento esterno che uso è chiamato 'samtools' (samtools.sourceforge.net) e tra le altre cose può restituire i record da un formato di file binario indicizzato chiamato BAM.Acquisizione dell'output binario da Process.StandardOutput

Utilizzo Process.Start per eseguire il comando esterno e so che posso acquisire l'output reindirizzando Process.StandardOutput. Il problema è che è un flusso di testo con una codifica, quindi non mi dà accesso ai byte grezzi dell'output. La soluzione quasi funzionante che ho trovato è quella di accedere al flusso sottostante.

Ecco il mio codice:

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

// ... ulteriori analisi segue

Ma quando ho eseguito questo, le prime 23bytes che ho letto non sono i primi 23 byte nel ouput, ma piuttosto da qualche parte diverse centinaia di migliaia di byte a valle. Presumo che StreamReader faccia un po 'di buffering e quindi il flusso sottostante è già avanzato, diciamo 4K nell'output. Il flusso sottostante non supporta la ricerca fino all'inizio.

E sono bloccato qui. Qualcuno ha una soluzione funzionante per eseguire un comando esterno e catturare il suo stdout in formato binario? L'uscita potrebbe essere molto grande, quindi mi piacerebbe trasmetterlo in streaming.

Qualsiasi aiuto apprezzato.

A proposito, la mia soluzione attuale è quella di avere samtools restituire i record in formato testo, quindi analizzarli, ma questo è piuttosto lento e spero di accelerare le cose usando direttamente il formato binario.

+0

L'unica cosa che riesco a pensare a mano a mano sarebbe quella di impostare la codifica desiderata su Unicode e quindi separare ogni carattere dallo StreamReader in due byte. Quale sarebbe un attacco orribile, e probabilmente fallirebbe miseramente se l'output avesse un numero dispari di byte. Una soluzione alternativa sarebbe implementare la propria codifica che mappa i byte direttamente ai loro rispettivi valori di char, come ASCII ma senza convertire il set superiore in "?". Ma lascerò che qualcun altro fornisca una risposta adeguata. :) – cdhowie

risposta

24

utilizzando StandardOutput.BaseStream è l'approccio corretto, ma non deve usare qualsiasi altra proprietà o metodo di cmdProcess.StandardOutput. Ad esempio, l'accesso a cmdProcess.StandardOutput.EndOfStream causerà StreamReader per StandardOutput per leggere parte del flusso, rimuovendo i dati a cui si desidera accedere.

Invece, è sufficiente leggere e analizzare i dati da br (presupponendo che si sappia come analizzare i dati e non si leggerà oltre la fine del flusso o si desidera prendere uno EndOfStreamException). In alternativa, se non si conosce la dimensione dei dati, utilizzare Stream.CopyTo per copiare l'intero flusso di output standard in un nuovo file o flusso di memoria.

+2

E dove deve essere chiamato Stream.CopyTo per gestire l'intero output che può essere estremamente grande? – SerG

7

Poiché si specifica esplicitamente l'esecuzione su Suse linux e mono, è possibile aggirare il problema utilizzando le chiamate unix native per creare il reindirizzamento e la lettura dallo stream. Come ad esempio:

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

sotto Unix, descrittori di file sono ereditate dai processi figli, a meno marcata altrimenti (vicino a exec). Pertanto, per reindirizzare stdout di un figlio, è sufficiente modificare il descrittore di file n. 1 nel processo principale prima di chiamare exec. Unix fornisce anche una cosa utile chiamata pipe che è un canale di comunicazione unidirezionale, con due descrittori di file che rappresentano i due endpoint. Per duplicare i descrittori di file, è possibile utilizzare dup o dup2 che creano entrambi una copia equivalente di un descrittore, ma dup restituisce un nuovo descrittore assegnato dal sistema e dup2 posiziona la copia in un obiettivo specifico (chiudendolo se necessario).Ciò che il codice di cui sopra fa, allora:

  1. Crea un tubo con endpoint reading e writing
  2. Salva una copia della corrente stdout descrittore
  3. Assegna endpoint scrittura del tubo stdout e chiude l'originale
  4. Avvia il processo figlio in modo che erediti stdout connesso all'endpoint di scrittura della pipe
  5. Ripristina losalvato.
  6. legge dal reading finale del tubo avvolgendolo in un UnixStream

nota, in codice nativo, un processo di solito è iniziato da una coppia fork + exec, quindi i descrittori di file può essere modificato in il processo figlio stesso, ma prima che il nuovo programma venga caricato. Questa versione gestita non è thread-safe in quanto deve modificare temporaneamente il stdout del processo padre.

Poiché il codice avvia il processo figlio senza reindirizzamento gestito, il runtime .NET non modifica alcun descrittore né crea alcun flusso. Quindi, l'unico lettore di uscita del bambino sarà il codice utente, che utilizza un UnixStream per aggirare problema di codifica 's il StreamReader,

+0

Puoi commentare (1) come la pipa viene collegata al nuovo processo 'stdout, e (2) come funziona il problema in cui lo StreamReader buffera alcuni byte alla sua creazione? – cdhowie

+0

Ho aggiornato la risposta. – Jester

1

Ho verificato cosa sta succedendo con il riflettore. Mi sembra che StreamReader non legga finché non chiami su di esso. Ma è creato con una dimensione del buffer di 0x1000, quindi forse lo fa. Ma fortunatamente, finché non lo leggi, puoi tranquillamente estrarre i dati bufferizzati: ha un byte di campo privato [] byteBuffer e due campi interi, byteLen e bytePos, il primo indica quanti byte ci sono nel buffer , il secondo significa quanti ne hai consumati, dovrebbe essere zero. Quindi, prima leggi questo buffer con reflection, quindi crea il BinaryReader.

+0

Oh ora capisco, si chiama EndOfStream, che causa davvero una lettura bufferizzata. Quindi, come ha suggerito Bradley, non farlo, e starai bene senza scherzare con i campi privati. – fejesjoco