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# 对应
PENDINGinject 未满足,ApplyAsync 尚未运行(IsActive == false 且 Completion 未完成)
LOADING / ACTIVEApplyAsync 正在运行 / 已完成,IsActive == true
FAILEDCompletion 以 PluginApplyException 失败
UNLOADING / DISPOSEDDisposeAsync 进行中 / 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 最终聚合报告全部失败。

下一章:服务:插件如何共享功能。

在 GitHub 上编辑此页