Captura de salida binaria de Process.StandardOutput

En C # (.NET 4.0 ejecutándose bajo Mono 2.8 en SuSE) me gustaría ejecutar un comando de proceso por lotes externo y capturar su salida en forma binaria. La herramienta externa que uso se llama ‘samtools’ (samtools.sourceforge.net) y, entre otras cosas, puede devolver registros de un formato de archivo binario indexado llamado BAM.

Utilizo Process.Start para ejecutar el comando externo, y sé que puedo capturar su salida redirigiendo Process.StandardOutput. El problema es que es una secuencia de texto con una encoding, por lo que no me da acceso a los bytes sin formato de la salida. La solución casi operativa que encontré es acceder a la transmisión subyacente.

Aquí está mi código:

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

// … más análisis sigue

Pero cuando ejecuto esto, los primeros 23bytes que leo no son los primeros 23 bytes en la salida, sino en algún lugar varios cientos o miles de bytes en sentido descendente. Supongo que StreamReader hace algo de almacenamiento en búfer y, por lo tanto, la secuencia subyacente ya está avanzada, digamos 4K en la salida. La secuencia subyacente no es compatible con la búsqueda desde el principio.

Y estoy atrapado aquí. ¿Alguien tiene una solución de trabajo para ejecutar un comando externo y capturar su stdout en forma binaria? La salida puede ser muy grande, por lo que me gustaría transmitirla.

Cualquier ayuda apreciada.

Por cierto, mi solución actual es hacer que los samtools devuelvan los registros en formato de texto, luego analizarlos, pero esto es bastante lento y espero acelerar las cosas usando directamente el formato binario.

El uso de StandardOutput.BaseStream es el enfoque correcto, pero no debe usar ninguna otra propiedad o método de cmdProcess.StandardOutput . Por ejemplo, al acceder a cmdProcess.StandardOutput.EndOfStream , StreamReader for StandardOutput leerá parte de la transmisión y eliminará los datos a los que desee acceder.

En su lugar, simplemente lea y analice los datos de br (suponiendo que sepa cómo analizar los datos, y que no leerán más allá del final de la transmisión, o que estén dispuestos a atrapar una EndOfStreamException ). Alternativamente, si no sabe qué tan grande es la información, use Stream.CopyTo para copiar toda la secuencia de salida estándar a un nuevo archivo o secuencia de memoria.

Como especificó explícitamente que se ejecuta en Suse linux y mono, puede solucionar el problema utilizando llamadas unix nativas para crear la redirección y leer de la transmisión. Como:

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

En Unix, los descriptores de archivos son heredados por procesos secundarios, a menos que se indique lo contrario ( close on exec ). Por lo tanto, para redirigir la stdout de un elemento secundario, todo lo que necesita hacer es cambiar el descriptor de archivo # 1 en el proceso principal antes de llamar a exec . Unix también proporciona una práctica llamada canalización, que es un canal de comunicación unidireccional, con dos descriptores de archivo que representan los dos puntos finales. Para duplicar descriptores de archivos, puede usar dup o dup2 ambos crean una copia equivalente de un descriptor, pero dup devuelve un nuevo descriptor asignado por el sistema y dup2 coloca la copia en un objective específico (cerrándolo si es necesario). Lo que hace el código anterior, entonces:

  1. Crea una tubería con terminales que reading y writing
  2. Guarda una copia del descriptor stdout actual
  3. Asigna el extremo de escritura de la tubería a stdout y cierra el original
  4. Inicia el proceso secundario para que herede stdout conectado al punto final de escritura de la tubería
  5. Restaura el stdout guardado
  6. Lee desde el punto final de reading de la tubería envolviéndolo en un UnixStream

Tenga en cuenta que, en el código nativo, un proceso generalmente se inicia con un par fork + exec , por lo que los descriptores de archivos pueden modificarse en el proceso hijo, pero antes de que se cargue el nuevo progtwig. Esta versión administrada no es segura para subprocesos, ya que tiene que modificar temporalmente la stdout del proceso principal.

Como el código inicia el proceso secundario sin redirección administrada, el tiempo de ejecución de .NET no cambia ningún descriptor ni crea ningún flujo. Por lo tanto, el único lector de la salida del hijo será el código de usuario, que utiliza un UnixStream para evitar el problema de encoding de StreamReader .

Revisé lo que está sucediendo con el reflector. Me parece que StreamReader no lee hasta que llame a leer en él. Pero está creado con un tamaño de buffer de 0x1000, entonces tal vez sí. Pero afortunadamente, hasta que realmente lea, puede sacar los datos almacenados de forma segura: tiene un byte de campo privado [] byteBuffer y dos campos enteros, byteLen y bytePos, el primero significa cuántos bytes hay en el búfer , el segundo significa cuántos ha consumido, debería ser cero. Así que primero lee este buffer con reflexión, luego crea el BinaryReader.