使用 C# 捕获进程输出

  • A+
所属分类:.NET技术
摘要

很多时候我们可能会需要执行一段命令获取一个输出,遇到的比较典型的就是之前我们需要用 FFMpeg 实现视频的编码压缩水印等一系列操作,当时使用的是 FFMpegCore 这个类库,这个类库的实现原理是启动另外一个进程,启动 ffmpeg 并传递相应的处理参数,并根据进程输出获取处理进度


使用 C# 捕获进程输出

Intro

很多时候我们可能会需要执行一段命令获取一个输出,遇到的比较典型的就是之前我们需要用 FFMpeg 实现视频的编码压缩水印等一系列操作,当时使用的是 FFMpegCore 这个类库,这个类库的实现原理是启动另外一个进程,启动 ffmpeg 并传递相应的处理参数,并根据进程输出获取处理进度

为了方便使用,实现了两个帮助类来方便的获取进程的输出,分别是 ProcessExecutorCommandRunner,前者更为灵活,可以通过事件添加自己的额外事件订阅处理,后者为简化版,主要是只获取输出的场景,两者的实现原理大体是一样的,启动一个 Process,并监听其输出事件获取输出

ProcessExecutor

使用示例,这个示例是获取保存 nuget 包的路径的一个示例:

using var executor = new ProcessExecutor("dotnet", "nuget locals global-packages -l"); var folder = string.Empty; executor.OnOutputDataReceived += (sender, str) => {     if(str is null)         return;      Console.WriteLine(str);      if(str.StartsWith("global-packages:"))     {         folder = str.Substring("global-packages:".Length).Trim();                         } }; executor.Execute();  Console.WriteLine(folder); 

ProcessExecutor 实现代码如下:

public class ProcessExecutor : IDisposable {     public event EventHandler<int> OnExited;      public event EventHandler<string> OnOutputDataReceived;      public event EventHandler<string> OnErrorDataReceived;      protected readonly Process _process;      protected bool _started;      public ProcessExecutor(string exePath) : this(new ProcessStartInfo(exePath))     {     }      public ProcessExecutor(string exePath, string arguments) : this(new ProcessStartInfo(exePath, arguments))     {     }      public ProcessExecutor(ProcessStartInfo startInfo)     {         _process = new Process()         {             StartInfo = startInfo,             EnableRaisingEvents = true,         };         _process.StartInfo.UseShellExecute = false;         _process.StartInfo.CreateNoWindow = true;         _process.StartInfo.RedirectStandardOutput = true;         _process.StartInfo.RedirectStandardInput = true;         _process.StartInfo.RedirectStandardError = true;     }      protected virtual void InitializeEvents()     {         _process.OutputDataReceived += (sender, args) =>         {             if (args.Data != null)             {                 OnOutputDataReceived?.Invoke(sender, args.Data);             }         };         _process.ErrorDataReceived += (sender, args) =>         {             if (args.Data != null)             {                 OnErrorDataReceived?.Invoke(sender, args.Data);             }         };         _process.Exited += (sender, args) =>         {             if (sender is Process process)             {                 OnExited?.Invoke(sender, process.ExitCode);             }             else             {                 OnExited?.Invoke(sender, _process.ExitCode);             }         };     }      protected virtual void Start()     {         if (_started)         {             return;         }         _started = true;          _process.Start();         _process.BeginOutputReadLine();         _process.BeginErrorReadLine();         _process.WaitForExit();     }      public async virtual Task SendInput(string input)     {         try         {             await _process.StandardInput.WriteAsync(input!);         }         catch (Exception e)         {             OnErrorDataReceived?.Invoke(_process, e.ToString());         }     }      public virtual int Execute()     {         InitializeEvents();         Start();         return _process.ExitCode;     }      public virtual async Task<int> ExecuteAsync()     {         InitializeEvents();         return await Task.Run(() =>         {             Start();             return _process.ExitCode;         }).ConfigureAwait(false);     }      public virtual void Dispose()     {         _process.Dispose();         OnExited = null;         OnOutputDataReceived = null;         OnErrorDataReceived = null;     } } 

CommandExecutor

上面的这种方式比较灵活但有些繁琐,于是有了下面这个版本

使用示例:

[Fact] public void HostNameTest() {     if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))     {         return;     }      var result = CommandRunner.ExecuteAndCapture("hostname");      var hostName = Dns.GetHostName();     Assert.Equal(hostName, result.StandardOut.TrimEnd());     Assert.Equal(0, result.ExitCode); } 

实现源码:

public static class CommandRunner {     public static int Execute(string commandPath, string arguments = null, string workingDirectory = null)     {         using var process = new Process()         {             StartInfo = new ProcessStartInfo(commandPath, arguments ?? string.Empty)             {                 UseShellExecute = false,                 CreateNoWindow = true,                  WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory             }         };          process.Start();         process.WaitForExit();         return process.ExitCode;     }      public static CommandResult ExecuteAndCapture(string commandPath, string arguments = null, string workingDirectory = null)     {         using var process = new Process()         {             StartInfo = new ProcessStartInfo(commandPath, arguments ?? string.Empty)             {                 UseShellExecute = false,                 CreateNoWindow = true,                  RedirectStandardOutput = true,                 RedirectStandardError = true,                  WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory             }         };         process.Start();         var standardOut = process.StandardOutput.ReadToEnd();         var standardError = process.StandardError.ReadToEnd();         process.WaitForExit();         return new CommandResult(process.ExitCode, standardOut, standardError);     } }  public sealed class CommandResult {     public CommandResult(int exitCode, string standardOut, string standardError)     {         ExitCode = exitCode;         StandardOut = standardOut;         StandardError = standardError;     }      public string StandardOut { get; }     public string StandardError { get; }     public int ExitCode { get; } } 

More

如果只要执行命令获取是否执行成功则使用 CommandRunner.Execute 即可,只获取输出和是否成功可以用 CommandRunner.ExecuteAndCapture 方法,如果想要进一步的添加事件订阅则使用 ProcessExecutor

Reference