diff --git a/KitX.Loader.CSharp/ArgsParser.cs b/KitX.Loader.CSharp/ArgsParser.cs index cbb2afc..f9aa35c 100644 --- a/KitX.Loader.CSharp/ArgsParser.cs +++ b/KitX.Loader.CSharp/ArgsParser.cs @@ -1,4 +1,6 @@ -using CommandLine; +using System.Threading; +using System.Threading.Tasks; +using CommandLine; namespace KitX.Loader.CSharp; @@ -9,24 +11,127 @@ public static void Parse(string[] args) Parser.Default.ParseArguments(args) .WithParsed(async option => { - if (option.PluginPath is null) - return; + await ParseOptionAsync(option); + }); + } + + public static Task ParseAsync(string[] args) + { + var tcs = new TaskCompletionSource(); + + Parser.Default.ParseArguments(args) + .WithParsed(async option => + { + try + { + await ParseOptionAsync(option); + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }) + .WithNotParsed(errors => + { + tcs.SetResult(false); + }); + + return tcs.Task; + } + + private static async Task ParseOptionAsync(Options option) + { + if (option.PluginPath is null) + return; - if (option.WorkingDirectory is not null) - Directory.SetCurrentDirectory(option.WorkingDirectory); + var communicationManager = await PrepareAsync(option); - var communicationManager = new CommunicationManager(); + // === 进程保活:等待退出信号 === + var exitSignal = new ManualResetEventSlim(false); - if (option.ConnectUrl is not null) - communicationManager = await communicationManager.Connect(option.ConnectUrl); - else communicationManager = null; + // 响应 Ctrl+C / Ctrl+Break + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + exitSignal.Set(); + }; - var pluginManager = new PluginManager() - .OnSendMessage(x => communicationManager?.SendMessageAsync(x)) - .LoadPlugin(option.PluginPath); + // 响应进程终止事件 + AppDomain.CurrentDomain.ProcessExit += (_, _) => exitSignal.Set(); - if (communicationManager is not null) - communicationManager.OnReceiveMessage = x => pluginManager.ReceiveMessage(x); + // 等待退出信号 + exitSignal.Wait(); + + // 优雅退出:关闭 WebSocket 连接 + if (communicationManager is not null) + await communicationManager.Close(); + } + + /// + /// 仅执行插件加载和连接,不阻塞等待退出信号。 + /// 适用于有自身消息循环的宿主(如 WPF),由宿主管理进程生命周期。 + /// + /// 命令行参数 + /// 用于优雅关闭的 CommunicationManager 实例;无连接或解析失败时为 null + public static async Task LoadWithoutBlockingAsync(string[] args) + { + // 通过 TCS 等待 WithParsed 的异步回调真正完成,避免在插件加载前就返回(竞态)。 + var tcs = new TaskCompletionSource(); + + Parser.Default.ParseArguments(args) + .WithParsed(async option => + { + try + { + tcs.SetResult(await PrepareAsync(option)); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }) + .WithNotParsed(errors => + { + tcs.SetResult(null); }); + + return await tcs.Task; + } + + /// + /// 公共加载流程:切换工作目录、建立连接、加载插件并接线消息回调。 + /// 连接失败时回退为无连接模式,仅加载插件。 + /// + private static async Task PrepareAsync(Options option) + { + if (option.PluginPath is null) + return null; + + if (option.WorkingDirectory is not null) + Directory.SetCurrentDirectory(option.WorkingDirectory); + + CommunicationManager? communicationManager = null; + + if (option.ConnectUrl is not null) + { + try + { + communicationManager = await new CommunicationManager().Connect(option.ConnectUrl); + } + catch + { + // 连接失败:回退为无连接模式,插件仍可本地运行 + } + } + + var pluginManager = new PluginManager() + .OnSendMessage(x => communicationManager?.SendMessageAsync(x)) + .LoadPlugin(option.PluginPath); + + if (communicationManager is not null) + communicationManager.OnReceiveMessage = x => pluginManager.ReceiveMessage(x); + + return communicationManager; } } diff --git a/KitX.Loader.CSharp/CommunicationManager.cs b/KitX.Loader.CSharp/CommunicationManager.cs index f1b8c99..5f1fcb7 100644 --- a/KitX.Loader.CSharp/CommunicationManager.cs +++ b/KitX.Loader.CSharp/CommunicationManager.cs @@ -22,13 +22,22 @@ public async Task Connect(string? url) { ArgumentNullException.ThrowIfNull(url, nameof(url)); - ArgumentNullException.ThrowIfNull(Client, nameof(Client)); + if (Client is null) + throw new InvalidOperationException("ClientWebSocket is not initialized"); - await Client.ConnectAsync(new Uri(url), CancellationToken.None); + try + { + await Client.ConnectAsync(new Uri(url), CancellationToken.None); + } + catch + { + throw; + } var waiting = true; + var timeout = DateTime.Now.AddSeconds(30); // 30 second timeout - while (waiting) + while (waiting && DateTime.Now < timeout) { switch (Client.State) { @@ -36,9 +45,10 @@ public async Task Connect(string? url) waiting = false; break; case WebSocketState.Connecting: + await Task.Delay(10); // Wait a bit before checking again break; case WebSocketState.Open: - new Thread(async () => await ReceiveAsync()).Start(); + _ = ReceiveAsync(); // Start receiving in background waiting = false; break; case WebSocketState.CloseSent: @@ -56,6 +66,9 @@ public async Task Connect(string? url) } } + if (Client.State != WebSocketState.Open) + throw new InvalidOperationException($"WebSocket failed to connect, state: {Client.State}"); + return this; } diff --git a/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj b/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj index 1c5469f..ab54123 100644 --- a/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj +++ b/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj @@ -2,14 +2,13 @@ Exe - net8.0 + net10.0 enable enable - diff --git a/KitX.Loader.CSharp/PluginManager.cs b/KitX.Loader.CSharp/PluginManager.cs index 56e0205..f0a5ac9 100644 --- a/KitX.Loader.CSharp/PluginManager.cs +++ b/KitX.Loader.CSharp/PluginManager.cs @@ -80,7 +80,14 @@ private void InitPlugin(IIdentityInterface plugin) JsonSerializer.Serialize(pluginInfo, serializerOptions) ); - Connector.Request().RegisterPlugin(pluginInfoToSend, pluginInfoToSend.Length).Send(); + try + { + Connector.Request().RegisterPlugin(pluginInfoToSend, pluginInfoToSend.Length).Send(); + } + catch + { + // 注册失败不阻断插件本地运行,保留原行为 + } controller = plugin.GetController(); @@ -91,7 +98,10 @@ private void InitPlugin(IIdentityInterface plugin) controller.Start(); } - private void SendMessage(string message) => sendMessageAction?.Invoke(message); + private void SendMessage(string message) + { + sendMessageAction?.Invoke(message); + } public void ReceiveMessage(string message) { @@ -116,6 +126,7 @@ public void ReceiveMessage(string message) case CommandRequestInfo.ReceiveCommand: + // 执行命令 - 插件通过 sendCommandAction 发送响应 controller?.Execute(command); break; diff --git a/KitX.Loader.CSharp/Program.cs b/KitX.Loader.CSharp/Program.cs index 6a6dd25..c01f8af 100644 --- a/KitX.Loader.CSharp/Program.cs +++ b/KitX.Loader.CSharp/Program.cs @@ -1,3 +1,19 @@ using KitX.Loader.CSharp; +using System.Threading.Tasks; -ArgsParser.Parse(args); +// 全局异常处理 +AppDomain.CurrentDomain.UnhandledException += (_, e) => +{ + Console.WriteLine($"[FATAL] Unhandled exception: {e.ExceptionObject}"); + Console.WriteLine($"[FATAL] Stack trace: {(e.ExceptionObject as Exception)?.StackTrace}"); + Environment.Exit(1); +}; + +TaskScheduler.UnobservedTaskException += (_, e) => +{ + Console.WriteLine($"[ERROR] Unobserved task exception: {e.Exception.Message}"); + e.SetObserved(); +}; + +// 使用同步方式阻塞等待异步解析完成 +ArgsParser.ParseAsync(args).GetAwaiter().GetResult(); diff --git a/KitX.Loader.WPF.Core/App.xaml b/KitX.Loader.WPF.Core/App.xaml index 3b526de..6dfe557 100644 --- a/KitX.Loader.WPF.Core/App.xaml +++ b/KitX.Loader.WPF.Core/App.xaml @@ -1,9 +1,9 @@  + Startup="Application_Startup" + Exit="Application_Exit"> - + diff --git a/KitX.Loader.WPF.Core/App.xaml.cs b/KitX.Loader.WPF.Core/App.xaml.cs index 93fb183..d797d3c 100644 --- a/KitX.Loader.WPF.Core/App.xaml.cs +++ b/KitX.Loader.WPF.Core/App.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Windows; using KitX.Loader.CSharp; @@ -6,11 +6,14 @@ namespace KitX.Loader.WPF.Core; public partial class App : Application { - private void Application_Startup(object sender, StartupEventArgs e) + private CommunicationManager? _communicationManager; + + private async void Application_Startup(object sender, StartupEventArgs e) { try { - ArgsParser.Parse(e.Args); + // 使用非阻塞方式加载插件,WPF 自身的消息循环管理进程生命周期 + _communicationManager = await ArgsParser.LoadWithoutBlockingAsync(e.Args); } catch (Exception o) { @@ -26,4 +29,20 @@ private void Application_Startup(object sender, StartupEventArgs e) Environment.Exit(1); } } + + private async void Application_Exit(object sender, ExitEventArgs e) + { + // 优雅退出:关闭 WebSocket 连接 + if (_communicationManager is not null) + { + try + { + await _communicationManager.Close(); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + } } diff --git a/KitX.Loader.WPF.Core/KitX.Loader.WPF.Core.csproj b/KitX.Loader.WPF.Core/KitX.Loader.WPF.Core.csproj index 09cfc24..e1c661f 100644 --- a/KitX.Loader.WPF.Core/KitX.Loader.WPF.Core.csproj +++ b/KitX.Loader.WPF.Core/KitX.Loader.WPF.Core.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net10.0-windows enable true diff --git a/KitX.Loader.Winform.Core/KitX.Loader.Winform.Core.csproj b/KitX.Loader.Winform.Core/KitX.Loader.Winform.Core.csproj index 5b397b9..5f89d89 100644 --- a/KitX.Loader.Winform.Core/KitX.Loader.Winform.Core.csproj +++ b/KitX.Loader.Winform.Core/KitX.Loader.Winform.Core.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net10.0-windows enable true enable