繁体   English   中英

从 Process.StandardOutput 捕获二进制输出

[英]Capturing binary output from 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 以文本格式返回记录,然后解析这些记录,但这很慢,我希望通过直接使用二进制格式来加快速度。

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

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

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

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

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

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

我检查了反射器发生了什么。 在我看来,StreamReader 在您调用 read 之前不会读取。 但它是用 0x1000 的缓冲区大小创建的,所以也许它确实如此。 但幸运的是,在您真正从中读取之前,您可以安全地从中获取缓冲数据:它有一个私有字段 byte[] byteBuffer,以及两个整数字段 byteLen 和 bytePos,第一个表示缓冲区中有多少字节,第二个意思是你吃了多少,应该是零。 所以首先用反射读取这个缓冲区,然后创建 BinaryReader。

您可以使用CliWrapSystem.Diagnostics.Process抽象为用于运行 shell 命令的富有表现力的 API。 例如,您可以这样做:

var output = new MemoryStream(); // a stream, but CliWrap supports other targets too

var cmd = Cli.Wrap("app.exe").WithArguments("foo bar") | output;

await cmd.ExecuteAsync();

也许你可以这样尝试:

public class ThirdExe
{
    private static TongueSvr _instance = null;
    private Diagnostics.Process _process = null;
    private Stream _messageStream;
    private byte[] _recvBuff = new byte[65536];
    private int _recvBuffLen;
    private Queue<TonguePb.Msg> _msgQueue = new Queue<TonguePb.Msg>();
    void StartProcess()
    {
        try
        {
            _process = new Diagnostics.Process();
            _process.EnableRaisingEvents = false;
            _process.StartInfo.FileName = "d:/code/boot/tongueerl_d.exe"; // Your exe
            _process.StartInfo.UseShellExecute = false;
            _process.StartInfo.CreateNoWindow = true;
            _process.StartInfo.RedirectStandardOutput = true;
            _process.StartInfo.RedirectStandardInput = true;
            _process.StartInfo.RedirectStandardError = true;
            _process.ErrorDataReceived += new Diagnostics.DataReceivedEventHandler(ErrorReceived);
            _process.Exited += new EventHandler(OnProcessExit);
            _process.Start();
            _messageStream = _process.StandardInput.BaseStream;
            _process.BeginErrorReadLine();
            AsyncRead();

        }
        catch (Exception e)
        {
            Debug.LogError("Unable to launch app: " + e.Message);
        }

    private void AsyncRead()
    {
        _process.StandardOutput.BaseStream.BeginRead(_recvBuff, 0, _recvBuff.Length
                , new AsyncCallback(DataReceived), null);
    }

    void DataReceived(IAsyncResult asyncResult)
    {
        int nread = _process.StandardOutput.BaseStream.EndRead(asyncResult);
        if (nread == 0)
        {
            Debug.Log("process read finished"); // process exit
            return;
        }
        _recvBuffLen += nread;
        Debug.LogFormat("recv data size.{0}  remain.{1}", nread, _recvBuffLen);
        ParseMsg();
        AsyncRead();
    }
    void ParseMsg()
    {
        if (_recvBuffLen < 4)
        {
            return;
        }
        int len = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_recvBuff, 0));
        if (len > _recvBuffLen - 4)
        {
            Debug.LogFormat("current call can't parse the NetMsg for data incomplete");
            return;
        }
        TonguePb.Msg msg = TonguePb.Msg.Parser.ParseFrom(_recvBuff, 4, len);
        Debug.LogFormat("recv msg count.{1}:\n {0} ", msg.ToString(), _msgQueue.Count + 1);
        _recvBuffLen -= len + 4;
        _msgQueue.Enqueue(msg);
    }

关键是_process.StandardOutput.BaseStream.BeginRead(_recvBuff, 0, _recvBuff.Length, new AsyncCallback(DataReceived), null); 非常重要的是转换为像Process.OutputDataReceived这样的异步读取事件。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM