mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-18 14:50:47 +08:00
feat: PluginIsolation 新增进程内 IPC 传输(InProcPluginIpcTransport)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
namespace LanMountainDesktop.PluginIsolation.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// 进程内 IPC 传输。将 <see cref="PluginIpcClient"/> 的调度委托直接绑定到
|
||||
/// <see cref="PluginIpcServer"/> 的处理入口,不经过命名管道。
|
||||
/// 用于不支持子进程/命名管道的平台(如 Android),插件契约与序列化路径保持不变:
|
||||
/// 请求仍会经过 JsonElement 序列化/反序列化边界,与管道传输行为一致。
|
||||
/// </summary>
|
||||
public sealed class InProcPluginIpcTransport : IDisposable
|
||||
{
|
||||
private readonly PluginIpcClient _client;
|
||||
private volatile bool _disposed;
|
||||
|
||||
private InProcPluginIpcTransport(PluginIpcClient client, PluginIpcServer server)
|
||||
{
|
||||
_client = client;
|
||||
Server = server;
|
||||
|
||||
client.RequestDispatcher = (route, payload, cancellationToken) =>
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return server.HandleRequestAsync(route, payload, cancellationToken);
|
||||
};
|
||||
|
||||
client.NotificationDispatcher = (route, payload, cancellationToken) =>
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return server.HandleNotificationAsync(route, payload, cancellationToken);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定后的服务端。
|
||||
/// </summary>
|
||||
public PluginIpcServer Server { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 绑定后的客户端。请求/通知将直接分发到 <see cref="Server"/>。
|
||||
/// </summary>
|
||||
public PluginIpcClient Client => _client;
|
||||
|
||||
/// <summary>
|
||||
/// 将客户端与服务端以进程内直通方式连接。
|
||||
/// </summary>
|
||||
/// <param name="client">插件侧客户端</param>
|
||||
/// <param name="server">宿主侧服务端</param>
|
||||
/// <returns>传输句柄;Dispose 后客户端调度将抛出 <see cref="ObjectDisposedException"/>。</returns>
|
||||
public static InProcPluginIpcTransport Connect(PluginIpcClient client, PluginIpcServer server)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
ArgumentNullException.ThrowIfNull(server);
|
||||
return new InProcPluginIpcTransport(client, server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开传输。之后客户端的请求/通知将失败。
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_client.RequestDispatcher = null;
|
||||
_client.NotificationDispatcher = null;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
}
|
||||
108
LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs
Normal file
108
LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.PluginIsolation.Ipc;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 进程内 IPC 传输测试:验证与管道传输相同的契约语义
|
||||
/// (请求/响应往返、通知投递、错误传播、释放语义)。
|
||||
/// </summary>
|
||||
public sealed class InProcPluginIpcTransportTests
|
||||
{
|
||||
private static (PluginIpcClient Client, PluginIpcServer Server) CreatePair()
|
||||
{
|
||||
var server = new PluginIpcServer(new PluginIpcServerOptions
|
||||
{
|
||||
PipeName = "inproc-test"
|
||||
});
|
||||
var client = new PluginIpcClient(new PluginIpcClientOptions
|
||||
{
|
||||
PipeName = "inproc-test"
|
||||
});
|
||||
return (client, server);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestAsync_RoundTripsThroughServerHandler()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
server.MapRequest<string, string>("test/echo", (payload, _) => Task.FromResult($"echo:{payload}"));
|
||||
using var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
|
||||
var response = await client.RequestAsync<string, string>("test/echo", "hello");
|
||||
|
||||
Assert.Equal("echo:hello", response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyAsync_DeliversPayloadToServerHandler()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
var received = new TaskCompletionSource<string?>();
|
||||
server.MapNotification<string>("test/notify", (payload, _) =>
|
||||
{
|
||||
received.TrySetResult(payload);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
using var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
|
||||
await client.NotifyAsync("test/notify", "ping");
|
||||
|
||||
var payload = await received.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("ping", payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestAsync_WhenHandlerThrows_PropagatesException()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
server.MapRequest<string, string>("test/fail", (_, _) => throw new InvalidOperationException("handler failed"));
|
||||
using var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => client.RequestAsync<string, string>("test/fail", "x"));
|
||||
Assert.Equal("handler failed", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestAsync_WhenRouteNotRegistered_Throws()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
using var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => client.RequestAsync<string, string>("test/missing", "x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestAsync_AfterDispose_ThrowsObjectDisposed()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
server.MapRequest<string, string>("test/echo", (payload, _) => Task.FromResult(payload));
|
||||
var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
transport.Dispose();
|
||||
|
||||
// Dispose 后调度委托被摘除,客户端回落到未绑定状态。
|
||||
await Assert.ThrowsAsync<NotSupportedException>(
|
||||
() => client.RequestAsync<string, string>("test/echo", "x"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestAsync_WithCancellation_PassesTokenToHandler()
|
||||
{
|
||||
var (client, server) = CreatePair();
|
||||
using var cts = new CancellationTokenSource();
|
||||
server.MapRequest<string, string>("test/cancel", async (_, ct) =>
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, ct);
|
||||
return "never";
|
||||
});
|
||||
using var transport = InProcPluginIpcTransport.Connect(client, server);
|
||||
|
||||
var task = client.RequestAsync<string, string>("test/cancel", "x", cts.Token);
|
||||
cts.Cancel();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => task);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop\LanMountainDesktop.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginIsolation.Ipc\LanMountainDesktop.PluginIsolation.Ipc.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.AirAppRuntime\LanMountainDesktop.AirAppRuntime.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Launcher\LanMountainDesktop.Launcher.csproj" />
|
||||
<ProjectReference Include="..\LanDesktopPLONDS.installer\LanDesktopPLONDS.installer.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user