From 2335693fdd115a654e2e8e3e5bc50719e23e379d Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 26 Jul 2026 22:28:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20PluginIsolation=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=86=85=20IPC=20=E4=BC=A0=E8=BE=93=EF=BC=88?= =?UTF-8?q?InProcPluginIpcTransport=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InProcPluginIpcTransport.cs | 74 ++++++++++++ .../InProcPluginIpcTransportTests.cs | 108 ++++++++++++++++++ .../LanMountainDesktop.Tests.csproj | 1 + 3 files changed, 183 insertions(+) create mode 100644 LanMountainDesktop.PluginIsolation.Ipc/InProcPluginIpcTransport.cs create mode 100644 LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs diff --git a/LanMountainDesktop.PluginIsolation.Ipc/InProcPluginIpcTransport.cs b/LanMountainDesktop.PluginIsolation.Ipc/InProcPluginIpcTransport.cs new file mode 100644 index 0000000..a5a414d --- /dev/null +++ b/LanMountainDesktop.PluginIsolation.Ipc/InProcPluginIpcTransport.cs @@ -0,0 +1,74 @@ +namespace LanMountainDesktop.PluginIsolation.Ipc; + +/// +/// 进程内 IPC 传输。将 的调度委托直接绑定到 +/// 的处理入口,不经过命名管道。 +/// 用于不支持子进程/命名管道的平台(如 Android),插件契约与序列化路径保持不变: +/// 请求仍会经过 JsonElement 序列化/反序列化边界,与管道传输行为一致。 +/// +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); + }; + } + + /// + /// 绑定后的服务端。 + /// + public PluginIpcServer Server { get; } + + /// + /// 绑定后的客户端。请求/通知将直接分发到 。 + /// + public PluginIpcClient Client => _client; + + /// + /// 将客户端与服务端以进程内直通方式连接。 + /// + /// 插件侧客户端 + /// 宿主侧服务端 + /// 传输句柄;Dispose 后客户端调度将抛出 + public static InProcPluginIpcTransport Connect(PluginIpcClient client, PluginIpcServer server) + { + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(server); + return new InProcPluginIpcTransport(client, server); + } + + /// + /// 断开传输。之后客户端的请求/通知将失败。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _client.RequestDispatcher = null; + _client.NotificationDispatcher = null; + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } +} diff --git a/LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs b/LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs new file mode 100644 index 0000000..70bb325 --- /dev/null +++ b/LanMountainDesktop.Tests/InProcPluginIpcTransportTests.cs @@ -0,0 +1,108 @@ +using System.Text.Json; +using LanMountainDesktop.PluginIsolation.Ipc; +using Xunit; + +namespace LanMountainDesktop.Tests; + +/// +/// 进程内 IPC 传输测试:验证与管道传输相同的契约语义 +/// (请求/响应往返、通知投递、错误传播、释放语义)。 +/// +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("test/echo", (payload, _) => Task.FromResult($"echo:{payload}")); + using var transport = InProcPluginIpcTransport.Connect(client, server); + + var response = await client.RequestAsync("test/echo", "hello"); + + Assert.Equal("echo:hello", response); + } + + [Fact] + public async Task NotifyAsync_DeliversPayloadToServerHandler() + { + var (client, server) = CreatePair(); + var received = new TaskCompletionSource(); + server.MapNotification("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("test/fail", (_, _) => throw new InvalidOperationException("handler failed")); + using var transport = InProcPluginIpcTransport.Connect(client, server); + + var ex = await Assert.ThrowsAsync( + () => client.RequestAsync("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( + () => client.RequestAsync("test/missing", "x")); + } + + [Fact] + public async Task RequestAsync_AfterDispose_ThrowsObjectDisposed() + { + var (client, server) = CreatePair(); + server.MapRequest("test/echo", (payload, _) => Task.FromResult(payload)); + var transport = InProcPluginIpcTransport.Connect(client, server); + transport.Dispose(); + + // Dispose 后调度委托被摘除,客户端回落到未绑定状态。 + await Assert.ThrowsAsync( + () => client.RequestAsync("test/echo", "x")); + } + + [Fact] + public async Task RequestAsync_WithCancellation_PassesTokenToHandler() + { + var (client, server) = CreatePair(); + using var cts = new CancellationTokenSource(); + server.MapRequest("test/cancel", async (_, ct) => + { + await Task.Delay(Timeout.Infinite, ct); + return "never"; + }); + using var transport = InProcPluginIpcTransport.Connect(client, server); + + var task = client.RequestAsync("test/cancel", "x", cts.Token); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(() => task); + } +} diff --git a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj index 763edb9..f59fcc1 100644 --- a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj +++ b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj @@ -19,6 +19,7 @@ +