13 KiB
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、cancelRequestProvider 保留转发层。 - 不保证强制中断任意 Provider;运行中取消仍遵循
std::stop_token的协作式 语义。 - 不启用当前关闭的诊断发布能力,只保证其坐标在以后启用时正确。
生命周期所有权
生命周期只由 LspServer 管理,不再由 Provider 通过 callback 间接修改。
删除 ServerLifecycleEvent、LifecycleCallback、Dispatcher 的生命周期 callback
注册与通知,以及 ExecutionContext::TriggerLifecycleEvent。initialize 的
Provider 只负责初始化参数、manager 状态和能力响应;shutdown、exit 与
$/cancelRequest 都由 core 直接处理,不再注册为 Provider。
服务器使用以下状态:
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_requestModule,并同步 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 兜底。 - 相关构建和测试通过,并完成一次针对无效兜底与临时兼容层的专项复审。