Skip to content
Open
133 changes: 119 additions & 14 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,24 +11,127 @@ public static void Parse(string[] args)
Parser.Default.ParseArguments<Options>(args)
.WithParsed(async option =>
{
if (option.PluginPath is null)
return;
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)
{
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();
}

/// <summary>
/// 仅执行插件加载和连接,不阻塞等待退出信号。
/// 适用于有自身消息循环的宿主(如 WPF),由宿主管理进程生命周期。
/// </summary>
/// <param name="args">命令行参数</param>
/// <returns>用于优雅关闭的 CommunicationManager 实例;无连接或解析失败时为 null</returns>
public static async Task<CommunicationManager?> LoadWithoutBlockingAsync(string[] args)
{
// 通过 TCS 等待 WithParsed 的异步回调真正完成,避免在插件加载前就返回(竞态)。
var tcs = new TaskCompletionSource<CommunicationManager?>();

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

/// <summary>
/// 公共加载流程:切换工作目录、建立连接、加载插件并接线消息回调。
/// 连接失败时回退为无连接模式,仅加载插件。
/// </summary>
private static async Task<CommunicationManager?> 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;
}
}
21 changes: 17 additions & 4 deletions KitX.Loader.CSharp/CommunicationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,33 @@ public async Task<CommunicationManager> 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)
{
case WebSocketState.None:
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:
Expand All @@ -56,6 +66,9 @@ public async Task<CommunicationManager> Connect(string? url)
}
}

if (Client.State != WebSocketState.Open)
throw new InvalidOperationException($"WebSocket failed to connect, state: {Client.State}");

return this;
}

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

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="KitX.Contract.CSharp" Version="23.4.6543.429" />
</ItemGroup>

<ItemGroup>
Expand Down
15 changes: 13 additions & 2 deletions KitX.Loader.CSharp/PluginManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)
{
Expand All @@ -116,6 +126,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(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