diff --git a/docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md b/docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md new file mode 100644 index 0000000..9d4aecd --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md @@ -0,0 +1,243 @@ +# LSP Core 生命周期、调度与传输设计 + +## 背景 + +`lsp.core.server` 当前用两个布尔值表达服务器生命周期,导致初始化前可以 +`shutdown`、可以重复 `initialize`,且 `shutdown` 响应后立即结束主循环, +没有按 LSP 要求等待 `exit`。普通请求在输入线程同步执行,因此服务器执行 +Provider 时无法读取 `$/cancelRequest`;现有取消入口没有实际取消逻辑。 + +消息入口和 Dispatcher 还存在几类协议边界问题:非法 JSON 与非法请求只记录 +日志,Provider 异常没有生成请求错误响应,依赖缺失或序列化失败使用 `"{}"` +伪响应,`Content-Length` 解析宽松且没有消息大小上限。生命周期 callback 在 +锁内执行,也允许重入死锁。诊断发布器虽然暂未启用,但会把 Tree-sitter 的 +UTF-8 字节列直接作为 LSP UTF-16 character。 + +本设计直接建立严格、单一的 LSP 3.17 行为,不保留原 API、旧生命周期行为、 +宽松 framing、Provider 形式的 `exit`/`$/cancelRequest` 或任何兼容适配层。 + +## 目标 + +- 用显式状态机约束 `initialize`、`shutdown` 与 `exit`。 +- 正常关闭等待 `exit`,并向进程入口返回准确退出码。 +- 让输入线程在普通请求执行期间继续读取取消通知。 +- 按请求实例精确取消,区分整数 ID 与字符串 ID。 +- 为请求解析、状态、Provider 异常和取消生成规范 JSON-RPC 错误。 +- 让 Dispatcher 构造后立即有效,不存在后注入依赖或伪响应。 +- 严格验证 LSP framing,遇到不可恢复的传输错误立即终止。 +- 删除已无必要的生命周期 callback 及其锁内调用路径。 +- 将诊断位置转换为 LSP UTF-16 坐标。 +- 删除无调用函数、重复错误构造和无效兜底代码。 + +## 非目标 + +- 不兼容旧的 `LspServer::Run()`、Dispatcher 默认构造或依赖 setter。 +- 不接受非 LSP 3.17 framing,也不尝试从流错位中恢复。 +- 不为已删除的 `shutdown`、`exit`、`cancelRequest` Provider 保留转发层。 +- 不保证强制中断任意 Provider;运行中取消仍遵循 `std::stop_token` 的协作式 + 语义。 +- 不启用当前关闭的诊断发布能力,只保证其坐标在以后启用时正确。 + +## 生命周期所有权 + +生命周期只由 `LspServer` 管理,不再由 Provider 通过 callback 间接修改。 +删除 `ServerLifecycleEvent`、`LifecycleCallback`、Dispatcher 的生命周期 callback +注册与通知,以及 `ExecutionContext::TriggerLifecycleEvent`。`initialize` 的 +Provider 只负责初始化参数、manager 状态和能力响应;`shutdown`、`exit` 与 +`$/cancelRequest` 都由 core 直接处理,不再注册为 Provider。 + +服务器使用以下状态: + +```cpp +enum class ServerState +{ + kUninitialized, + kRunning, + kShutdownRequested, + kExiting, +}; +``` + +状态只在输入线程修改,因此不使用原子布尔值: + +| 当前状态 | 输入 | 行为 | 新状态 | +| --- | --- | --- | --- | +| `kUninitialized` | `initialize` | 同步执行并发送成功响应 | `kRunning` | +| `kUninitialized` | 其他请求 | `ServerNotInitialized` | 不变 | +| `kUninitialized` | `exit` | 结束主循环,返回 1 | `kExiting` | +| `kRunning` | 重复 `initialize` | `InvalidRequest` | 不变 | +| `kRunning` | `shutdown` | 取消并等待普通请求,关闭 manager,发送响应 | `kShutdownRequested` | +| `kRunning` | `exit` | 结束主循环,返回 1 | `kExiting` | +| `kShutdownRequested` | `exit` | 结束主循环,返回 0 | `kExiting` | +| `kShutdownRequested` | 请求 | `InvalidRequest` | 不变 | +| `kShutdownRequested` | 其他通知 | 忽略并记录 | 不变 | + +`initialize` 只有在 Provider 正常返回并成功发送响应后才转换状态;异常或错误 +响应不推进状态。`shutdown` 完成请求排空和 manager shutdown 并成功发送响应后 +才转换状态。`initialized` 只在 `kRunning` 接受。EOF、短消息体和 framing 错误 +均属于非正常退出,返回 1。 + +`LspServer::Run()` 改为返回 `int`。launcher 直接使用该退出码,不再在 +`Run()` 返回后无条件报告“正常停止”。`exit` Provider 中的 sleep 和 +`std::exit()` 被删除,进程退出统一沿 `Run()` → launcher → `main()` 返回。 +无论收到正常或异常 `exit`,core 都先取消并等待仍活动的普通请求,再从 +`Run()` 返回;异常 EOF/framing 路径执行相同的任务收尾,但不把它误报为正常 +关闭。 + +## 请求执行与取消 + +`initialize`、`shutdown` 在输入线程同步执行,因为它们改变全局生命周期。 +普通请求提交给 `AsyncExecutor`。`exit` 与 `$/cancelRequest` 是 core 控制消息, +在输入线程立即处理;其他通知仍按输入顺序同步分发,避免 `didOpen`、 +`didChange` 与后续请求发生人为重排。 + +请求键保留 ID 类型: + +- 整数 ID `1` 映射为 `i:1`。 +- 字符串 ID `"1"` 映射为 `s:1`。 + +任务 ID 使用 core 专属前缀,例如 `lsp-request:i:1`,避免与 Provider 自己提交 +的后台任务冲突。服务器在 mutex 保护的映射中保存 +`request key -> shared request state`,其中包含该请求的 `TaskHandle`。输入线程 +先登记 request state,再调用 `Submit`,并在重新读取输入前绑定返回的 handle; +完成 callback 按 state 身份注销映射。这样快速完成或同步提交失败都不会造成 +“callback 先注销、输入线程后插入”的陈旧句柄,也不需要持有映射锁调用 +`Submit`。同一类型、同一 ID 尚未完成时再次出现,按 `InvalidRequest` 拒绝, +不替换或误取消旧请求。 + +异步任务调用 Dispatcher 并返回序列化响应。callback 根据 `TaskResult` 统一 +收尾: + +- `kCompleted`:发送 Provider 响应。 +- `kCancelled`:发送 `RequestCancelled`。 +- `kFailed`:记录原始异常并发送 `InternalError`。 + +`$/cancelRequest` 严格反序列化 `CancelParams`,按带类型的键查找当前句柄并 +调用 `TaskHandle::Cancel()`。找不到或任务已经完成时只记录,不生成通知响应。 +Dispatcher 将当前请求的 `std::stop_token` 放入 `ExecutionContext`;可协作 +取消的 Provider 和 manager 从 context 取得并向长循环传递。任务开始前、 +Provider 返回后都由 `AsyncExecutor` 再检查停止状态,保证已取消请求不会发送 +成功结果。 + +收到合法 `shutdown` 时,服务器先复制全部活动请求句柄,在锁外逐个请求取消 +并等待 callback 完成,再直接调用 `ManagerHub::Shutdown()` 并构造 null result +响应。这样 manager shutdown 不会与普通请求继续访问共享状态并发发生。等待 +期间不持有请求映射锁或输出锁。 + +## Dispatcher 与错误构造 + +`RequestDispatcher` 构造函数强制接收 `AsyncExecutor&` 和 `ManagerHub&`。 +`LspServer` 按 `ManagerHub`、`AsyncExecutor`、Dispatcher 的依赖顺序声明和构造 +成员。删除默认构造、`SetRequestScheduler`、`SetManagerHub`、空指针检查和 +`"{}"` 返回值。 + +Dispatcher 只负责查找 Provider、创建 `ExecutionContext` 并调用 Provider: + +- 未注册请求生成 `MethodNotFound`。 +- 未注册通知只记录日志。 +- Provider 异常不在 Dispatcher 中吞掉,交给请求任务 callback 转换为 + `InternalError`;通知入口捕获异常并只记录日志。 + +错误响应由一个公共构造函数生成,接收可选 Request ID、错误码和消息。 +Provider 与 server 共用该实现,删除 `SendError` 与 +`BuildErrorResponseMessage` 的重复组装逻辑。已识别请求回显其 ID;无法识别 +ID 时序列化为 `null`。 + +错误响应自身序列化失败视为内部不变量破坏并抛出,不返回 `"{}"`、手写固定 +JSON 或空字符串。外层只在能够构造规范响应时继续运行,不增加第二套序列化 +兜底。 + +## JSON-RPC 消息验证 + +`HandleMessage` 先区分 JSON 语法错误与消息结构错误: + +| 输入 | 处理 | +| --- | --- | +| 非法 JSON | `ParseError`,ID 为 `null` | +| 合法 JSON,但顶层不是对象 | `InvalidRequest`,ID 为 `null` | +| JSON 对象但不满足 JSON-RPC 2.0 消息结构 | `InvalidRequest` | +| 请求可识别 ID,但 Request 反序列化失败 | `InvalidRequest`,回显有效 ID | +| 合法请求的 Provider 抛异常 | `InternalError`,回显请求 ID | +| 合法通知的 Provider 抛异常 | 只记录,不发送响应 | +| 合法客户端响应 | 记录或交给现有响应入口,不回送响应 | + +请求、通知和响应必须显式携带 `"jsonrpc":"2.0"`。Request ID 只接受协议已 +定义的整数或字符串。响应必须恰好包含 `result` 或 `error` 之一。无法可靠 +分类为请求的非法客户端响应不触发响应,避免形成 response-to-response 循环。 + +## 严格 framing + +输入只接受 LSP 3.17 header/body framing: + +- header 行和 header 结束符必须使用 `\r\n`。 +- 必须且只能出现一个 `Content-Length`。 +- 长度使用 `std::from_chars` 解析十进制数字,并要求完整消费字段值。 +- 长度必须大于 0 且不超过 16 MiB。 +- 可接受规范定义的可选 + `Content-Type: application/vscode-jsonrpc; charset=utf-8`;未知、重复或格式 + 错误字段视为 framing 错误,不接受旧 charset 拼写。 +- 消息体必须精确读满声明长度。 + +读取结果明确区分“完整消息”“EOF”和“致命 framing 错误”。EOF 或致命错误 +立即退出主循环,不 sleep、不继续扫描、不尝试重新同步流。输出继续由单一 +mutex 串行写入并检查 stream 状态;同步写失败直接向 launcher 传播,异步 +callback 写失败记录 fatal I/O 状态,主循环在下一控制点以 1 退出。 + +## Diagnostics 坐标 + +`lsp.utils.text_coordinates` 增加 Tree-sitter UTF-8 字节点位到 LSP UTF-16 +`Position` 的反向转换。转换按目标行扫描完整 UTF-8 字符,并累计 UTF-16 code +unit;目标字节列落在多字节字符内部时收敛到该字符起点,超过行尾时收敛到 +行尾。 + +`PublishDiagnostics` 使用该接口分别转换语法错误起止位置,不再直接复制 +`start_column`/`end_column`。转换复用已有 UTF-8 解码规则,不在 server 中 +维护第二套编码逻辑。 + +## 清理范围 + +- 删除 `RequiresSyncProcessing` 与 `CanProcessRequest`,状态策略只保留一个 + 实际调用入口。 +- 删除 Dispatcher 生命周期 callback 及其 mutex。 +- 删除不再注册且由 core 直接处理的 `provider/shutdown`、`provider/exit`、 + `provider/cancel_request` Module,并同步 Provider registry 与 CMake Module + 列表。 +- 删除 initialize Provider 的生命周期事件代码。 +- 合并错误响应构造,删除 `"{}"`、固定 JSON、空响应和 sleep 重试兜底。 +- `SetupLogger` 保持独立函数;它职责明确,不属于本次 core 重构。 + +## 测试设计 + +测试遵循 TDD,每项先在旧实现上观察预期失败: + +- initialize 前普通请求返回 `ServerNotInitialized`。 +- initialize 前 shutdown 被拒绝;重复 initialize 被拒绝。 +- shutdown 响应后仍等待 exit。 +- `shutdown -> exit` 返回 0,直接 exit、shutdown 后 EOF 和普通 EOF 返回 1。 +- 非法 JSON 返回 `ParseError`,非法 JSON-RPC 对象返回 `InvalidRequest`。 +- 未注册请求返回 `MethodNotFound`。 +- 抛异常请求返回 `InternalError`;抛异常通知不产生响应。 +- 普通请求不会阻塞主循环读取 `$/cancelRequest`。 +- 取消整数 ID 与字符串 ID 不冲突,取消只影响对应请求实例。 +- 已取消请求返回 `RequestCancelled`,完成请求不会被迟到取消改写。 +- shutdown 取消并等待所有活动普通请求。 +- 缺失、重复、非数字、部分数字、零、超限 `Content-Length` 和短消息体导致 + 非零退出;规范消息仍可连续读取。 +- callback 重入不再存在死锁路径,因为生命周期 callback 机制已删除。 +- 中文和 emoji 前后的 diagnostic character 使用 UTF-16 code unit。 + +优先扩展 `test_provider` 和服务器进程级 JSON 测试,直接验证真实 framing、 +响应和退出码。完成后构建 `tsl-server`,运行 scheduler、provider 与 LSP JSON +相关测试。已知无关的 AST、symbol、semantic 脚本失败不作为本任务的通过依据。 + +## 完成条件 + +- 生命周期只能沿设计表转换,退出码与关闭顺序一致。 +- 普通请求异步执行,取消通知能在请求运行期间被读取并精确定位任务。 +- 所有请求错误都产生合法 JSON-RPC 响应,通知错误不产生响应。 +- Dispatcher 不存在无效中间状态或伪响应。 +- framing 错误不会进入 sleep/重试/继续读取分支。 +- diagnostics 使用 UTF-16 character。 +- core 中不存在原两个状态 bool、未使用检查函数、空取消分支、生命周期 callback + 或 `"{}"`/固定 JSON 兜底。 +- 相关构建和测试通过,并完成一次针对无效兜底与临时兼容层的专项复审。 diff --git a/memory-bank/progress.md b/memory-bank/progress.md index cf4dafd..42d189b 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -62,8 +62,8 @@ ## Workflow State -phase: done -spec: docs/superpowers/specs/2026-07-12-async-executor-cancellation-design.md +phase: planning +spec: docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md plan: docs/superpowers/plans/2026-07-12-async-executor-cancellation.md executor: executing-plans constraints: karpathy-guidelines,.agents,AGENT_RULES