📝 docs(spec): design strict LSP core lifecycle
This commit is contained in:
@@ -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 兜底。
|
||||||
|
- 相关构建和测试通过,并完成一次针对无效兜底与临时兼容层的专项复审。
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
## Workflow State
|
## Workflow State
|
||||||
|
|
||||||
<!-- workflow-state:start -->
|
<!-- workflow-state:start -->
|
||||||
phase: done
|
phase: planning
|
||||||
spec: docs/superpowers/specs/2026-07-12-async-executor-cancellation-design.md
|
spec: docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md
|
||||||
plan: docs/superpowers/plans/2026-07-12-async-executor-cancellation.md
|
plan: docs/superpowers/plans/2026-07-12-async-executor-cancellation.md
|
||||||
executor: executing-plans
|
executor: executing-plans
|
||||||
constraints: karpathy-guidelines,.agents,AGENT_RULES
|
constraints: karpathy-guidelines,.agents,AGENT_RULES
|
||||||
|
|||||||
Reference in New Issue
Block a user