refactor: 枚举移入Models目录,命名空间更新为Rainbow.Entity.Models
大石头 authored at 2026-07-02 12:54:58
6.76 KiB
RainbowBridge
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 };
}