Подтвердить что ты не робот

Захват двоичного вывода из Process.StandardOutput

В С# (.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 в вывод. Основной поток не поддерживает возврат к началу.

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

  • Создает канал с конечными точками reading и writing
  • Сохраняет копию текущего дескриптора stdout
  • Назначает конечную точку записи на трубку stdout и закрывает оригинал
  • Запускает дочерний процесс, чтобы он наследовал stdout, подключенный к конечной точке записи канала
  • Восстанавливает сохраненный stdout
  • Считывается из конечной точки reading для канала, обертывая его в UnixStream

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

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

Ответ 3

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