diff --git a/CoreAgent.Domain/Interfaces/System/Command/ISystemCommandExecutor.cs b/CoreAgent.Domain/Interfaces/System/Command/ISystemCommandExecutor.cs index 13a3e5a..3ebe24e 100644 --- a/CoreAgent.Domain/Interfaces/System/Command/ISystemCommandExecutor.cs +++ b/CoreAgent.Domain/Interfaces/System/Command/ISystemCommandExecutor.cs @@ -11,16 +11,19 @@ public interface ISystemCommandExecutor /// 执行命令并实时输出结果 /// /// 要执行的命令 - /// 输出处理器 + /// 标准输出处理器 + /// 标准错误处理器 /// 取消令牌源 + /// 超时时间(毫秒),-1表示不设置超时 /// 执行任务 - Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, CancellationTokenSource cancellationTokenSource); + Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, Action errorHandler, CancellationTokenSource cancellationTokenSource, int timeout = -1); /// /// 执行命令并返回结果 /// /// 要执行的命令 /// 取消令牌源 + /// 超时时间(毫秒),-1表示不设置超时 /// 命令执行结果 - Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource); + Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource, int timeout = -1); } \ No newline at end of file diff --git a/CoreAgent.Infrastructure/Command/Base/BaseCommandExecutor.cs b/CoreAgent.Infrastructure/Command/Base/BaseCommandExecutor.cs index f96b9e0..8175f82 100644 --- a/CoreAgent.Infrastructure/Command/Base/BaseCommandExecutor.cs +++ b/CoreAgent.Infrastructure/Command/Base/BaseCommandExecutor.cs @@ -17,23 +17,55 @@ public abstract class BaseCommandExecutor : ISystemCommandExecutor protected BaseCommandExecutor(ILogger logger) { - _logger = logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public abstract Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, CancellationTokenSource cancellationTokenSource); + /// + /// 执行命令并实时输出结果 + /// + public abstract Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, Action errorHandler, CancellationTokenSource cancellationTokenSource, int timeout = -1); - public abstract Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource); + /// + /// 执行命令并返回结果 + /// + public abstract Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource, int timeout = -1); - protected async Task ExecuteCommandInternalAsync(string command, string arguments, CancellationTokenSource cancellationTokenSource) + /// + /// 获取命令参数,由子类实现具体的命令参数格式化逻辑 + /// + protected abstract string GetCommandArguments(string command); + + /// + /// 执行命令的通用逻辑 + /// + protected virtual async Task ExecuteCommandInternalAsync(string command, string arguments, CancellationTokenSource cancellationTokenSource, int timeout = -1) { + if (string.IsNullOrWhiteSpace(command)) + { + throw new ArgumentException("命令不能为空", nameof(command)); + } + + if (cancellationTokenSource == null) + { + throw new ArgumentNullException(nameof(cancellationTokenSource)); + } + var startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); try { _logger.LogDebug("开始执行命令: {Command}", command); + var cmd = Cli.Wrap(DefaultShell) .WithArguments(arguments) .WithValidation(CommandResultValidation.None); - var result = await cmd.ExecuteBufferedAsync(cancellationTokenSource.Token); + + // 如果设置了超时,创建一个新的 CancellationTokenSource + using var timeoutCts = timeout > 0 ? new CancellationTokenSource(timeout) : null; + using var linkedCts = timeout > 0 + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, timeoutCts!.Token) + : cancellationTokenSource; + + var result = await cmd.ExecuteBufferedAsync(linkedCts.Token); var executionTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime; if (result.IsSuccess) @@ -47,10 +79,16 @@ public abstract class BaseCommandExecutor : ISystemCommandExecutor return CommandExecutionResult.Failure(result.StandardError, executionTime); } } + catch (OperationCanceledException ex) + { + var executionTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime; + _logger.LogError(ex, "命令执行超时: {Command}", command); + return CommandExecutionResult.Failure("命令执行超时", executionTime); + } catch (Exception ex) { var executionTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime; - _logger.LogError(ex, "命令执行失败: {Command}", command); + _logger.LogError(ex, "命令执行失败: {Command}, 错误: {ErrorMessage}", command, ex.Message); return CommandExecutionResult.Failure(ex.Message, executionTime); } } diff --git a/CoreAgent.Infrastructure/Command/Executors/LinuxCommandExecutor.cs b/CoreAgent.Infrastructure/Command/Executors/LinuxCommandExecutor.cs index bac0078..70f6c77 100644 --- a/CoreAgent.Infrastructure/Command/Executors/LinuxCommandExecutor.cs +++ b/CoreAgent.Infrastructure/Command/Executors/LinuxCommandExecutor.cs @@ -4,6 +4,7 @@ using CoreAgent.Domain.Interfaces.System.Command; using CoreAgent.Domain.Models.System; using CoreAgent.Infrastructure.Command.Base; using Microsoft.Extensions.Logging; +using System.IO; namespace CoreAgent.Infrastructure.Command.Executors; @@ -12,35 +13,92 @@ namespace CoreAgent.Infrastructure.Command.Executors; /// public class LinuxCommandExecutor : BaseCommandExecutor { - private const string ShellPath = "/bin/bash"; + private const string BashPath = "/bin/bash"; + private const string ShPath = "/bin/sh"; + private readonly string _shellPath; public LinuxCommandExecutor(ILogger logger) : base(logger) { + // 优先使用 bash,如果不存在则使用 sh + _shellPath = File.Exists(BashPath) ? BashPath : ShPath; + _logger.LogInformation("使用Shell: {ShellPath}", _shellPath); } - protected override string DefaultShell => ShellPath; + protected override string DefaultShell => _shellPath; - public override async Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, CancellationTokenSource cancellationTokenSource) + public override async Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, Action errorHandler, CancellationTokenSource cancellationTokenSource, int timeout = -1) { + if (string.IsNullOrWhiteSpace(command)) + { + throw new ArgumentException("命令不能为空", nameof(command)); + } + + if (outputHandler == null) + { + throw new ArgumentNullException(nameof(outputHandler)); + } + + if (errorHandler == null) + { + throw new ArgumentNullException(nameof(errorHandler)); + } + + if (cancellationTokenSource == null) + { + throw new ArgumentNullException(nameof(cancellationTokenSource)); + } + try { _logger.LogDebug("开始执行命令: {Command}", command); - var cmd = Cli.Wrap(DefaultShell) - .WithArguments($"-c \"{command}\"") + + var cmd = Cli.Wrap(_shellPath) + .WithArguments(GetCommandArguments(command)) .WithStandardOutputPipe(PipeTarget.ToDelegate(outputHandler)) - .WithStandardErrorPipe(PipeTarget.ToDelegate(outputHandler)) + .WithStandardErrorPipe(PipeTarget.ToDelegate(errorHandler)) .WithValidation(CommandResultValidation.None); - var result = await cmd.ExecuteBufferedAsync(cancellationTokenSource.Token); + + // 如果设置了超时,创建一个新的 CancellationTokenSource + using var timeoutCts = timeout > 0 ? new CancellationTokenSource(timeout) : null; + using var linkedCts = timeout > 0 + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, timeoutCts!.Token) + : cancellationTokenSource; + + var result = await cmd.ExecuteBufferedAsync(linkedCts.Token); _logger.LogInformation("命令执行结果: {IsSuccess}", result.IsSuccess); } + catch (OperationCanceledException ex) + { + _logger.LogError(ex, "命令执行超时: {Command}", command); + throw; + } catch (Exception ex) { - _logger.LogError(ex, "命令执行失败: {Command}", command); + _logger.LogError(ex, "命令执行失败: {Command}, 错误: {ErrorMessage}", command, ex.Message); + throw; } } - public override async Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource) + public override async Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource, int timeout = -1) { - return await ExecuteCommandInternalAsync(command, $"-c \"{command}\"", cancellationTokenSource); + return await ExecuteCommandInternalAsync(command, GetCommandArguments(command), cancellationTokenSource, timeout); + } + + protected override string GetCommandArguments(string command) + { + if (string.IsNullOrWhiteSpace(command)) + { + throw new ArgumentException("命令不能为空", nameof(command)); + } + + // 转义命令中的特殊字符 + var escapedCommand = command + .Replace("\"", "\\\"") + .Replace("'", "\\'") + .Replace("$", "\\$") + .Replace("`", "\\`") + .Replace("\\", "\\\\"); + + return $"-c '{escapedCommand}'"; } } \ No newline at end of file diff --git a/CoreAgent.Infrastructure/Command/Executors/WindowsCommandExecutor.cs b/CoreAgent.Infrastructure/Command/Executors/WindowsCommandExecutor.cs index 35c5687..3fd86e8 100644 --- a/CoreAgent.Infrastructure/Command/Executors/WindowsCommandExecutor.cs +++ b/CoreAgent.Infrastructure/Command/Executors/WindowsCommandExecutor.cs @@ -21,27 +21,81 @@ public class WindowsCommandExecutor : BaseCommandExecutor protected override string DefaultShell => ShellPath; - public override async Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, CancellationTokenSource cancellationTokenSource) + public override async Task ExecuteCommandWithOutputAsync(string command, Action outputHandler, Action errorHandler, CancellationTokenSource cancellationTokenSource, int timeout = -1) { + if (string.IsNullOrWhiteSpace(command)) + { + throw new ArgumentException("命令不能为空", nameof(command)); + } + + if (outputHandler == null) + { + throw new ArgumentNullException(nameof(outputHandler)); + } + + if (errorHandler == null) + { + throw new ArgumentNullException(nameof(errorHandler)); + } + + if (cancellationTokenSource == null) + { + throw new ArgumentNullException(nameof(cancellationTokenSource)); + } + try { _logger.LogDebug("开始执行命令: {Command}", command); + var cmd = Cli.Wrap(DefaultShell) - .WithArguments($"/c {command}") + .WithArguments(GetCommandArguments(command)) .WithStandardOutputPipe(PipeTarget.ToDelegate(outputHandler)) - .WithStandardErrorPipe(PipeTarget.ToDelegate(outputHandler)) + .WithStandardErrorPipe(PipeTarget.ToDelegate(errorHandler)) .WithValidation(CommandResultValidation.None); - var result = await cmd.ExecuteBufferedAsync(cancellationTokenSource.Token); + + // 如果设置了超时,创建一个新的 CancellationTokenSource + using var timeoutCts = timeout > 0 ? new CancellationTokenSource(timeout) : null; + using var linkedCts = timeout > 0 + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationTokenSource.Token, timeoutCts!.Token) + : cancellationTokenSource; + + var result = await cmd.ExecuteBufferedAsync(linkedCts.Token); _logger.LogInformation("命令执行结果: {IsSuccess}", result.IsSuccess); } + catch (OperationCanceledException ex) + { + _logger.LogError(ex, "命令执行超时: {Command}", command); + throw; + } catch (Exception ex) { - _logger.LogError(ex, "命令执行失败: {Command}", command); + _logger.LogError(ex, "命令执行失败: {Command}, 错误: {ErrorMessage}", command, ex.Message); + throw; } } - public override async Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource) + public override async Task ExecuteCommandAsync(string command, CancellationTokenSource cancellationTokenSource, int timeout = -1) { - return await ExecuteCommandInternalAsync(command, $"/c {command}", cancellationTokenSource); + return await ExecuteCommandInternalAsync(command, GetCommandArguments(command), cancellationTokenSource, timeout); + } + + protected override string GetCommandArguments(string command) + { + if (string.IsNullOrWhiteSpace(command)) + { + throw new ArgumentException("命令不能为空", nameof(command)); + } + + // Windows CMD 命令转义 + var escapedCommand = command + .Replace("\"", "\\\"") + .Replace("^", "^^") + .Replace("&", "^&") + .Replace("|", "^|") + .Replace("<", "^<") + .Replace(">", "^>") + .Replace("%", "%%"); + + return $"/c \"{escapedCommand}\""; } } \ No newline at end of file