2. 生命周期与 effect
Cordis 插件可能因配置修改、热重载、显式释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于
effect,会在所属插件卸载时按 LIFO 逆序撤销(src/Cordis/EffectScope.cs);在这些 API 之外管理的资源
必须包装在 ctx.Effect(...) 中。
Effect
对于 Cordis 尚未管理的资源——定时器、连接、watcher——获取它,并把释放逻辑交给 ctx.Effect:
ctx.Plugin(Plugin.From(c =>
{
c.Effect(async () => // C# API 直接登记卸载时执行的 disposer
{
await Task.Delay(50); // 模拟异步 flush
Console.WriteLine("parent cleaned up");
});
c.Plugin(Plugin.From(inner =>
{
Console.WriteLine("heartbeat plugin loading");
var timer = new System.Threading.Timer(_ => Console.WriteLine("tick"), null, 200, 200);
inner.Effect(() => // 这里传入的就是 cleanup,不是 resource factory
{
timer.Dispose();
Console.WriteLine("heartbeat cleaned up");
});
}));
}));
await Task.Delay(700); // 让程序保持运行,随后 await using root 触发卸载
运行(dotnet run)后会得到:
heartbeat plugin loading
tick
tick
...
heartbeat cleaned up
parent cleaned up
请留意三点:
c.Plugin(...)把来自代码的插件挂载为子插件——这与 YAML loader 为每个配置项执行的操作相同。 返回的PluginHandle可以随时DisposeAsync(),它会等该插件的所有清理(包括异步 disposer)完成后才结束, 并递归卸载它挂载的子插件。- 传给
ctx.Effect的委托就是 cleanup 本身,只在卸载期间运行;C# API 不会先执行一个 resource factory, 再收集它返回的 disposer。 - 父级先登记自己的 cleanup,后挂载子插件。卸载时 LIFO 让子插件先清理,再进入父级 cleanup; 异步 disposer 会被完整等待。
Fiber 状态机的 C# 承载
dsh 的每个 fiber 在 PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED / FAILED 之间转换。
C# 版没有独立的 fiber 枚举,状态由 PluginHandle 与激活图共同承载:
| dsh fiber 状态 | C# 对应 |
|---|---|
| PENDING | inject 未满足,ApplyAsync 尚未运行(IsActive == false 且 Completion 未完成) |
| LOADING / ACTIVE | ApplyAsync 正在运行 / 已完成,IsActive == true |
| FAILED | Completion 以 PluginApplyException 失败 |
| UNLOADING / DISPOSED | DisposeAsync 进行中 / IsDisposed == true |
你会在第 6 章再次遇到 PENDING—— 它通常就是”为什么我的插件没有输出”的答案。
已经属于 effect 的操作
你很少需要亲自写 ctx.Effect,因为内置注册 API 本身已经是 effect:
ctx.On(event, listener):监听器在卸载时移除(第 4 章)。ctx.Plugin(child):子插件随父插件一同 dispose。ctx.Provide<T>(instance):服务注册随提供方卸载而移除(第 3 章)。- harness 注册表(如
IToolRegistry.Register)返回的IDisposable也应交给ctx.Effect, 随调用插件撤销(第 7 章)。
有一项顺序注意事项:disposer 按注册逆序逐个调用,当前异步 disposer 完成后才进入下一项。 某项失败会被记录,但不会阻止后续 cleanup;scope 最终聚合报告全部失败。
下一章:服务:插件如何共享功能。