using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using NewLife;
namespace Rainbow.Services;
/// <summary>Shell 命令执行器。跨平台安全执行 Shell/命令行</summary>
public class ShellExecutor : IShellExecutor
{
/// <summary>命令执行超时(毫秒)</summary>
public Int32 TimeoutMs { get; set; } = 30_000;
/// <summary>Sudo 白名单</summary>
public SudoWhitelist Whitelist { get; set; }
/// <summary>审计日志记录器</summary>
public ShellAuditLogger AuditLogger { get; set; }
/// <summary>是否启用审计</summary>
public Boolean EnableAudit { get; set; } = true;
private static readonly SemaphoreSlim _semaphore = new(4, 4);
/// <summary>构造 Shell 执行器</summary>
/// <param name="whitelist">Sudo 白名单</param>
/// <param name="auditLogger">审计日志记录器</param>
public ShellExecutor(SudoWhitelist whitelist = null, ShellAuditLogger auditLogger = null) { Whitelist = whitelist; AuditLogger = auditLogger; }
/// <summary>同步执行命令</summary>
public ShellResult Execute(String command, String[] arguments, Boolean needSudo = false) => ExecuteAsync(command, arguments, needSudo).GetAwaiter().GetResult();
/// <summary>同步执行命令</summary>
public ShellResult Execute(String command, String arguments, Boolean needSudo = false) => ExecuteAsync(command, arguments, needSudo).GetAwaiter().GetResult();
/// <summary>异步执行命令(数组参数)</summary>
public async Task<ShellResult> ExecuteAsync(String command, String[] arguments, Boolean needSudo = false, CancellationToken cancellationToken = default)
{
if (command.IsNullOrEmpty()) throw new ArgumentNullException(nameof(command));
if (needSudo && Whitelist != null && !Whitelist.IsAllowed(command)) return ShellResult.Fail($"不在白名单: {command}");
return await ExecuteInternalAsync(command, BuildArguments(arguments), needSudo, cancellationToken);
}
/// <summary>异步执行命令</summary>
public async Task<ShellResult> ExecuteAsync(String command, String arguments, Boolean needSudo = false, CancellationToken cancellationToken = default)
{
if (command.IsNullOrEmpty()) throw new ArgumentNullException(nameof(command));
if (needSudo && Whitelist != null && !Whitelist.IsAllowed(command)) return ShellResult.Fail($"不在白名单: {command}");
return await ExecuteInternalAsync(command, arguments, needSudo, cancellationToken);
}
private async Task<ShellResult> ExecuteInternalAsync(String command, String arguments, Boolean needSudo, CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
var result = new ShellResult { Command = command, Arguments = arguments };
// 确定平台对应的 Shell
var (shell, shellArgs) = GetPlatformShell(command, arguments, needSudo);
await _semaphore.WaitAsync(cancellationToken);
try
{
var psi = new ProcessStartInfo
{
FileName = shell,
Arguments = shellArgs,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = psi };
var stdout = new StringBuilder();
var stderr = new StringBuilder();
process.OutputDataReceived += (_, e) => { if (e.Data != null) stdout.AppendLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) stderr.AppendLine(e.Data); };
try
{
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!process.WaitForExit(TimeoutMs))
{
process.Kill();
result.ExitCode = -1;
result.Stderr = "执行超时";
}
else
{
process.WaitForExit();
result.ExitCode = process.ExitCode;
result.Stdout = stdout.ToString().TrimEnd();
result.Stderr = stderr.ToString().TrimEnd();
}
}
catch (Exception ex)
{
result.ExitCode = -1;
result.Stderr = $"执行异常: {ex.Message}";
}
sw.Stop();
result.Elapsed = sw.ElapsedMilliseconds;
AuditLogger?.Write(result);
return result;
}
finally { _semaphore.Release(); }
}
/// <summary>根据当前平台确定 Shell 和参数。Linux/macOS 用 /bin/bash;Windows 用 cmd.exe /c</summary>
private static (String shell, String args) GetPlatformShell(String command, String arguments, Boolean needSudo)
{
if (OperatingSystem.IsLinux())
{
if (needSudo)
return ("/usr/bin/sudo", $"/bin/bash -c \"{command} {EscapeArg(arguments)}\"");
return ("/bin/bash", $"-c \"{command} {EscapeArg(arguments)}\"");
}
if (OperatingSystem.IsMacOS())
{
// macOS 也用 bash,支持 sudo
if (needSudo)
return ("/usr/bin/sudo", $"/bin/bash -c \"{command} {EscapeArg(arguments)}\"");
return ("/bin/bash", $"-c \"{command} {EscapeArg(arguments)}\"");
}
// Windows 及未知平台:用 cmd.exe /c
return ("cmd.exe", $"/c \"{command} {EscapeArg(arguments)}\"");
}
private static String BuildArguments(String[] args) => String.Join(" ", args.Select(EscapeArg));
private static String EscapeArg(String arg)
{
if (String.IsNullOrEmpty(arg)) return "\"\"";
return arg.Contains(' ') || arg.Contains('"') ? $"\"{arg.Replace("\"", "\\\"")}\"" : arg;
}
}
/// <summary>Shell 命令执行结果</summary>
public class ShellResult
{
/// <summary>执行的命令</summary>
public String Command { get; set; }
/// <summary>命令参数</summary>
public String Arguments { get; set; }
/// <summary>退出码</summary>
public Int32 ExitCode { get; set; }
/// <summary>标准输出</summary>
public String Stdout { get; set; }
/// <summary>标准错误</summary>
public String Stderr { get; set; }
/// <summary>执行耗时(毫秒)</summary>
public Int64 Elapsed { get; set; }
/// <summary>是否成功(ExitCode == 0)</summary>
public Boolean Success => ExitCode == 0;
/// <summary>创建失败结果</summary>
/// <param name="error">错误信息</param>
/// <returns>失败结果</returns>
public static ShellResult Fail(String error) => new() { ExitCode = -1, Stderr = error };
}
|