diff --git a/KitX.Loader.CSharp/ArgsParser.cs b/KitX.Loader.CSharp/ArgsParser.cs index cbb2afc..281fc22 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,17 +11,141 @@ public static void Parse(string[] args) Parser.Default.ParseArguments(args) .WithParsed(async option => { + 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) + { + Console.WriteLine($"[DEBUG] ArgsParser: PluginPath = {option.PluginPath}"); + Console.WriteLine($"[DEBUG] ArgsParser: ConnectUrl = {option.ConnectUrl}"); + Console.WriteLine($"[DEBUG] ArgsParser: WorkingDirectory = {option.WorkingDirectory}"); + + if (option.PluginPath is null) + return; + + if (option.WorkingDirectory is not null) + Directory.SetCurrentDirectory(option.WorkingDirectory); + + var communicationManager = new CommunicationManager(); + + if (option.ConnectUrl is not null) + { + Console.WriteLine($"[DEBUG] Connecting to {option.ConnectUrl}..."); + try + { + communicationManager = await communicationManager.Connect(option.ConnectUrl); + Console.WriteLine($"[DEBUG] Connected successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] Connection failed: {ex.Message}"); + Console.WriteLine($"[DEBUG] Stack trace: {ex.StackTrace}"); + } + } + else + { + Console.WriteLine("[DEBUG] No ConnectUrl provided, running without connection"); + communicationManager = null; + } + + var pluginManager = new PluginManager() + .OnSendMessage(x => communicationManager?.SendMessageAsync(x)) + .LoadPlugin(option.PluginPath); + + if (communicationManager is not null) + communicationManager.OnReceiveMessage = x => pluginManager.ReceiveMessage(x); + + // === 进程保活:等待退出信号 === + var exitSignal = new ManualResetEventSlim(false); + + // 响应 Ctrl+C / Ctrl+Break + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + exitSignal.Set(); + }; + + // 响应进程终止事件 + AppDomain.CurrentDomain.ProcessExit += (_, _) => exitSignal.Set(); + + // 等待退出信号 + exitSignal.Wait(); + + // 优雅退出:关闭 WebSocket 连接 + if (communicationManager is not null) + await communicationManager.Close(); + } + + /// + /// 仅执行插件加载和连接,不阻塞等待退出信号。 + /// 适用于有自身消息循环的宿主(如 WPF),由宿主管理进程生命周期。 + /// + /// 命令行参数 + /// 用于优雅关闭的 CommunicationManager 实例 + public static async Task LoadWithoutBlockingAsync(string[] args) + { + CommunicationManager? communicationManager = null; + + Parser.Default.ParseArguments(args) + .WithParsed(async option => + { + Console.WriteLine($"[DEBUG] ArgsParser: PluginPath = {option.PluginPath}"); + Console.WriteLine($"[DEBUG] ArgsParser: ConnectUrl = {option.ConnectUrl}"); + Console.WriteLine($"[DEBUG] ArgsParser: WorkingDirectory = {option.WorkingDirectory}"); + if (option.PluginPath is null) return; if (option.WorkingDirectory is not null) Directory.SetCurrentDirectory(option.WorkingDirectory); - var communicationManager = new CommunicationManager(); + communicationManager = new CommunicationManager(); if (option.ConnectUrl is not null) - communicationManager = await communicationManager.Connect(option.ConnectUrl); - else communicationManager = null; + { + Console.WriteLine($"[DEBUG] Connecting to {option.ConnectUrl}..."); + try + { + communicationManager = await communicationManager.Connect(option.ConnectUrl); + Console.WriteLine($"[DEBUG] Connected successfully!"); + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] Connection failed: {ex.Message}"); + communicationManager = null; + } + } + else + { + Console.WriteLine("[DEBUG] No ConnectUrl provided, running without connection"); + communicationManager = null; + } var pluginManager = new PluginManager() .OnSendMessage(x => communicationManager?.SendMessageAsync(x)) @@ -28,5 +154,7 @@ public static void Parse(string[] args) 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..66aeaea 100644 --- a/KitX.Loader.CSharp/CommunicationManager.cs +++ b/KitX.Loader.CSharp/CommunicationManager.cs @@ -20,42 +20,80 @@ public CommunicationManager() public async Task Connect(string? url) { + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Starting connection to {url}"); + ArgumentNullException.ThrowIfNull(url, nameof(url)); - ArgumentNullException.ThrowIfNull(Client, nameof(Client)); + if (Client is null) + { + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Client is NULL!"); + throw new InvalidOperationException("ClientWebSocket is not initialized"); + } - await Client.ConnectAsync(new Uri(url), CancellationToken.None); + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Client state before connect: {Client.State}"); + + try + { + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Calling ConnectAsync..."); + await Client.ConnectAsync(new Uri(url), CancellationToken.None); + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: ConnectAsync completed, state: {Client.State}"); + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: ConnectAsync failed: {ex.Message}"); + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Stack trace: {ex.StackTrace}"); + throw; + } var waiting = true; + var timeout = DateTime.Now.AddSeconds(30); // 30 second timeout - while (waiting) + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Waiting for connection open, initial state: {Client.State}"); + + while (waiting && DateTime.Now < timeout) { switch (Client.State) { case WebSocketState.None: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = None"); waiting = false; break; case WebSocketState.Connecting: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = Connecting..."); + await Task.Delay(10); // Wait a bit before checking again break; case WebSocketState.Open: - new Thread(async () => await ReceiveAsync()).Start(); + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = Open!"); + _ = ReceiveAsync(); // Start receiving in background waiting = false; break; case WebSocketState.CloseSent: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = CloseSent"); waiting = false; break; case WebSocketState.CloseReceived: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = CloseReceived"); waiting = false; break; case WebSocketState.Closed: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = Closed"); waiting = false; break; case WebSocketState.Aborted: + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: State = Aborted"); waiting = false; break; } } + if (Client.State != WebSocketState.Open) + { + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Failed to connect, final state: {Client.State}"); + throw new InvalidOperationException($"WebSocket failed to connect, state: {Client.State}"); + } + + Console.WriteLine($"[DEBUG] CommunicationManager.Connect: Connection established!"); + return this; } diff --git a/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj b/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj index 1c5469f..a14e0d5 100644 --- a/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj +++ b/KitX.Loader.CSharp/KitX.Loader.CSharp.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/KitX.Loader.CSharp/PluginManager.cs b/KitX.Loader.CSharp/PluginManager.cs index 56e0205..6f3bd20 100644 --- a/KitX.Loader.CSharp/PluginManager.cs +++ b/KitX.Loader.CSharp/PluginManager.cs @@ -76,11 +76,22 @@ private void InitPlugin(IIdentityInterface plugin) { pluginInfo = plugin.GetPluginInfo(); + Console.WriteLine($"[DEBUG] InitPlugin called, sendMessageAction is: {(sendMessageAction is null ? "NULL" : "SET")}"); + var pluginInfoToSend = Encoding.UTF8.GetBytes( JsonSerializer.Serialize(pluginInfo, serializerOptions) ); - Connector.Request().RegisterPlugin(pluginInfoToSend, pluginInfoToSend.Length).Send(); + Console.WriteLine($"[DEBUG] Sending RegisterPlugin request..."); + try + { + Connector.Request().RegisterPlugin(pluginInfoToSend, pluginInfoToSend.Length).Send(); + Console.WriteLine($"[DEBUG] RegisterPlugin sent successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] RegisterPlugin failed: {ex.Message}"); + } controller = plugin.GetController(); @@ -91,7 +102,11 @@ private void InitPlugin(IIdentityInterface plugin) controller.Start(); } - private void SendMessage(string message) => sendMessageAction?.Invoke(message); + private void SendMessage(string message) + { + Console.WriteLine($"[DEBUG] SendMessage called, action is: {(sendMessageAction is null ? "NULL" : "SET")}"); + sendMessageAction?.Invoke(message); + } public void ReceiveMessage(string message) { @@ -116,6 +131,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..b512344 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($"[DEBUG] Error closing communication: {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