Files
tsl-devkit/docs/superpowers/specs/2026-07-13-lsp-core-lifecycle-design.md
T

13 KiB
Raw Blame History

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 或任何兼容适配层。

目标

  • 用显式状态机约束 initializeshutdownexit
  • 正常关闭等待 exit,并向进程入口返回准确退出码。
  • 让输入线程在普通请求执行期间继续读取取消通知。
  • 按请求实例精确取消,区分整数 ID 与字符串 ID。
  • 为请求解析、状态、Provider 异常和取消生成规范 JSON-RPC 错误。
  • 让 Dispatcher 构造后立即有效,不存在后注入依赖或伪响应。
  • 严格验证 LSP framing,遇到不可恢复的传输错误立即终止。
  • 删除已无必要的生命周期 callback 及其锁内调用路径。
  • 将诊断位置转换为 LSP UTF-16 坐标。
  • 删除无调用函数、重复错误构造和无效兜底代码。

非目标

  • 不兼容旧的 LspServer::Run()、Dispatcher 默认构造或依赖 setter。
  • 不接受非 LSP 3.17 framing,也不尝试从流错位中恢复。
  • 不为已删除的 shutdownexitcancelRequest Provider 保留转发层。
  • 不保证强制中断任意 Provider;运行中取消仍遵循 std::stop_token 的协作式 语义。
  • 不启用当前关闭的诊断发布能力,只保证其坐标在以后启用时正确。

生命周期所有权

生命周期只由 LspServer 管理,不再由 Provider 通过 callback 间接修改。 删除 ServerLifecycleEventLifecycleCallback、Dispatcher 的生命周期 callback 注册与通知,以及 ExecutionContext::TriggerLifecycleEventinitialize 的 Provider 只负责初始化参数、manager 状态和能力响应;shutdownexit$/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 路径执行相同的任务收尾,但不把它误报为正常 关闭。

请求执行与取消

initializeshutdown 在输入线程同步执行,因为它们改变全局生命周期。 普通请求提交给 AsyncExecutorexit$/cancelRequest 是 core 控制消息, 在输入线程立即处理;其他通知仍按输入顺序同步分发,避免 didOpendidChange 与后续请求发生人为重排。

请求键保留 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&LspServerManagerHubAsyncExecutor、Dispatcher 的依赖顺序声明和构造 成员。删除默认构造、SetRequestSchedulerSetManagerHub、空指针检查和 "{}" 返回值。

Dispatcher 只负责查找 Provider、创建 ExecutionContext 并调用 Provider

  • 未注册请求生成 MethodNotFound
  • 未注册通知只记录日志。
  • Provider 异常不在 Dispatcher 中吞掉,交给请求任务 callback 转换为 InternalError;通知入口捕获异常并只记录日志。

错误响应由一个公共构造函数生成,接收可选 Request ID、错误码和消息。 Provider 与 server 共用该实现,删除 SendErrorBuildErrorResponseMessage 的重复组装逻辑。已识别请求回显其 ID;无法识别 ID 时序列化为 null

错误响应自身序列化失败视为内部不变量破坏并抛出,不返回 "{}"、手写固定 JSON 或空字符串。外层只在能够构造规范响应时继续运行,不增加第二套序列化 兜底。

JSON-RPC 消息验证

HandleMessage 先区分 JSON 语法错误与消息结构错误:

输入 处理
非法 JSON ParseErrorID 为 null
合法 JSON,但顶层不是对象 InvalidRequestID 为 null
JSON 对象但不满足 JSON-RPC 2.0 消息结构 InvalidRequest
请求可识别 ID,但 Request 反序列化失败 InvalidRequest,回显有效 ID
合法请求的 Provider 抛异常 InternalError,回显请求 ID
合法通知的 Provider 抛异常 只记录,不发送响应
合法客户端响应 记录或交给现有响应入口,不回送响应

请求、通知和响应必须显式携带 "jsonrpc":"2.0"。Request ID 只接受协议已 定义的整数或字符串。响应必须恰好包含 resulterror 之一。无法可靠 分类为请求的非法客户端响应不触发响应,避免形成 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 中 维护第二套编码逻辑。

清理范围

  • 删除 RequiresSyncProcessingCanProcessRequest,状态策略只保留一个 实际调用入口。
  • 删除 Dispatcher 生命周期 callback 及其 mutex。
  • 删除不再注册且由 core 直接处理的 provider/shutdownprovider/exitprovider/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 兜底。
  • 相关构建和测试通过,并完成一次针对无效兜底与临时兼容层的专项复审。