Process.start: 如何获取输出?

404

我想从我的Mono/.NET应用程序中运行一个外部命令行程序。例如,我想要运行mencoder。这是否可能:

  1. 获取命令行shell输出,并将其写入我的文本框中?
  2. 获得数字值以显示经过的时间的进度条?
10个回答

568

当你创建Process对象时,适当地设置StartInfo

var proc = new Process 
{
    StartInfo = new ProcessStartInfo
    {
        FileName = "program.exe",
        Arguments = "command line arguments to your executable",
        UseShellExecute = false,
        RedirectStandardOutput = true,
        CreateNoWindow = true
    }
};

然后启动该进程并从中读取:

proc.Start();
while (!proc.StandardOutput.EndOfStream)
{
    string line = proc.StandardOutput.ReadLine();
    // do something with line
}

您可以使用int.Parse()int.TryParse()将字符串转换为数字值。如果读取的字符串中有无效的数字字符,则可能需要进行一些字符串操作。


6
我在想你如何处理StandardError?顺便说一下,我真的很喜欢这段代码片段!非常简洁明了。 - codea
3
谢谢,但我觉得我的表述不够清晰:我应该再添加一个循环来实现这个功能吗? - codea
@codea - 我明白了。您可以创建一个循环,当两个流都到达EOF时终止。这可能会变得有点复杂,因为一个流不可避免地会先到达EOF,您不希望再从中读取。您还可以在两个不同的线程中使用两个循环。 - Ferruccio
2
对于读取流,等待进程自身终止是否更加健壮,而不是等待流结束? - Gusdor
2
@Gusdor - 我不这么认为。当进程终止时,它的流将自动关闭。此外,进程可能在终止之前很久就关闭了其流。 - Ferruccio
1
我正在尝试在Ffmpeg上使用这段代码,需要帮助,卡在找出进程工作是否完成的地方。 - Anirudha Gupta

380

你可以同步或异步地处理输出。

1. 同步示例

static void runCommand()
{
    Process process = new Process();
    process.StartInfo.FileName = "cmd.exe";
    process.StartInfo.Arguments = "/c DIR"; // Note the /c command (*)
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    process.Start();
    //* Read the output (or the error)
    string output = process.StandardOutput.ReadToEnd();
    Console.WriteLine(output);
    string err = process.StandardError.ReadToEnd();
    Console.WriteLine(err);
    process.WaitForExit();
}
注意,最好同时处理输出错误:它们必须分别处理。

(*) 对于某些命令(这里是StartInfo.Arguments),您必须添加/c指令,否则进程将在WaitForExit()中冻结。

2. 异步示例

static void runCommand() 
{
    //* Create your Process
    Process process = new Process();
    process.StartInfo.FileName = "cmd.exe";
    process.StartInfo.Arguments = "/c DIR";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    //* Set your output and error (asynchronous) handlers
    process.OutputDataReceived += new DataReceivedEventHandler(OutputHandler);
    process.ErrorDataReceived += new DataReceivedEventHandler(OutputHandler);
    //* Start process and handlers
    process.Start();
    process.BeginOutputReadLine();
    process.BeginErrorReadLine();
    process.WaitForExit();
}

static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine) 
{
    //* Do your stuff with the output (write to console/log/StringBuilder)
    Console.WriteLine(outLine.Data);
}

如果您不需要对输出进行复杂操作,可以绕过OutputHandler方法,直接行内添加处理程序:

//* Set your output and error (asynchronous) handlers
process.OutputDataReceived += (s, e) => Console.WriteLine(e.Data);
process.ErrorDataReceived += (s, e) => Console.WriteLine(e.Data);

5
一定要喜欢异步编程!我能够(稍作改写)在 VB.net 中使用这段代码。 - Richard Barker
9
请注意:你的第一个(同步)方法是不正确的!你不应该同时同步读取StandardOutput和StandardError!这将导致死锁。至少其中之一必须是异步的。 - S.Serpooshan
14
Process.WaitForExit() 是线程阻塞的,因此是同步的。虽然这不是答案的重点,但我认为我可以补充一下。加上 process.EnableRaisingEvents = true 并利用 Exited 事件来实现完全异步化。 - Tom
1
您缺少一个Dispose。 - Samuel
1
这太棒了。和@RichardBarker一样,我也能够将其转录到VB.Net中使用,而且它完全符合我的需求!为每个“OutputDataReceived”和“ErrorDataReceived”添加事件处理程序,并将数据附加到公共字符串生成器(在多个shell命令中使用它们)使我能够钩住StdOut / StdErr数据并处理它以向我的用户提供反馈!太棒了! - k1dfr0std
显示剩余4条评论

26

好的,对于任何想要同时读取错误和输出但在其他答案提供的解决方案(像我一样)中遇到死锁的人,这里是一个解决方案,我在阅读了MSDN有关StandardOutput属性的说明后构建了它。

答案基于T30的代码:

static void runCommand()
{
    //* Create your Process
    Process process = new Process();
    process.StartInfo.FileName = "cmd.exe";
    process.StartInfo.Arguments = "/c DIR";
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.RedirectStandardError = true;
    //* Set ONLY ONE handler here.
    process.ErrorDataReceived += new DataReceivedEventHandler(ErrorOutputHandler);
    //* Start process
    process.Start();
    //* Read one element asynchronously
    process.BeginErrorReadLine();
    //* Read the other one synchronously
    string output = process.StandardOutput.ReadToEnd();
    Console.WriteLine(output);
    process.WaitForExit();
}

static void ErrorOutputHandler(object sendingProcess, DataReceivedEventArgs outLine) 
{
    //* Do your stuff with the output (write to console/log/StringBuilder)
    Console.WriteLine(outLine.Data);
}

谢谢您添加这个。我可以问一下您使用的是什么命令吗? - T30
我正在使用C#开发一个应用程序,旨在启动mysqldump.exe,向用户显示应用程序生成的每个消息,等待其完成,然后执行一些其他任务。我不明白你在说什么样的命令?整个问题都是关于从C#启动进程的。 - cubrman
1
如果您使用两个单独的处理程序,就不会出现死锁。 - ovi
在你的例子中,你只读取了 process.StandardOutput 一次...就在你启动它之后,但是当进程正在运行时,人们希望能够持续地读取它,不是吗? - ovi
@Curbman,我认为T30是在问“什么命令”,因为你正在启动名为“cmd.exe”的进程。 - Eric Wood

10

在 .NET 中,读取进程的输出流是标准方式,方法是从 Process 的 StandardOutput 流中读取。链接的 MSDN 文档中有示例。类似地,您可以从StandardError 读取,并写入StandardInput


6

6
你可以使用共享内存让两个进程通信,可以查看MemoryMappedFile
你需要在父进程中使用 "using" 语句创建一个内存映射文件mmf,然后创建第二个进程直到其终止,并让它使用 BinaryWriter 将结果写入 mmf 中。接着在父进程中从mmf读取结果,你也可以通过命令行参数或硬编码方式传递mmf名称。
使用映射文件时,请确保在父进程中释放映射文件之前,子进程已将结果写入映射文件中。
示例: 父进程
    private static void Main(string[] args)
    {
        using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("memfile", 128))
        {
            using (MemoryMappedViewStream stream = mmf.CreateViewStream())
            {
                BinaryWriter writer = new BinaryWriter(stream);
                writer.Write(512);
            }

            Console.WriteLine("Starting the child process");
            // Command line args are separated by a space
            Process p = Process.Start("ChildProcess.exe", "memfile");

            Console.WriteLine("Waiting child to die");

            p.WaitForExit();
            Console.WriteLine("Child died");

            using (MemoryMappedViewStream stream = mmf.CreateViewStream())
            {
                BinaryReader reader = new BinaryReader(stream);
                Console.WriteLine("Result:" + reader.ReadInt32());
            }
        }
        Console.WriteLine("Press any key to continue...");
        Console.ReadKey();
    }

子进程
    private static void Main(string[] args)
    {
        Console.WriteLine("Child process started");
        string mmfName = args[0];

        using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(mmfName))
        {
            int readValue;
            using (MemoryMappedViewStream stream = mmf.CreateViewStream())
            {
                BinaryReader reader = new BinaryReader(stream);
                Console.WriteLine("child reading: " + (readValue = reader.ReadInt32()));
            }
            using (MemoryMappedViewStream input = mmf.CreateViewStream())
            {
                BinaryWriter writer = new BinaryWriter(input);
                writer.Write(readValue * 2);
            }
        }

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey();
    }

要使用这个示例,您需要创建一个包含两个项目的解决方案,然后将子进程的构建结果从 %childDir%/bin/debug 复制到 %parentDirectory%/bin/debug 中,最后运行父项目。 childDirparentDirectory 是您电脑上项目的文件夹名称。
祝好运 :)

3
我在调用 Process.StandardOutput.ReadLineProcess.StandardOutput.ReadToEnd 时遇到了臭名昭著的死锁问题。
我的目标/用例很简单。启动一个进程并重定向它的输出,以便我可以捕获该输出并通过.NET Core的ILogger<T>将其记录到控制台,同时还将重定向的输出附加到文件日志中。
以下是我使用内置异步事件处理程序Process.OutputDataReceivedProcess.ErrorDataReceived的解决方案。
var p = new Process
{
    StartInfo = new ProcessStartInfo(
        command.FileName, command.Arguments
    )
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
    }
};


// Asynchronously pushes StdOut and StdErr lines to a thread safe FIFO queue
var logQueue = new ConcurrentQueue<string>();
p.OutputDataReceived += (sender, args) => logQueue.Enqueue(args.Data);
p.ErrorDataReceived += (sender, args) => logQueue.Enqueue(args.Data);

// Start the process and begin streaming StdOut/StdErr
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();

// Loop until the process has exited or the CancellationToken is triggered
do
{
    var lines = new List<string>();
    while (logQueue.TryDequeue(out var log))
    {
        lines.Add(log);
        _logger.LogInformation(log)
    }
    File.AppendAllLines(_logFilePath, lines);

    // Asynchronously sleep for some time
    try
    {
        Task.Delay(5000, stoppingToken).Wait(stoppingToken);
    }
    catch(OperationCanceledException) {}

} while (!p.HasExited && !stoppingToken.IsCancellationRequested);

3
您可以使用以下代码记录进程输出:
ProcessStartInfo pinfo = new ProcessStartInfo(item);
pinfo.CreateNoWindow = false;
pinfo.UseShellExecute = true;
pinfo.RedirectStandardOutput = true;
pinfo.RedirectStandardInput = true;
pinfo.RedirectStandardError = true;
pinfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
var p = Process.Start(pinfo);
p.WaitForExit();
Process process = Process.Start(new ProcessStartInfo((item + '>' + item + ".txt"))
{
    UseShellExecute = false,
    RedirectStandardOutput = true
});
process.WaitForExit();
string output = process.StandardOutput.ReadToEnd();
if (process.ExitCode != 0) { 
}

2

如何启动进程(例如批处理文件、Perl脚本、控制台程序)并在Windows表单上显示其标准输出:

processCaller = new ProcessCaller(this);
//processCaller.FileName = @"..\..\hello.bat";
processCaller.FileName = @"commandline.exe";
processCaller.Arguments = "";
processCaller.StdErrReceived += new DataReceivedHandler(writeStreamInfo);
processCaller.StdOutReceived += new DataReceivedHandler(writeStreamInfo);
processCaller.Completed += new EventHandler(processCompletedOrCanceled);
processCaller.Cancelled += new EventHandler(processCompletedOrCanceled);
// processCaller.Failed += no event handler for this one, yet.

this.richTextBox1.Text = "Started function.  Please stand by.." + Environment.NewLine;

// the following function starts a process and returns immediately,
// thus allowing the form to stay responsive.
processCaller.Start();    

您可以在此链接上找到 ProcessCaller启动进程并显示其标准输出


1

在 Windows 和 Linux 上对我有效的解决方案如下:

// GET api/values
        [HttpGet("cifrado/{xml}")]
        public ActionResult<IEnumerable<string>> Cifrado(String xml)
        {
            String nombreXML = DateTime.Now.ToString("ddMMyyyyhhmmss").ToString();
            String archivo = "/app/files/"+nombreXML + ".XML";
            String comando = " --armor --recipient bibankingprd@bi.com.gt  --encrypt " + archivo;
            try{
                System.IO.File.WriteAllText(archivo, xml);                
                //String comando = "C:\\GnuPG\\bin\\gpg.exe --recipient licorera@local.com --armor --encrypt C:\\Users\\Administrador\\Documents\\pruebas\\nuevo.xml ";
                ProcessStartInfo startInfo = new ProcessStartInfo() {FileName = "/usr/bin/gpg",  Arguments = comando }; 
                Process proc = new Process() { StartInfo = startInfo, };
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.RedirectStandardError = true;
                proc.Start();
                proc.WaitForExit();
                Console.WriteLine(proc.StandardOutput.ReadToEnd());
                return new string[] { "Archivo encriptado", archivo + " - "+ comando};
            }catch (Exception exception){
                return new string[] { archivo, "exception: "+exception.ToString() + " - "+ comando };
            }
        }

通用catch(Exception)捕获的异常必须重新抛出,否则它会吞噬异常,这可能会被“上层”代码等待。在给定的示例中,如果异常发生在try块内部,则调试器不会停止。 - Evgeny Gorbovoy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接