天天看點

使用 C# 捕獲程序輸出

使用 C# 捕獲程序輸出

Intro​​#​​

很多時候我們可能會需要執行一段指令擷取一個輸出,遇到的比較典型的就是之前我們需要用 ​

​FFMpeg​

​ 實作視訊的編碼壓縮水印等一系列操作,當時使用的是 ​

​FFMpegCore​

​ 這個類庫,這個類庫的實作原理是啟動另外一個程序,啟動 ffmpeg 并傳遞相應的處理參數,并根據程序輸出擷取處理進度

為了友善使用,實作了兩個幫助類來友善的擷取程序的輸出,分别是 ​

​ProcessExecutor​

​ 和 ​

​CommandRunner​

​,前者更為靈活,可以通過事件添加自己的額外事件訂閱處理,後者為簡化版,主要是隻擷取輸出的場景,兩者的實作原理大體是一樣的,啟動一個 Process,并監聽其輸出事件擷取輸出

​ProcessExecutor​

​​​#​​

使用示例,這個示例是擷取儲存 nuget 包的路徑的一個示例:

Copy

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​​#​​

  • ​​https://github.com/rosenbjerg/FFMpegCore​​
  • ​​https://github.com/WeihanLi/WeihanLi.Common/blob/dev/src/WeihanLi.Common/Helpers/ProcessExecutor.cs​​
  • ​​https://github.com/WeihanLi/WeihanLi.Common/blob/dev/test/WeihanLi.Common.Test/HelpersTest/ProcessExecutorTest.cs​​
  • ​​https://github.com/WeihanLi/WeihanLi.Common/blob/dev/test/WeihanLi.Common.Test/HelpersTest/CommandRunnerTest.cs​​

出處:​​javascript:void(0)​​

​FFMpeg​

​FFMpegCore​

​ProcessExecutor​

​CommandRunner​

​ProcessExecutor​

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; }
}
      

繼續閱讀