C# 从Process.StandardOutput捕获二进制输出

C# 从Process.StandardOutput捕获二进制输出,c#,process,binary,C#,Process,Binary,在C#(在SuSE上Mono 2.8下运行的.NET 4.0)中,我希望运行一个外部批处理命令并以二进制形式捕获其输出。我使用的外部工具称为“samtools”(samtools.sourceforge.net),除此之外,它还可以从名为BAM的索引二进制文件格式返回记录 我使用Process.Start来运行外部命令,我知道我可以通过重定向Process.StandardOutput来捕获它的输出。问题是,这是一个带有编码的文本流,因此它不允许我访问输出的原始字节。我发现的几乎有效的解决方案是

在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);
//。。。接下来是更多的解析

但当我运行这个程序时,我读取的前23个字节不是输出中的前23个字节,而是下游几百或几千个字节。我假设StreamReader进行了一些缓冲,因此底层流已经被提升到输出中,比如说4K。基础流不支持返回到起始位置

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

谢谢你的帮助


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

因为您明确指定在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下,文件描述符由子进程继承,除非另有标记(执行时关闭)。因此,要重定向子进程的
stdout
,只需在调用
exec
之前更改父进程中的文件描述符1即可。Unix还提供了一种称为管道的便利工具,它是一种单向通信通道,具有两个表示两个端点的文件描述符。对于复制文件描述符,可以使用
dup
dup2
这两种方法创建描述符的等效副本,但
dup
返回系统分配的新描述符,并
dup2
将副本放置在特定目标中(必要时关闭)。那么,上述代码的作用是:

  • 创建端点为<代码>读取<代码>和<代码>写入<代码>的管道
  • 保存当前
    stdout
    描述符的副本
  • 将管道的写入端点指定给标准输出并关闭原始
  • 启动子进程,使其继承连接到管道写入端点的
    stdout
  • 恢复保存的
    stdout
  • 通过在
    UnixStream中包装管道,从管道的
    读取
    端点读取
  • 注意,在本机代码中,进程通常由
    fork
    +
    exec
    对启动,因此可以在子进程本身中修改文件描述符,但必须在加载新程序之前进行。此托管版本不是线程安全的,因为它必须临时修改父进程的
    stdout


    由于代码在没有托管重定向的情况下启动子进程,.NET运行时不会更改任何描述符或创建任何流。因此,孩子输出的唯一读取器将是用户代码,它使用
    UnixStream
    来解决
    StreamReader
    的编码问题,使用
    StandardOutput。BaseStream
    是正确的方法,但您不能使用
    cmdProcess.StandardOutput
    的任何其他属性或方法。例如,访问
    cmdProcess.StandardOutput.EndOfStream
    将导致
    StandardOutput
    StreamReader
    读取流的一部分,从而删除要访问的数据


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

    我查看了reflector的情况。在我看来,StreamReader在你调用read之前是不会阅读的。但它是用0x1000的缓冲区大小创建的,所以可能是这样。但幸运的是,在您实际读取之前,您可以安全地从中获取缓冲数据:它有一个私有字段byte[]byteBuffer,和两个整数字段byteLen和bytePos,第一个表示缓冲区中有多少字节,第二个表示您消耗了多少字节,应该为零。因此,首先通过反射读取此缓冲区,然后创建BinaryReader。

    您可以使用它在运行shell命令的表达式API后面抽象出
    System.Diagnostics.Process
    。例如,您可以执行以下操作:

    var output=newmemoryStream();//流,但CliWrap也支持其他目标
    var cmd=Cli.Wrap(“app.exe”)。带参数(“foo-bar”)|输出;
    wait cmd.ExecuteAsync();
    
    我能想到的唯一一件事就是将所需的编码设置为Unicode,然后将StreamReader中的每个字符分成两个字节。这将是一次可怕的黑客攻击,如果输出的字节数为奇数,则很可能会失败。解决方法是实现自己的编码,将字节直接映射到各自的字符值,如ASCII,但不将上限转换为“?”。但我会让其他人想出一个恰当的答案你能评论一下(1)管道是如何连接到新进程的stdout的,以及(2)这是如何解决StreamReader在创建时缓冲一些字节的问题的吗?哦,现在我明白了,你称之为EndOfStream,这确实会导致缓冲读取。就像B一样