Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 132 additions & 4 deletions KitX.Loader.CSharp/ArgsParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using CommandLine;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;

namespace KitX.Loader.CSharp;

Expand All @@ -9,17 +11,141 @@ public static void Parse(string[] args)
Parser.Default.ParseArguments<Options>(args)
.WithParsed(async option =>
{
await ParseOptionAsync(option);
});
}

public static Task ParseAsync(string[] args)
{
var tcs = new TaskCompletionSource<bool>();

Parser.Default.ParseArguments<Options>(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();
}

/// <summary>
/// 仅执行插件加载和连接,不阻塞等待退出信号。
/// 适用于有自身消息循环的宿主(如 WPF),由宿主管理进程生命周期。
/// </summary>
/// <param name="args">命令行参数</param>
/// <returns>用于优雅关闭的 CommunicationManager 实例</returns>
public static async Task<CommunicationManager?> LoadWithoutBlockingAsync(string[] args)
{
CommunicationManager? communicationManager = null;

Parser.Default.ParseArguments<Options>(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))
Expand All @@ -28,5 +154,7 @@ public static void Parse(string[] args)
if (communicationManager is not null)
communicationManager.OnReceiveMessage = x => pluginManager.ReceiveMessage(x);
});

return communicationManager;
}
}
46 changes: 42 additions & 4 deletions KitX.Loader.CSharp/CommunicationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,80 @@ public CommunicationManager()

public async Task<CommunicationManager> 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;
}

Expand Down
2 changes: 1 addition & 1 deletion KitX.Loader.CSharp/KitX.Loader.CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
20 changes: 18 additions & 2 deletions KitX.Loader.CSharp/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)
{
Expand All @@ -116,6 +131,7 @@ public void ReceiveMessage(string message)

case CommandRequestInfo.ReceiveCommand:

// 执行命令 - 插件通过 sendCommandAction 发送响应
controller?.Execute(command);

break;
Expand Down
18 changes: 17 additions & 1 deletion KitX.Loader.CSharp/Program.cs
Original file line number Diff line number Diff line change
@@ -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();
6 changes: 3 additions & 3 deletions KitX.Loader.WPF.Core/App.xaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<Application x:Class="KitX.Loader.WPF.Core.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:KitX.Loader.WPF.Core"
Startup="Application_Startup">
Startup="Application_Startup"
Exit="Application_Exit">
<Application.Resources>

</Application.Resources>
</Application>
25 changes: 22 additions & 3 deletions KitX.Loader.WPF.Core/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using System;
using System;
using System.Windows;
using KitX.Loader.CSharp;

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)
{
Expand All @@ -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}");
}
}
}
}
2 changes: 1 addition & 1 deletion KitX.Loader.WPF.Core/KitX.Loader.WPF.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion KitX.Loader.Winform.Core/KitX.Loader.Winform.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
Loading