diff --git a/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs b/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs index 5cc98bd2..2141cd5c 100644 --- a/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs +++ b/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs @@ -1,6 +1,6 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using BetterGenshinImpact.Core.Config; +using CommunityToolkit.Mvvm.ComponentModel; using System; -using BetterGenshinImpact.Core.Config; namespace BetterGenshinImpact.Core.Script.Group; @@ -9,4 +9,10 @@ public partial class ScriptGroupConfig : ObservableObject { [ObservableProperty] private PathingPartyConfig _pathingConfig = new(); + + /// + /// Shell 执行配置 + /// + [ObservableProperty] + private ShellConfig _shellConfig = new(); } diff --git a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs index 727ba343..ba3efe70 100644 --- a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs +++ b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs @@ -4,6 +4,7 @@ using BetterGenshinImpact.Core.Script.Project; using BetterGenshinImpact.GameTask; using BetterGenshinImpact.GameTask.AutoPathing; using BetterGenshinImpact.GameTask.AutoPathing.Model; +using BetterGenshinImpact.GameTask.Shell; using BetterGenshinImpact.ViewModel.Pages; using CommunityToolkit.Mvvm.ComponentModel; using System; @@ -12,7 +13,6 @@ using System.Dynamic; using System.IO; using System.Text.Json.Serialization; using System.Threading.Tasks; -using BetterGenshinImpact.GameTask.Shell; namespace BetterGenshinImpact.Core.Script.Group; @@ -63,7 +63,6 @@ public partial class ScriptGroupProject : ObservableObject [JsonIgnore] public ScriptProject? Project { get; set; } - public ExpandoObject? JsScriptSettingsObject { get; set; } /// @@ -71,6 +70,7 @@ public partial class ScriptGroupProject : ObservableObject /// [JsonIgnore] public ScriptGroup? GroupInfo { get; set; } + /// /// 下一个从此执行标志 /// @@ -132,6 +132,7 @@ public partial class ScriptGroupProject : ObservableObject { throw new Exception("FolderName 为空"); } + Project = new ScriptProject(FolderName); } @@ -143,15 +144,16 @@ public partial class ScriptGroupProject : ObservableObject { throw new Exception("JS脚本未初始化"); } + JsScriptSettingsObject ??= new ExpandoObject(); - + var pathingPartyConfig = GroupInfo?.Config.PathingConfig; - if (!(pathingPartyConfig is {Enabled:true,JsScriptUseEnabled:true})) + if (!(pathingPartyConfig is { Enabled: true, JsScriptUseEnabled: true })) { pathingPartyConfig = null; } - await Project.ExecuteAsync(JsScriptSettingsObject,pathingPartyConfig); + await Project.ExecuteAsync(JsScriptSettingsObject, pathingPartyConfig); } else if (Type == "KeyMouse") { @@ -173,8 +175,9 @@ public partial class ScriptGroupProject : ObservableObject } else if (Type == "Shell") { - var task = ShellExecutor.BuildFromShellName(Name); - await task.Execute(); + var shellConfig = GroupInfo?.Config.ShellConfig ?? new ShellConfig(); + var task = new ShellTask(ShellTaskParam.BuildFromConfig(Name, shellConfig)); + await task.Start(CancellationContext.Instance.Cts.Token); } } diff --git a/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs b/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs new file mode 100644 index 00000000..b7f413af --- /dev/null +++ b/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs @@ -0,0 +1,23 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System; + +namespace BetterGenshinImpact.Core.Config; + +/// +/// shell执行配置 +/// +[Serializable] +public partial class ShellConfig : ObservableObject +{ + // 禁用Shell任务 + [ObservableProperty] private bool _disable; + + // 最长等待命令返回的时间,单位秒,<=0不等待,直接返回。 + [ObservableProperty] private int _timeout = 60; + + // 隐藏命令执行窗口 + [ObservableProperty] private bool _noWindow = true; + + // 向log打印命令执行输出 + [ObservableProperty] private bool _output = true; +} diff --git a/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs b/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs deleted file mode 100644 index d66868da..00000000 --- a/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Diagnostics; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using BetterGenshinImpact.GameTask.Common; -using Microsoft.Extensions.Logging; - -namespace BetterGenshinImpact.GameTask.Shell; - -[Serializable] -public class ShellExecutor -{ - private string command = string.Empty; - private int maxWaitSeconds = 60; - private bool noWindow = true; - private bool output = true; - - public static readonly JsonSerializerOptions JsonOptions = new() - { - NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - - public async Task Execute(CancellationToken ct = default) - { - if (string.IsNullOrEmpty(command)) - { - TaskControl.Logger.LogWarning("无法执行Shell: Shell为空"); - return; - } - - var cmd = new Process(); - cmd.StartInfo.FileName = "cmd.exe"; - cmd.StartInfo.Arguments = "/k @echo off"; - cmd.StartInfo.RedirectStandardInput = true; - cmd.StartInfo.RedirectStandardOutput = true; - cmd.StartInfo.CreateNoWindow = noWindow; - cmd.StartInfo.UseShellExecute = false; - if (ct.IsCancellationRequested) - { - TaskControl.Logger.LogError("shell {Shell} 被取消", command); - } - - TaskControl.Logger.LogInformation("执行shell:{Shell},超时时间为 {Wait} 秒", command, maxWaitSeconds); - var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(maxWaitSeconds)); - var mixedToken = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutSignal.Token).Token; - - cmd.Start(); - var outputShell = ""; - var outputText = ""; - var cmdCanceled = false; - try - { - await cmd.StandardInput.WriteLineAsync(command.AsMemory(), mixedToken); - await cmd.StandardInput.FlushAsync(mixedToken); - cmd.StandardInput.Close(); - await cmd.WaitForExitAsync(mixedToken); - if (output) - { - outputShell = await cmd.StandardOutput.ReadLineAsync(mixedToken) ?? ""; - outputText = await cmd.StandardOutput.ReadToEndAsync(mixedToken); - } - } - catch (OperationCanceledException) - { - cmdCanceled = true; - } - - if (!cmd.HasExited || cmdCanceled) - { - cmd.Kill(); - if (ct.IsCancellationRequested) - { - TaskControl.Logger.LogError("shell {Shell} 被取消", command); - } - else if (timeoutSignal.IsCancellationRequested) - { - TaskControl.Logger.LogWarning("shell {Shell} 超时", command); - } - else - { - TaskControl.Logger.LogWarning("shell {Shell} 出现异常输出,可能未能成功执行。", command); - } - } - - if (output) - { - TaskControl.Logger.LogInformation("shell {End} 运行结束,输出:{Output}", outputShell, outputText); - } - else - { - TaskControl.Logger.LogInformation("shell {End} 运行结束", command); - } - - SystemControl.ActivateWindow(); - } - - public static ShellExecutor BuildFromShellName(string name) - { - var obj = new ShellExecutor - { - command = name - }; - return obj; - } - - public static ShellExecutor BuildFromJson(string json) - { - // 留给以后玩 - var task = JsonSerializer.Deserialize(json, JsonOptions) ?? - throw new Exception("Failed to deserialize ShellExecutorTask"); - return task; - } -} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Shell/ShellTask.cs b/BetterGenshinImpact/GameTask/Shell/ShellTask.cs new file mode 100644 index 00000000..66fdd38b --- /dev/null +++ b/BetterGenshinImpact/GameTask/Shell/ShellTask.cs @@ -0,0 +1,146 @@ +using BetterGenshinImpact.GameTask.Common; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.GameTask.Shell; + +public class ShellTask(ShellTaskParam param) : ISoloTask +{ + public string Name => "Shell"; + + public Task Start(CancellationToken ct = default) + { + return Execute(ct); + } + + private async Task Execute(CancellationToken ct) + { + if (param.Disable) + { + TaskControl.Logger.LogWarning("无法执行Shell: Shell任务被禁用"); + return; + } + + if (string.IsNullOrEmpty(param.Command)) + { + TaskControl.Logger.LogWarning("无法执行Shell: Shell为空"); + return; + } + + if (ct.IsCancellationRequested) + { + TaskControl.Logger.LogError("shell {Shell} 被取消", param.Command); + } + + TaskControl.Logger.LogInformation("执行shell:{Shell},超时时间为 {Wait} 秒", param.Command, param.TimeoutSeconds); + + var mixedToken = ct; + var waitForExit = true; + CancellationTokenSource? timeoutSignal = null; + if (param.TimeoutSeconds > 0) + { + timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(param.TimeoutSeconds)); + // 超时取消或任务被取消 + mixedToken = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutSignal.Token).Token; + } + else + { + // timeout小于0不等待,不获取输出,仅仅启动shell即返回 + waitForExit = false; + } + + ShellExecutionRecord result; + try + { + result = await StartAndInject(param, waitForExit, mixedToken); + } + catch (OperationCanceledException) + { + if (timeoutSignal is { IsCancellationRequested: true }) + { + TaskControl.Logger.LogError("shell {Shell} 执行超时", param.Command); + } + + TaskControl.Logger.LogError("shell {Shell} 被取消", param.Command); + return; + } + + if (result.End) + { + if (param.Output && result.HasOutput) + { + TaskControl.Logger.LogInformation("shell {End} 运行结束,输出:{Output}", result.Shell, result.Output); + return; + } + + TaskControl.Logger.LogInformation("shell {End} 运行结束", param.Command); + } + + SystemControl.ActivateWindow(); + } + + /// + /// 启动cmd并注入要执行的命令 + /// + /// Task参数 + /// 是否等待到执行结束 + /// CancellationToken + private static async Task StartAndInject(ShellTaskParam param, bool waitForExit, + CancellationToken ct) + { + using var cmd = new Process(); + cmd.StartInfo = BuildStartInfo(param); + cmd.Start(); + await cmd.StandardInput.WriteLineAsync(param.Command.AsMemory(), ct); + await cmd.StandardInput.FlushAsync(ct); + cmd.StandardInput.Close(); + if (!waitForExit) + { + return new ShellExecutionRecord(false, "", ""); + } + + var outputShell = ""; + var outputText = ""; + await cmd.WaitForExitAsync(ct); + if (param.Output) + { + outputShell = await cmd.StandardOutput.ReadLineAsync(ct) ?? ""; + outputText = await cmd.StandardOutput.ReadToEndAsync(ct); + } + + if (cmd.HasExited) + { + return new ShellExecutionRecord(true, outputShell, outputText); + } + + cmd.Kill(); + return new ShellExecutionRecord(false, outputShell, outputText); + } + + private static ProcessStartInfo BuildStartInfo(ShellTaskParam param) + { + return new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/k @echo off", + RedirectStandardInput = true, + RedirectStandardOutput = true, + CreateNoWindow = param.NoWindow, + UseShellExecute = false + }; + } + + /// + /// Shell执行记录 + /// + /// 是否是结束后的记录 + /// 执行输出的Shell + /// Shell输出的内容 + private record ShellExecutionRecord(bool End, string Shell, string Output) + { + public bool HasOutput => !string.IsNullOrEmpty(Output) || !string.IsNullOrEmpty(Shell); + } +} diff --git a/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs b/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs new file mode 100644 index 00000000..1fbd129f --- /dev/null +++ b/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs @@ -0,0 +1,27 @@ +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.GameTask.Model; + +namespace BetterGenshinImpact.GameTask.Shell; + +public class ShellTaskParam : BaseTaskParam +{ + private ShellTaskParam(string command, int configTimeoutSeconds, bool configNoWindow, bool configOutput, bool configDisable) + { + Command = command; + TimeoutSeconds = configTimeoutSeconds; + NoWindow = configNoWindow; + Output = configOutput; + Disable = configDisable; + } + + public readonly bool Disable; + public readonly string Command; + public readonly int TimeoutSeconds; + public readonly bool NoWindow; + public readonly bool Output; + + public static ShellTaskParam BuildFromConfig(string command, ShellConfig config) + { + return new ShellTaskParam(command, config.Timeout, config.NoWindow, config.Output, config.Disable); + } +}