Repository navigation
feat(plugins): 宿主调用插件函数入口(#1351 PR 3) - #1366
xiaoyumuxi wants to merge 3 commits into
Conversation
…s on macOS Build providers start as sessions of the plugin that owns them, so Core negotiates litheFunctions. Plugin web pages list, invoke and cancel the functions of their own instances through functions.* bridge methods, with consent, progress events, Core error codes and pluginErrorCode. Shared language servers serve no plugin's functions; lithe/host/* stays closed (see the new Agent Note). Refs #1351
macOS sends the workspace as a directory URI ending in '/', so the sample's inside-workspace check rejected every path and each build reported sample.config. Resolve the root before comparing, and make the plugin_sdk_node end-to-end test send that form of rootUri. Found during the macOS native acceptance of plugin functions (#1366). Refs #1351
The sample-build plugin from #1365 was exercised in a debug app: list, invoke with progress, plugin failure codes, cancel, cross-plugin refusal, and process cleanup on project close and plugin disable. plugin-functions is now verified on macOS. Consent for functions.* is remembered per plugin and workspace until the project closes, like build.*; the Note and the contract README said it was asked on every call. Refs #1351
Lithe Review结论: Findings
Scope Check
Suggestions
Summary整体接线沿用现有 Provider 和 LSP 生命周期,没有按插件名分支;归属、错误、进度、取消与资源清理路径均有对应实现。本次有两项非阻塞建议,涉及并发容量和调用标识的契约一致性。未发现需要阻塞合并的问题。 Verification
|
xiaoyumuxi
left a comment
There was a problem hiding this comment.
补充代码审查(head 5c5df59)。已有自动评论指出同意提示并发绕过 16 调用上限,以及完成后可复用 call ID,这里不重复发相同行内意见。另发现同意提示期间取消请求的时序缺口,详见行内评论。建议把调用 ID/生命周期的保留提前到异步同意之前,并补时序测试。本次为静态审查,未独立运行测试。
| guard functionCalls.count < Self.functionCallLimit else { | ||
| throw PluginWebBridgeFailure.unavailable("This page already runs \(Self.functionCallLimit) function calls.") | ||
| } | ||
| try await consent(String(localized: "Run \(functionID) of \(entry.instance.displayName)"), workspaceURL: workspaceURL) |
There was a problem hiding this comment.
[P2] 等待同意期间,functions.cancel 无法取消已发起的调用。 functionCalls[call] 直到 await consent(...) 返回后才登记。页面先发送 functions.invoke({ call: 'c1', ... }) 并停在同意框,再发送 functions.cancel({ call: 'c1' }),第 250 行只会对一个不存在的任务执行 no-op 并返回 {};随后若同意框被批准,原请求仍会启动可能修改工作区的函数。建议在等待同意之前就保留调用记录/取消标记,同意后核对该标记再真正启动;补一个 invoke → cancel → approve consent 的回归测试。
- functions.invoke takes its slot under the 16-call limit and registers its call before awaiting consent, so concurrent requests cannot all pass a stale count and exceed the limit once consent is granted. - functions.cancel during the consent prompt marks the call; it ends with requestCancelled after consent and the provider function never starts. - A page's call ID is never accepted twice, also after the call ended, so late progress of an old call cannot be attributed to a new one; the set is released when the page closes. - Contract and Agent Note describe the limit, cancel, and ID rules; tests cover invoke -> cancel -> approve, 16 calls pending consent plus one, and ID reuse.
xiaoyumuxi
left a comment
There was a problem hiding this comment.
第二轮深入审查:宿主调用链与故障时序
审查锚定 5c5df59f854485318528f65bca1aa5881f704f44,向下追踪到已有 Rust Core 的请求发送、管道写入、取消和进程终止实现。
结论
新增 4 条行内发现(1 个 P1、3 个 P2)。其中 P1 是新增函数入口在 MainActor 上同步写 Provider 管道,遇到不再读取 stdin 的后端时会阻塞 UI,连同取消和事件轮询一起被阻塞。建议合并前优先处理。
其他发现分别是:Provider 初始化期间的调用取消不能及时结束等待、把任意 Core details 当成稳定错误码、以及新的 scripted plugin start 忽略 initialState、破坏了原有初始化中停止测试的受控前置条件。
已有的“同意等待期间绕过 16 上限”“call ID 复用”和上一轮“同意框期间丢失取消”不重复发评论。本轮初始化等待问题发生在同意已结束、调用已登记之后,是另一个取消阶段。
实际验证与限制
- 静态追踪:PluginWebBridge → BuildSystemService → MacBuildProviderSessions → LanguageServerRuntimeSession → RustCoreBridge → Rust RuntimeSession / SystemProcess。
- 在 Linux / Swift 6.2.1 上运行了两个隔离探针。同步管道探针使用实际 Node SDK 后端,让一个函数占用 JS 线程不再读 stdin,然后在 MainActor 同步写入约 700 KiB 的另一个函数请求;100 ms 的 MainActor 计时器直到约 402 ms、外部 watchdog 强杀后端后才执行。这验证了阻塞机制,不是声称已经在完整 macOS 应用中实测同一时间值。
- 共享启动 Task 的取消探针显示:取消等待者后 150 ms 仍未结束,只有释放 startup gate 后才得到 requestCancelled,与当前 task.value 后才检查取消的结构一致。
- 错误码和测试 fixture 两项来自代码追踪;未声称观察到真实 CI 随机失败。
- 未运行完整 macOS 测试、Swift 应用构建、Rust Core 端到端测试或 Windows 验证。探针不替代这些平台回归。
SDK 侧另外的进程回收缺口已评论在 #1365,避免在两个 PR 重复列同一问题。
| } | ||
| let operation: LanguageServerRuntimeOperation | ||
| switch core.requestPluginFunction( | ||
| sessionID: sessionID, pluginID: pluginID, functionID: functionID, params: params, timeout: timeout |
There was a problem hiding this comment.
[P1] 不要在 MainActor 上同步写入可能不再读取的 Provider 管道。 这里的 core.requestPluginFunction 是同步调用。实际链路通过 RustCoreBridge 的同步 FFI,进入 Rust RuntimeSession::function_request → send_request_or_fail → SystemProcess::write_input 的 write_all,并非只把操作放入非阻塞队列。
触发条件很具体:一个插件函数长时间占用 Node 线程、暂停调试或后端卡住而不再读取 stdin,页面又发起一个合法的大参数调用(如约 700 KiB)。管道满后这里会阻塞 MainActor;下面的 cancellation handler 尚未安装,页面取消和 Swift 事件轮询也无法执行。Core 的普通请求超时可以排队 completion/cancellation,但 outbound maintenance 因发送锁被持有而跳过,不能靠该超时保证这次写入返回。
隔离 Swift + 实际 SDK 后端探针已验证同一机制:100 ms 主 actor 计时器直到约 402 ms、外部强杀后端后才运行;未运行完整 macOS 应用。建议将可能阻塞的发送移出 MainActor,并提供有界发送/终止后端的逃生路径;补“不读 stdin + 大请求 + 用户取消”的原生回归测试。
| timeout: Duration, | ||
| progress: @escaping @MainActor @Sendable (PluginFunctionProgress) -> Void | ||
| ) async throws -> ToolingJSONValue { | ||
| let session = try await functionSession(for: target) |
There was a problem hiding this comment.
[P2] Provider 还在启动时,取消调用无法及时结束其等待。 此处 functionSession 最终会在 startSession / runningSession 中等待共享的非结构化启动 Task.value;它们只在 value 返回后才 Task.checkCancellation()。因此用户已经同意、调用已经登记后,在 Provider 初始化挂起时执行 functions.cancel 或关闭页面,仅标记调用 Task 为 cancelled,并不能结束它当前的 await。原 invoke Promise 和调用名额要一直保留到启动成功/失败;当前初始化预算可达 60 秒。
已用隔离 Swift 探针验证相同 await 结构:取消等待者后仍不结束,释放启动 gate 后才得到取消。这里不是上一轮同意框期间“取消根本没记住”的问题。建议给每个调用建立可独立取消的启动等待者,取消时立即结束该调用,但不要直接取消其他调用也在复用的共享启动 Task;补 cancel-during-initialize 测试。
| case .failure(let error): | ||
| throw PluginFunctionFailure(code: error.details ?? error.code, message: error.message) | ||
| } | ||
| functionProgress[operation.operationID] = progress |
There was a problem hiding this comment.
[P2] 只把已识别的 plugin.function 拒绝码从 details 提升为 code。 error.details ?? error.code 假定每个 Core details 都是稳定函数码,但真实传输错误不符合这个假定。例如 Rust SystemProcess::write_input 的写入失败返回 ProcessFailed,并用 .with_details(error.to_string()) 保存 Broken pipe (os error 32) 一类诊断文本;这条错误沿当前调用链返回后,会被作为页面的 error.code,丢失原有稳定代码并随平台/语言变化。
建议仅当 details 是允许的 plugin.function.* 契约码时使用它,否则保留 error.code,诊断仍作为 detail/message。可给 scripted Core 注入一个 code 为传输失败、details 为任意 OS 错误文案的拒绝,断言网页拿到的 code 不会变成诊断句子。本项来自真实错误构造与映射链路的静态核对,未在完整应用中注入 broken pipe。
| ]) | ||
| ), | ||
| LanguageServerRuntimeEvent(type: "stateChanged", state: "ready") | ||
| ] |
There was a problem hiding this comment.
[P2] 新的 plugin start 假实现需要保留真正的 initializing 分支。 这里不论 ScriptedProviderCore.initialState 是什么,都立即排入 stateChanged: ready。而现有 releasingProvidersStopsReadyAndInitializingSessions(initializing: true) 专门构造 initialState: "initializing", readyAfterStop: true,目的是在收到 ready 前停止,并验证延迟 ready 不会触发构建请求。
本 PR 把 Provider 改走 startPluginLanguageServer 后,该配置被忽略:ready 现在可能在测试调用 retainProviders 前就被轮询处理,使“初始化中停止”的前置条件依赖调度顺序,或实际测成已经 ready 的停止。建议按 initialState 控制初始 ready 事件,并用显式 gate 保留停止后的迟到 ready 场景。这里指出的是 fixture 丢失受控时序,不声称本轮已经跑出 macOS CI 随机失败。
关联 #1351(PR 3:宿主调用插件函数入口)。基于已合并的 PR 1 (#1358) 的 Core 协议;Node SDK 在 #1365,验收插件
sdk/node/examples/sample-build位于该 PR 分支。功能点
插件自己的网页窗口现在可以列出、调用、取消自己构建 Provider 进程提供的插件函数,并收到结果、进度和插件错误码。宿主没有任何按插件名的分支。
改动清单
接线:Provider 以插件会话启动
LanguageServerRuntimeCore新增startPluginLanguageServer(pluginID:…)和requestPluginFunction(…),两者都有默认实现,旧的 Core 实现不用改。RustCoreBridge:lsp.startServer可以带pluginId。lsp.functionRequest。pluginErrorCode字段。RustLanguageServerRuntimeAdapter实现上面两个方法,并把pluginErrorCode映射进事件。LanguageServerRuntimeSession:pluginID,有它时用插件会话启动。functionsChanged来维护functionIDs,订阅requestProgress来分发进度。invokeFunction:Core 拒绝时保留plugin.function.*码;任务取消时调用lsp.cancelOperation。MacBuildProviderSessions:pluginID创建会话。pluginFunctions(of:)和invokePluginFunction,复用正在运行的进程,否则按send的方式启动。plugin.function.unavailable拒绝。服务与功能模型
BuildProviderRequesting新增两个方法,默认实现返回 unavailable。BuildSystemService:pluginFunctions和invokePluginFunction。captureOutput改用providerTarget(_:workspaceURL:)。BuildSystemsFeatureModel:转发这两个方法。网页桥接
PluginWebBridge新增三个方法:functions.list {instance}返回{functions}。它会启动 Provider,所以要先同意。functions.invoke {instance, function, params?, call?}返回{value}。需要同意(与build.*相同:同一插件和工作区同意一次后,到关闭项目前不再询问);同一callID 不能重复使用;每页最多同时 16 个调用。functions.cancel {call}返回{}。functions.progress,只在调用时给了callID 才发送。entry(),只能访问本插件的实例。Core 还会再校验一次pluginID。plugin.function.*、requestCancelled、requestTimeout),桥接错误新增pluginErrorCode。MacPluginWebPage的 JS 拒绝对象也带上pluginErrorCode。@MainActor @Sendable,以兼容不同语言模式的 target。文档、Note 与矩阵
shared/contracts/plugins/v1/README.md:functions.*三行,事件表新增functions.progress。2026-10-20-host-plugin-function-invocation.md,记录以下决策:lithe/host/*暂缓及理由:没有真实需求,权限要和信任审查一起设计,以后按新协议版本加不会破坏兼容。plugin-functions:更新 macOS 证据、验证说明和备注,macOS 验证状态改为 verified(见下方原生验收)。实现状态仍为 partial,因为 Windows 没有插件后端宿主。Windows 不变。测试
PluginWebBridgeTests新增 3 个测试:pluginErrorCode能到达页面。MacBuildProviderSessionsTests新增 2 个测试,用脚本化的假 Core:pluginID;函数列表、结果、进度、插件错误码和 Core 拒绝码都正确。cancelLanguageServerOperation;停用插件会停止 Provider。本地验证
scripts/test-macos.sh --filter "PluginWebBridgeTests|MacBuildProviderSessionsTests|BuildSystemServiceTests":82 个测试全部通过。swift build通过。verify-test-stability.shverify-service-boundaries.shverify-runtime-bundle-immutability.shverify-shared-contracts.shverify-agent-notes.shverify-platform-feature-matrix.shverify-platform-feature-matrix-change.sh origin/preview HEADmacOS 原生验收(2026-10-20)
在 debug 应用里,用 #1365 的
sdk/node/examples/sample-build(加一个只用于验收的网页窗口)和另一个带构建系统的插件做了验收:functions.list返回snapshot.read、workspace.build;成功构建一次后,列表里多出运行时注册的snapshot.clear。functions.invoke正常返回{value},页面依次收到三条functions.progress。失败的项目返回plugin.function.failed,并带上插件自己的pluginErrorCode: sample.buildFailed。functions.cancel后调用以requestCancelled结束,构建子进程也随之退出。plugin.web.forbidden,并且在弹同意框之前就被拒绝。plugin.function.unknown。rootUri末尾带斜杠时会拒绝所有路径,修复已推送到 feat(plugins): 官方 Node 插件后端 SDK 与示例构建插件 (#1351 PR 2) #1365(8a2e04109)。合并顺序
#1365 和本 PR 都修改了
shared/platform-feature-matrix/features/plugin-functions.json和契约 README,后合并的那个需要解决冲突,两边内容都保留。未完成