XiHan.Framework.Plugins.OutOfProcess
进程外插件宿主(isolation: out-of-process):按协议 xihan.plugin.v1 启动插件子进程、握手、配置、监视健康与崩溃重启,把插件声明的操作登记为经完整管道的代理操作、把订阅的消息投递给插件,并提供受权限约束的宿主回调。适用于任意语言写的插件与远程 Provider。
| 项 | 内容 |
|---|---|
| 包 ID | XiHan.Framework.Plugins.OutOfProcess |
| 环层 | Ring 5(协议与插件) |
| 能力 | 提供 plugins.out-of-process@1;依赖 kernel.hosting@1 |
| 特性入口 | OutOfProcessPluginsFeature |
| 配置节 | XiHan:Plugins:OutOfProcess |
| 依赖 | XiHan.Framework.Kernel、XiHan.Framework.Plugins.Protocol、XiHan.Framework.Messaging.Abstractions、XiHan.Framework.Localization.Abstractions;ASP.NET Core 共享框架;Grpc.AspNetCore.Server、Grpc.Net.Client |
| AOT | 不兼容(Google.Protobuf 的描述符反射未声明裁剪兼容) |
| 稳定性 | Preview |
概述
特性在配置阶段按 Directories 同步发现插件:读 xihan.manifest.json(要求 kind: plugin、isolation: out-of-process),核对 Grants 授予的权限,以插件目录为工作目录启动 entry 指向的可执行文件,只继承必要的环境变量并追加 XIHAN_PLUGIN_*,在 HandshakeTimeout 内读到 stdout 上的握手行、连上插件端点并调用 Handshake,再按 spec/plugin/handshake.md 核对清单与声明。声明的操作登记为代理操作,清单 contributes.permissions 声明的权限码连同它们一次登记进 PermissionRegistry(显示名取码本身,同组的操作可以引用,ADR-0080),消息目录接进本地化器,订阅的主题登记事件处理器。
启动阶段启动回调服务器(Windows 上是命名管道,其他系统是 Unix 域套接字)与 onStartup 的插件:Configure(下发配置节 JSON、权限、绑定与工作目录)→ Start → 健康监视。懒激活的插件在握手后停止进程,首次调用或投递时再启动。停止阶段按与登记相反的顺序停止全部插件,再关闭回调服务器。
运行期每次调用带 traceparent、截止时间与已登记的 Baggage;业务失败来自插件的 Status.error,用握手得到的目录翻译;传输失败映射为 PLUGINS_UNAVAILABLE_001、KERNEL_TIMEOUT_001 等。插件回调 Host.Invoke / Host.Publish 以插件身份执行(主体 ID 为清单 ID,方案 Plugin)。每个插件是应用的一个健康条目 plugins.<清单 ID>。
何时使用
- 插件用 Go、Rust 等其他语言写,或要独立进程、独立依赖:用本包。Go 有现成的 framework-sdk-go。
- 用插件提供子系统能力(远程 Provider,如 Caching.Remote):用本包发现插件,再装配对应的
<领域>.Remote适配器。 - 要在管道阶段插入中间件:进程外插件做不到,用 Plugins.Wasm。清单写了
contributes.pipeline的进程外插件在发现时被拒(PLUGINS_VALIDATION_001),不会照常加载、把中间件静默忽略。 - 受信任的 .NET 扩展、不需要独立进程:Plugins.InProcess 更轻。
安装与启用
dotnet add package XiHan.Framework.Plugins.OutOfProcess --prerelease当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Plugins.OutOfProcess;
using XiHan.Framework.Profiles.Standard;
var builder = WebApplication.CreateBuilder(args);
builder.AddXiHan(xihan => xihan.UseStandardProfile().UseFeature<OutOfProcessPluginsFeature>());
var app = builder.Build();
app.UseXiHan();
app.MapXiHanHealth();
app.Run();配置
| 键 | 类型 | 缺省 | 说明 |
|---|---|---|---|
Directories | 字符串数组 | 空 | 启动时装载的插件目录,一个目录一个插件;相对路径相对当前目录 |
Grants:<清单 ID> | 字符串数组 | 空授予 | 授予该插件的权限;清单声明的权限必须全部授予才能加载 |
HandshakeTimeoutSeconds | 正数 | 10 | 读到握手行并完成 Handshake 的时限 |
StartTimeoutSeconds | 正数 | 30 | Configure 与 Start 各自的时限 |
StopGraceSeconds | 正数 | 取 XiHan:Kernel:StopTimeoutSeconds(缺省 30) | 停止宽限;超时先发终止信号,5 秒后仍未退出再终止进程树 |
HealthIntervalSeconds | 正数 | 10 | 健康推送断开后的轮询间隔 |
HealthFailures | 正整数 | 3 | 连续多少次健康轮询失败视为进程失效 |
RestartLimit | 非负整数 | 5 | RestartWindowSeconds 内最多自动重启多少次,之后保持 Failed |
RestartWindowSeconds | 正数 | 600 | 统计重启次数的时间窗 |
WorkDirectory | 字符串 | 临时目录下的 xihan-plugins | 插件工作目录的根,每个插件一个子目录,宿主停止时不删除 |
{
"XiHan": {
"Plugins": {
"OutOfProcess": {
"Directories": [ "plugins/echo" ],
"Grants": { "xihan.samples.echo": [ "capability:kernel.pipeline" ] },
"HandshakeTimeoutSeconds": 10,
"StartTimeoutSeconds": 30,
"HealthIntervalSeconds": 10,
"HealthFailures": 3,
"RestartLimit": 5,
"RestartWindowSeconds": 600
}
},
"Echo": { "Greeting": "hello" }
}
}时长不是正数、HealthFailures 不是正整数、RestartLimit 不是非负整数时,OutOfProcessPluginOptions.FromConfiguration 以带配置键的 ArgumentException 拒绝启动。插件自己的配置节由插件清单 contributes.config.section 决定(上例 XiHan:Echo),宿主在 Configure 时把它序列化为 JSON 对象下发;绑定从 XiHan:Bindings:<名字> 取,只下发清单声明的绑定。
主要类型
| 类型 | 说明 |
|---|---|
OutOfProcessPluginsFeature | 特性入口,能力常量 Capability(plugins.out-of-process) |
OutOfProcessPluginHost | 宿主:实现 IPluginHost、IHealthContributor、IAsyncDisposable;Describe(manifestId) 返回诊断快照,HostEndpoint 是回调端点;常量 PipelinePermission、MessagingPermission、PluginScheme、HealthPrefix |
OutOfProcessPluginStatus | 诊断快照:Status(PluginStatus)、ProcessId、ProtocolVersion、Restarts、Lazy |
OutOfProcessPluginOptions | 选项与 FromConfiguration、GrantsFor、ResolveWorkDirectory |
特性把宿主同时登记为 OutOfProcessPluginHost、IPluginHost 与 IHealthContributor 单例。
使用示例
查看插件状态:
using XiHan.Framework.Plugins.OutOfProcess;
static string Describe(IServiceProvider services, string manifestId)
{
var host = services.GetRequiredService<OutOfProcessPluginHost>();
// 未加载为 null;发现时被拒的插件也在宿主里,状态为 Failed、没有进程
return host.Describe(manifestId) is { } status
? $"{status.Status.State} pid={status.ProcessId} restarts={status.Restarts} lazy={status.Lazy}"
: "not loaded";
}经内核调用器调用插件操作(与本地操作写法相同):
using System.Text;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Kernel.Operations;
static async Task<OperationResponse> EchoAsync(IServiceProvider services)
{
await using var scope = services.CreateAsyncScope();
var context = App.CreateContext(scope.ServiceProvider, CancellationToken.None, deadline: DateTimeOffset.UtcNow.AddSeconds(5));
var invoker = scope.ServiceProvider.GetRequiredService<IOperationInvoker>();
return await invoker.InvokeAsync(context, new OperationRequest("echoes.create", Encoding.UTF8.GetBytes("""{"text":"ping"}""")));
}完整的插件示例:.NET 写的 samples/EchoPlugin(只依赖 protobuf 与 gRPC,不引用任何 XiHan 包)、Go 写的 samples/EchoGoPlugin 与 samples/CacheGoPlugin。
行为与限制
- 发现失败不阻止应用启动,也不会静默消失:清单不合法(含写了
contributes.pipeline:只有 Wasm 插件能贡献中间件,消息键plugins.process.pipeline_unsupported,细节middleware列出声明的中间件名)、权限未授予、可执行文件不存在的插件不启动进程,以Failed登记(出现在Plugins里、可Describe、健康条目plugins.<清单 ID>,清单读不出来时只有健康条目plugins.<目录名>),宿主启动时记错误日志,不重试;握手失败的插件同样以Failed留在宿主里。运行期LoadAsync的同类失败直接返回给调用方、不登记。 - 启动阶段的握手、
Configure、Start失败按重启策略重试(spec/plugin/lifecycle.md§6):宿主附着到容器后按与崩溃相同的退避在后台重试,计入RestartLimit;运行期LoadAsync的这类失败返回给调用方,插件保持Failed并同样重试,直到UnloadAsync。首次握手就失败的插件重试成功后,代理操作按运行期加载补登记(只对IOperationInvoker可见,协议适配器不新增路由),握手里的订阅与消息目录要宿主重启才生效(记警告);声明登记不进去(声明的权限码与已登记的重复KERNEL_SECURITY_001、与已有操作重名,或操作的permission既不是宿主已登记的、也不在本插件清单里,KERNEL_VALIDATION_005,或授权能力的启动核对之后操作的policy不存在,SECURITY_VALIDATION_004)则按发现时被拒处理、不再重试,一个权限码与代理操作都不留下(一个插件的权限码与操作全进或全不进,ADR-0071、ADR-0080)。懒激活插件首次调用时启动失败,该次调用失败,下一次调用再试。宿主附着后改用容器里的TimeProvider计时退避、健康轮询与停止等待。插件操作与应用操作重名、插件声明的权限码与宿主或别的插件已登记的码重复,都是配置错误,启动失败。 - 崩溃(进程非
Stop引起的退出,或健康轮询连续失败后被终止)进入Failed(PLUGINS_UNEXPECTED_002),按 1 秒起翻倍、上限 60 秒退避重启;窗口内超过RestartLimit次后保持Failed,直到UnloadAsync+LoadAsync。重启后的握手声明必须与首次一致。 - 插件
Failed或Degraded让应用进入Degraded,不让应用退出。 - 停止:发
Stop(宽限秒数与原因shutdown/unload/restart)→ 在宽限内等待退出 → 超时先发终止信号:关闭插件的 stdin,Unix 上同时发 SIGTERM(Windows 没有 SIGTERM,关闭 stdin 就是等价的信号)→ 5 秒内仍未退出才终止进程树;两种结局都记为Failed(PLUGINS_TIMEOUT_001)。插件应把 SIGTERM 与 stdin 关闭都当作立即收尾退出的请求。 - 运行期
LoadAsync的代理操作只对IOperationInvoker可见,协议适配器不会为它新增路由。卸载先按归属撤回插件的代理操作与声明的权限码、再停止进程(ADR-0085):之后调用返回KERNEL_NOTFOUND_001,在途调用照常完成或以PLUGINS_UNAVAILABLE_001结束;同一插件可以直接再加载,不用重启宿主。卸载之后才完成的重试握手不再登记(只登记仍是宿主当前运行时的插件)。订阅与消息目录不随卸载撤回。 - 回调需要会话令牌(元数据
xihan-plugin-session),缺失或不符为Unauthenticated+PLUGINS_DENIED_002;Host.Invoke要capability:kernel.pipeline,Host.Publish要capability:messaging.queue,权限须在清单声明且已授予(授予多于声明的部分不生效),未声明或未授予为PermissionDenied+PLUGINS_DENIED_002。 - 不负责:流式操作在 .NET 侧的发布(协议已定义
InvokeStream帧)、操作系统沙箱(net.outbound:/fs.*:只核对授予)、Wasm。 - 事件订阅:握手的每个
Subscription按它的mode订阅主题(spec/plugin/invocation.md§4):空或competing是竞争消费(订阅了主题的宿主实例里只有一个把消息交给插件),broadcast是广播(每个在线的宿主实例都交给自己的插件进程,尽力而为、不进死信,RFC-0009「订阅方式」)。宿主每个(主题,方式)登记一次转交处理器,只把投递交给以同一方式订阅了主题的插件;mode不合法或同一(主题,类型)以两种方式各声明一次是PLUGINS_VALIDATION_004。 - 清单的
contributes.config声明XiHan:Plugins:OutOfProcess。宿主另读别的领域的两项共享设置,不在清单里(RFC-0002「配置节」):XiHan:Kernel:StopTimeoutSeconds(插件停止宽限的缺省)与XiHan:Localization:DefaultLocale(握手时告诉插件的缺省区域);每个插件收到的是它自己清单声明的配置节。
相关
- 插件
- Plugins.Protocol、Plugins.Wasm、Caching.Remote
- Conformance 一致性套件
- 规范:RFC-0017《进程外插件协议》、RFC-0019《远程 Provider》
