从Process.StandardOutput捕获二进制输出

Ste*_*son 24 c# binary process

在C#(在SuSE上以Mono 2.8运行的.NET 4.0)中,我想运行外部批处理命令并以二进制形式捕获其输出.我使用的外部工具称为"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);
Run Code Online (Sandbox Code Playgroud)

// ...更多解析如下

但是当我运行它时,我读取的前23个字节不是输出中的前23个字节,而是下游几百或几千个字节.我假设StreamReader进行了一些缓冲,因此底层流已经提前输入4K.底层流不支持寻求回头.

我被困在这里.有没有人有一个工作的解决方案来运行外部命令并以二进制形式捕获其标准输出?输出可能非常大,所以我想流式传输.

任何帮助赞赏.

顺便说一句,我目前的解决方法是让samtools以文本格式返回记录,然后解析那些,但这非常慢,我希望通过直接使用二进制格式来加快速度.

Bra*_*ger 29

使用StandardOutput.BaseStream是正确的方法,但您不得使用任何其他属性或方法cmdProcess.StandardOutput.例如,访问cmdProcess.StandardOutput.EndOfStream将导致StreamReaderfor StandardOutput读取流的一部分,删除您要访问的数据.

相反,只需从中读取和解析数据br(假设您知道如何解析数据,并且不会读取流的末尾,或者愿意捕获数据EndOfStreamException).或者,如果您不知道数据有多大,请使用Stream.CopyTo将整个标准输出流复制到新文件或内存流.

  • 应该调用Stream.CopyTo来处理可能非常庞大的整个输出? (2认同)

Jes*_*ter 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在unix下,文件描述符由子进程继承,除非另有标记(在exec上关闭).因此,要重定向stdout子项,您需要做的就是在调用之前更改父进程中的文件描述符#1 exec.Unix还提供了一个方便的东西,称为管道,它是一个单向通信通道,有两个文件描述符代表两个端点.对于复制文件描述符,您可以使用dupdup2两者创建描述符的等效副本,但dup返回由系统分配的新描述符,并将dup2副本放在特定目标中(必要时将其关闭).以上代码的作用是:

  1. 使用端点和创建管道readingwriting
  2. 保存当前stdout描述符的副本
  3. 将管道的写入端点分配给stdout原始组件并关闭原始组件
  4. 启动子进程,使其继承stdout连接到管道的写端点
  5. 恢复已保存的 stdout
  6. reading通过将其包装在管道中从管道的端点读取UnixStream

注意,在本机代码中,进程通常由fork+ exec对启动,因此可以在子进程本身中修改文件描述符,但是在加载新程序之前.此托管版本不是线程安全的,因为它必须临时修改stdout父进程.

由于代码在没有托管重定向的情况下启动子进程,因此.NET运行时不会更改任何描述符或创建任何流.因此,孩子输出的唯一读者将是用户代码,它使用a UnixStream来解决StreamReader编码问题,