在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以文本格式返回记录,然后解析那些,但这很慢,我希望通过直接使用二进制格式来加快速度。
答案 0 :(得分:29)
使用StandardOutput.BaseStream
是正确的方法,但您不得使用cmdProcess.StandardOutput
的任何其他属性或方法。例如,访问cmdProcess.StandardOutput.EndOfStream
会导致StreamReader
StandardOutput
读取部分流,删除您要访问的数据。
相反,只需从br
读取并解析数据(假设您知道如何解析数据,并且不会读取流的末尾,或者愿意捕获EndOfStreamException
) 。或者,如果您不知道数据有多大,请使用Stream.CopyTo
将整个标准输出流复制到新文件或内存流中。
答案 1 :(得分: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);
}
}
在unix下,文件描述符由子进程继承,除非另有标记(关闭exec )。因此,要重定向子项的stdout
,您需要做的就是在调用exec
之前更改父进程中的文件描述符#1。 Unix还提供了一个称为 pipe 的方便的东西,它是一个单向通信通道,有两个文件描述符代表两个端点。对于重复文件描述符,您可以使用dup
或dup2
两者创建描述符的等效副本,但dup
返回系统分配的新描述符dup2
将副本放在特定目标中(必要时关闭它)。以上代码的作用是:
reading
和writing
stdout
描述符stdout
并关闭原始stdout
stdout
reading
UnixStream
端点读取
醇>
注意,在本机代码中,进程通常由fork
+ exec
对启动,因此可以在子进程本身中修改文件描述符,但是在加载新程序之前。此托管版本不是线程安全的,因为它必须临时修改父进程的stdout
。
由于代码在没有托管重定向的情况下启动子进程,因此.NET运行时不会更改任何描述符或创建任何流。因此,孩子输出的唯一读者将是用户代码,该代码使用UnixStream
解决StreamReader
的编码问题,
答案 2 :(得分:1)
我查看了反射器发生了什么。在我看来,StreamReader在您调用read之前不会读取。但是它的缓冲区大小为0x1000,所以可能会这样。但幸运的是,在你实际读取它之前,你可以安全地从中获取缓冲数据:它有一个私有字段byte [] byteBuffer,以及两个整数字段byteLen和bytePos,第一个意味着缓冲区中有多少字节,第二个意味着你消耗了多少,应该为零。所以首先用反射读取这个缓冲区,然后创建BinaryReader。
答案 3 :(得分:1)
您可以使用CliWrap来将System.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();