Захват двоичного вывода из Process.StandardOutput - PullRequest
24 голосов
/ 10 ноября 2010

В C # (.NET 4.0 работает под Mono 2.8 на SuSE) я хотел бы запустить внешнюю пакетную команду и записать ее вывод в двоичном виде. Внешний инструмент, который я использую, называется 'samtools' (samtools.sourceforge.net) и, помимо прочего, он может возвращать записи из индексированного двоичного формата файла, называемого BAM.

Я использую 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);

// ... больше разбора следует

Но когда я запускаю это, первые 23 байта, которые я читаю, это не первые 23 байта в выходном файле, а где-то несколько сотен или тысяч байтов ниже по потоку. Я предполагаю, что StreamReader выполняет некоторую буферизацию, и поэтому основной поток уже расширен, скажем, 4K в вывод. Базовый поток не поддерживает поиск назад к началу.

И я застрял здесь. У кого-нибудь есть работающее решение для запуска внешней команды и записи ее стандартного вывода в двоичном виде? Выход может быть очень большим, поэтому я бы хотел его воспроизвести.

Любая помощь приветствуется.

Кстати, мой обходной путь - чтобы samtools возвращал записи в текстовом формате, а затем анализировал их, но это довольно медленно, и я надеюсь ускорить процесс, используя двоичный формат напрямую.

Ответы [ 3 ]

29 голосов
/ 27 декабря 2010

Использование StandardOutput.BaseStream является правильным подходом, но вы не должны использовать любое другое свойство или метод cmdProcess.StandardOutput. Например, доступ к cmdProcess.StandardOutput.EndOfStream приведет к тому, что StreamReader для StandardOutput прочитает часть потока, удалив данные, к которым вы хотите получить доступ.

Вместо этого просто читайте и анализируйте данные из br (при условии, что вы знаете, как анализировать данные, и не будете читать после окончания потока или готовы перехватить EndOfStreamException). В качестве альтернативы, если вы не знаете, насколько велики данные, используйте Stream.CopyTo, чтобы скопировать весь стандартный поток вывода в новый файл или поток памяти.

7 голосов
/ 21 декабря 2010

Поскольку вы явно указали запуск в 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 дескрипторы файлов наследуются дочерними процессами, если не указано иное ( close на exec ).Таким образом, для перенаправления stdout дочернего элемента все, что вам нужно сделать, это изменить дескриптор файла # 1 в родительском процессе перед вызовом exec.Unix также предоставляет удобную вещь, называемую pipe , которая является однонаправленным каналом связи с двумя файловыми дескрипторами, представляющими две конечные точки.Для дублирования файловых дескрипторов вы можете использовать dup или dup2, оба из которых создают эквивалентную копию дескриптора, но dup возвращает новый дескриптор, выделенный системой, а dup2 помещает копию в конкретную цель (закрывая его при необходимости).Что делает приведенный выше код, то:

  1. Создает трубу с конечными точками reading и writing
  2. Сохраняет копию текущего stdoutдескриптор
  3. Назначает конечную точку записи канала stdout и закрывает исходный
  4. Запускает дочерний процесс, поэтому он наследует stdout, подключенный к конечной точке записи канала
  5. Восстанавливает сохраненные stdout
  6. Считывает из конечной точки reading канала, помещая его в UnixStream

Примечание. В собственном коде процесс обычно запускаетсяпарой fork + exec, поэтому дескрипторы файлов могут быть изменены в самом дочернем процессе, но до загрузки новой программы.Эта управляемая версия не является поточно-ориентированной, поскольку она должна временно изменить stdout родительского процесса.

Поскольку код запускает дочерний процесс без управляемого перенаправления, среда выполнения .NET не изменяет дескрипторы илисоздавать любые потоки.Таким образом, единственным читателем выходных данных дочернего элемента будет код пользователя, который использует UnixStream, чтобы обойти проблему кодирования StreamReader,

1 голос
/ 27 декабря 2010

Я проверил, что происходит с отражателем.Мне кажется, что StreamReader не читает, пока вы не вызовете read на нем.Но он создан с размером буфера 0x1000, так что, возможно, так и есть.Но, к счастью, до тех пор, пока вы действительно не прочитаете из него, вы можете безопасно извлечь из него буферизованные данные: у него есть личное поле byte [] byteBuffer и два целочисленных поля, byteLen и bytePos, первое означает, сколько байтов находится в буфере, второе означает, сколько вы потребляли, должно быть ноль.Поэтому сначала прочитайте этот буфер с отражением, затем создайте BinaryReader.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...