38 KiB
LSP Core Lifecycle Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 将 LSP core 改为严格的生命周期状态机、可取消异步请求、规范 JSON-RPC 错误和不可恢复 framing,并移除旧 Provider/兜底路径。
Architecture: LspServer 独占生命周期和控制消息,Dispatcher 在构造时绑定 manager/scheduler,仅负责 Provider 路由。生产入口与测试入口分别注入 Provider 注册器;测试入口仍运行同一个 server 和 stdio framing,以确定性阻塞/异常 Provider 验证异步取消和错误响应。
Tech Stack: C++23 Modules、Glaze、Taskflow/AsyncExecutor、LSP 3.17、Tree-sitter、CMake/CTest、Python 3
Plan Meta
- Plan Group:
lsp-core-lifecycle - Parent Plan: none
- Verification Scope:
tsl-server、test_provider、test_scheduler、test_core_server、test_cli_startup、LSP JSON transport smoke - Verification Gate: 目标全部构建成功;上述 CTest 全部通过;core 无生命周期 bool、取消空分支、
"{}"/固定 JSON 兜底或已删除 Provider 引用 - Executor:
executing-plans - Constraints:
karpathy-guidelines、.agents、AGENT_RULES.md、不保留兼容层、所有提交使用规范 emoji - Worktree Note: 当前工作区已有用户改动;保留所有无关修改。
lsp-server/src/cli/launcher.cppm已有 namespace/args_parser 改动,提交时只暂存本 Plan 新增的 registrar 和Run()退出码 hunk。
Task 1: 建立 core 生命周期所有权和可测试服务器入口
Files:
-
Create:
lsp-server/test/test_provider/core_server_fixture.cppm -
Create:
lsp-server/test/test_core_server.py -
Modify:
lsp-server/src/core/dispatcher.cppm -
Modify:
lsp-server/src/core/server.cppm -
Modify:
lsp-server/src/cli/launcher.cppm -
Modify:
lsp-server/src/provider/base/interface.cppm -
Modify:
lsp-server/src/provider/initialize/initialize.cppm -
Modify:
lsp-server/src/provider/manifest.cppm -
Delete:
lsp-server/src/provider/shutdown/shutdown.cppm -
Delete:
lsp-server/src/provider/exit/exit.cppm -
Delete:
lsp-server/src/provider/cancel_request/cancel_request.cppm -
Modify:
lsp-server/test/test_provider/test_main.cppm -
Modify:
lsp-server/test/test_provider/provider_misc_test.cppm -
Modify:
lsp-server/test/test_provider/provider_surface_test.cppm -
Modify:
lsp-server/test/test_provider/json_provider_coverage_test.cppm -
Modify:
lsp-server/test/test_provider/CMakeLists.txt -
Modify:
lsp-server/test/CMakeLists.txt -
Step 1: 写生命周期 RED 测试和测试服务器模式
在 core_server_fixture.cppm 定义只用于测试进程的初始化 Provider,并通过正式
LspServer 运行:
export module lsp.test.provider.core_server_fixture;
import std;
import spdlog;
import lsp.codec.facade;
import lsp.core.server;
import lsp.provider.base.interface;
import lsp.protocol;
export namespace lsp::test::provider
{
int RunCoreServerFixture();
}
namespace lsp::test::provider
{
class FixtureInitialize final : public core::IRequestProvider
{
public:
std::string GetMethod() const override { return "initialize"; }
std::string GetProviderName() const override { return "FixtureInitialize"; }
std::string ProvideResponse(const protocol::RequestMessage& request,
core::ExecutionContext&) override
{
protocol::ResponseMessage response;
response.id = request.id;
response.result = protocol::LSPAny(protocol::LSPObject{});
return codec::Serialize(response).value();
}
};
int RunCoreServerFixture()
{
spdlog::set_level(spdlog::level::off);
core::LspServer server(
std::cin,
std::cout,
[](core::RequestDispatcher& dispatcher) {
dispatcher.RegisterRequestProvider(
std::make_shared<FixtureInitialize>());
},
2,
"");
return server.Run();
}
}
在 test_main.cppm 的正常测试输出前处理该模式:
if (arg == "--core-server-fixture")
return lsp::test::provider::RunCoreServerFixture();
在 test_core_server.py 添加 framing 工具和生命周期用例:
def frame(message: dict) -> bytes:
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
def read_messages(data: bytes) -> list[dict]:
messages = []
offset = 0
while offset < len(data):
header_end = data.find(b"\r\n\r\n", offset)
assert header_end >= 0
header = data[offset:header_end].decode("ascii")
fields = dict(line.split(": ", 1) for line in header.split("\r\n"))
length = int(fields["Content-Length"])
body_start = header_end + 4
body_end = body_start + length
assert body_end <= len(data)
messages.append(json.loads(data[body_start:body_end].decode("utf-8")))
offset = body_end
return messages
def response_by_id(data: bytes, request_id: int | str) -> dict:
return next(message for message in read_messages(data)
if message.get("id") == request_id)
def run_raw(server: Path, payload: bytes) -> subprocess.CompletedProcess[bytes]:
return subprocess.run(
[str(server), "--core-server-fixture"],
input=payload,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5,
check=False,
)
def run_batch(server: Path, messages: list[dict]) -> subprocess.CompletedProcess[bytes]:
payload = b"".join(frame(message) for message in messages)
return run_raw(server, payload)
class LspClient:
def __init__(self, server: Path) -> None:
self.proc = subprocess.Popen(
[str(server), "--core-server-fixture"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.buffer = b""
def send(self, message: dict) -> None:
assert self.proc.stdin is not None
self.proc.stdin.write(frame(message))
self.proc.stdin.flush()
def read(self, timeout: float = 1.0) -> dict:
assert self.proc.stdout is not None
deadline = time.monotonic() + timeout
while True:
header_end = self.buffer.find(b"\r\n\r\n")
if header_end >= 0:
header = self.buffer[:header_end].decode("ascii")
fields = dict(line.split(": ", 1)
for line in header.split("\r\n"))
length = int(fields["Content-Length"])
body_start = header_end + 4
body_end = body_start + length
if len(self.buffer) >= body_end:
body = self.buffer[body_start:body_end]
self.buffer = self.buffer[body_end:]
return json.loads(body.decode("utf-8"))
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("timed out waiting for LSP response")
ready, _, _ = select.select(
[self.proc.stdout.fileno()], [], [], remaining)
if not ready:
continue
chunk = os.read(self.proc.stdout.fileno(), 4096)
if not chunk:
raise RuntimeError("fixture closed stdout before a response")
self.buffer += chunk
def close_input(self) -> int:
assert self.proc.stdin is not None
self.proc.stdin.close()
return self.proc.wait(timeout=5)
def assert_lifecycle(server: Path) -> None:
before_init = run_batch(server, [
{"jsonrpc": "2.0", "id": 1, "method": "shutdown"},
{"jsonrpc": "2.0", "method": "exit"},
])
assert before_init.returncode == 1
assert response_by_id(before_init.stdout, 1)["error"]["code"] == -32002
repeated = run_batch(server, [
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
{"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {}},
{"jsonrpc": "2.0", "id": 3, "method": "shutdown"},
{"jsonrpc": "2.0", "method": "exit"},
])
assert repeated.returncode == 0
assert response_by_id(repeated.stdout, 2)["error"]["code"] == -32600
assert response_by_id(repeated.stdout, 3)["result"] is None
direct_exit = run_batch(
server, [{"jsonrpc": "2.0", "method": "exit"}])
assert direct_exit.returncode == 1
client = LspClient(server)
client.send({"jsonrpc": "2.0", "id": 11,
"method": "initialize", "params": {}})
assert client.read()["id"] == 11
client.send({"jsonrpc": "2.0", "id": 12, "method": "shutdown"})
assert client.read()["result"] is None
assert client.proc.poll() is None
client.send({"jsonrpc": "2.0", "method": "exit"})
assert client.close_input() == 0
脚本导入 argparse、json、os、select、subprocess、time 和 Path,
解析必填 --server 后调用 assert_lifecycle(Path(args.server)) 并返回 0。响应
解析严格按 Content-Length 切分,不按换行猜测 JSON 边界。
在 test_provider/CMakeLists.txt 的 source 和 Module file-set 中加入 fixture、
server.cppm、bridge/win32_stdio.cppm、manager/bootstrap.cppm;在父级
test/CMakeLists.txt 的 Python 分支注册:
add_test(NAME test_core_server
COMMAND ${PYTHON3_EXECUTABLE}
${CMAKE_CURRENT_LIST_DIR}/test_core_server.py
--server $<TARGET_FILE:test_provider>)
- Step 2: 运行测试并确认 RED
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
Expected: FAIL,LspServer 还没有 stream/registrar 构造函数且 Run() 仍返回
void;失败来自新生命周期接口,而不是 Python 语法错误。
- Step 3: 收紧 Dispatcher 构造与错误响应接口
将 Dispatcher 改为构造即有效:
class ExecutionContext
{
public:
ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler,
manager::ManagerHub& manager_hub)
: async_executor_(scheduler), manager_hub_(manager_hub) {}
scheduler::async_executor::AsyncExecutor& GetScheduler() const;
manager::ManagerHub& GetManagerHub() const;
private:
scheduler::async_executor::AsyncExecutor& async_executor_;
manager::ManagerHub& manager_hub_;
};
class RequestDispatcher
{
public:
RequestDispatcher(scheduler::async_executor::AsyncExecutor& scheduler,
manager::ManagerHub& manager_hub);
private:
scheduler::async_executor::AsyncExecutor& async_executor_;
manager::ManagerHub& manager_hub_;
};
删除 ServerLifecycleEvent、LifecycleCallback、两个 setter、callback 注册/通知
和所有 nullable dependency 分支。错误构造只保留一套实现:
std::string BuildErrorResponseMessage(
std::optional<protocol::RequestId> id,
protocol::ErrorCodes code,
std::string_view message)
{
protocol::ResponseMessage response;
response.id = std::move(id);
response.error = protocol::ResponseError{
.jsonrpc = "2.0",
.code = static_cast<protocol::integer>(code),
.message = std::string(message),
.data = std::nullopt,
};
return transform::Serialize(response).value();
}
std::string BuildErrorResponseMessage(
const protocol::RequestMessage& request,
protocol::ErrorCodes code,
std::string_view message)
{
return BuildErrorResponseMessage(request.id, code, message);
}
HandleUnknownRequest 调用该函数;序列化失败直接抛出,不返回 "{}" 或固定
JSON。
- Step 4: 实现 server 状态机和控制消息
增加可注入注册器、stream 引用和显式状态:
using ProviderRegistrar = std::function<void(RequestDispatcher&)>;
enum class ServerState
{
kUninitialized,
kRunning,
kShutdownRequested,
kExiting,
};
LspServer(std::istream& input,
std::ostream& output,
ProviderRegistrar registrar,
std::size_t concurrency,
std::string interpreter_path);
int Run();
成员按依赖顺序声明:
std::istream& input_;
std::ostream& output_;
manager::ManagerHub manager_hub_;
scheduler::async_executor::AsyncExecutor async_executor_;
RequestDispatcher dispatcher_;
ServerState state_ = ServerState::kUninitialized;
int exit_code_ = 1;
std::atomic<bool> fatal_io_error_ = false;
请求状态策略集中在 HandleRequest:
if (request.method == "initialize")
{
if (state_ != ServerState::kUninitialized)
return SendError(request, protocol::ErrorCodes::InvalidRequest,
"Server is already initialized");
const auto response = dispatcher_.Dispatch(request);
SendMessage(response);
const auto parsed = transform::Deserialize<protocol::ResponseMessage>(response);
if (!parsed || parsed->error)
return;
state_ = ServerState::kRunning;
return;
}
if (request.method == "shutdown")
{
if (state_ == ServerState::kUninitialized)
return SendError(request, protocol::ErrorCodes::ServerNotInitialized,
"Server not initialized");
if (state_ != ServerState::kRunning)
return SendError(request, protocol::ErrorCodes::InvalidRequest,
"Shutdown already requested");
async_executor_.WaitAll();
manager_hub_.Shutdown();
protocol::ResponseMessage response;
response.id = request.id;
response.result = protocol::LSPAny(std::nullptr_t{});
SendMessage(transform::Serialize(response).value());
state_ = ServerState::kShutdownRequested;
return;
}
生命周期控制分支之后,普通请求只允许在 kRunning:
if (state_ == ServerState::kUninitialized)
{
SendError(request, protocol::ErrorCodes::ServerNotInitialized,
"Server not initialized");
return;
}
if (state_ == ServerState::kShutdownRequested)
{
SendError(request, protocol::ErrorCodes::InvalidRequest,
"Server is shutting down");
return;
}
SendMessage(dispatcher_.Dispatch(request));
notification 策略只有一个入口:exit 在任何状态都终止;kUninitialized 忽略
所有其他通知;kShutdownRequested 只允许 exit;kRunning 才 dispatch
普通通知。Task 3 再在普通通知之前插入 $/cancelRequest 控制分支。
SendMessage 使用注入的 output stream,并把写失败标为 fatal:
void LspServer::SendMessage(const std::string& message)
{
std::lock_guard lock(output_mutex_);
output_ << "Content-Length: " << message.size() << "\r\n\r\n"
<< message << std::flush;
if (!output_)
{
fatal_io_error_ = true;
throw std::runtime_error("Failed to write LSP message");
}
}
主循环每次读取前检查 fatal_io_error_ 并以 1 结束。同步写异常传播到 launcher;
异步 callback 写异常由 executor 记录,但 fatal flag 会在主循环下一控制点生效。
exit 不再 dispatch:保存旧状态,设置 kExiting,只有旧状态为
kShutdownRequested 时把 exit_code_ 设为 0。EOF 保持 1。删除析构函数中的
状态写入、主循环 sleep、RequiresSyncProcessing、CanProcessRequest 和
生命周期事件入口。
- Step 5: 删除 lifecycle Provider 和迁移调用点
删除 shutdown/exit/cancel_request 三个 Module。AllProviders 移除 Shutdown;
Initialize::ProvideResponse 删除事件触发,只在序列化失败时抛出。Provider
bridge 只保留实际使用的接口别名:
using ExecutionContext = lsp::core::ExecutionContext;
using IProvider = lsp::core::IProvider;
using IRequestProvider = lsp::core::IRequestProvider;
using INotificationProvider = lsp::core::INotificationProvider;
using lsp::core::BuildErrorResponseMessage;
测试环境统一改为:
struct ProviderEnv
{
scheduler::async_executor::AsyncExecutor scheduler{1};
manager::ManagerHub hub{};
core::RequestDispatcher dispatcher{scheduler, hub};
core::ExecutionContext context{scheduler, hub};
};
删除 provider misc/surface 中三个已删除 Provider 的 import、声明、注册、子进程 入口和断言;Initialize 测试只验证响应与 manager 状态。同步移除 test CMake 中的三个 Module。
launcher 显式注入正式注册器并返回 server 退出码:
core::LspServer server(
std::cin,
std::cout,
provider::RegisterAllProviders,
config.thread_count,
config.interpreter_path);
const int exit_code = server.Run();
spdlog::shutdown();
return exit_code;
- Step 6: 运行 GREEN 验证
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
Expected: build PASS;生命周期用例 PASS,shutdown 响应含 result: null,正常
关闭返回 0,异常退出返回 1。
- Step 7: 提交生命周期改动
只暂存本 Task 文件;launcher 使用逐 hunk 暂存,保留原有 namespace/args_parser 改动。检查后提交:
git diff --cached --check
git commit -m ":bug: fix(core): enforce strict LSP lifecycle"
Task 2: 严格分类 JSON-RPC 消息并统一请求错误
Files:
-
Modify:
lsp-server/test/test_core_server.py -
Modify:
lsp-server/src/core/server.cppm -
Modify:
lsp-server/src/core/dispatcher.cppm -
Step 1: 写 ParseError、InvalidRequest 和 MethodNotFound RED 测试
新增批量原始消息测试:
def raw_frame(body: bytes) -> bytes:
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
def assert_json_rpc_errors(server: Path) -> None:
payload = b"".join([
raw_frame(b'{"jsonrpc":"2.0",'),
raw_frame(b'[]'),
raw_frame(b'{"jsonrpc":"1.0","id":7,"method":"initialize"}'),
frame({"jsonrpc": "2.0", "id": 8, "method": "initialize", "params": {}}),
frame({"jsonrpc": "2.0", "id": 9, "method": "missing/method"}),
frame({"jsonrpc": "2.0", "id": 10, "method": "shutdown"}),
frame({"jsonrpc": "2.0", "method": "exit"}),
])
result = run_raw(server, payload)
assert result.returncode == 0
responses = read_messages(result.stdout)
assert responses[0]["error"]["code"] == -32700
assert responses[0]["id"] is None
assert responses[1]["error"]["code"] == -32600
assert response_by_id(result.stdout, 7)["error"]["code"] == -32600
assert response_by_id(result.stdout, 9)["error"]["code"] == -32601
把 assert_json_rpc_errors(server) 加入脚本 main(),确保 CTest 实际执行新用例。
- Step 2: 运行测试并确认 RED
Run:
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
Expected: FAIL,非法 JSON/结构当前只写日志,没有对应响应。
- Step 3: 实现严格消息分类
HandleMessage 按以下顺序处理:
auto any = transform::Deserialize<protocol::LSPAny>(raw_message);
if (!any)
{
SendError(std::nullopt, protocol::ErrorCodes::ParseError, "Parse error");
return;
}
if (!any->Is<protocol::LSPObject>())
{
SendError(std::nullopt, protocol::ErrorCodes::InvalidRequest,
"Invalid Request");
return;
}
const auto& object = any->Get<protocol::LSPObject>();
const auto jsonrpc = object.find("jsonrpc");
if (jsonrpc == object.end() || !jsonrpc->second.Is<protocol::string>() ||
jsonrpc->second.Get<protocol::string>() != "2.0")
{
SendError(ExtractRequestId(object), protocol::ErrorCodes::InvalidRequest,
"Invalid Request");
return;
}
ExtractRequestId 只接受 integer/string:
std::optional<protocol::RequestId> ExtractRequestId(
const protocol::LSPObject& object)
{
const auto id = object.find("id");
if (id == object.end())
return std::nullopt;
if (id->second.Is<protocol::integer>())
return protocol::RequestId{id->second.Get<protocol::integer>()};
if (id->second.Is<protocol::string>())
return protocol::RequestId{id->second.Get<protocol::string>()};
return std::nullopt;
}
method 必须是 string;有 method+id 是
request,无 id 是 notification;response 必须有 id 且 result/error 恰好一
个。不可分类的 response 只记录,不发送 response-to-response。
SendError 只负责调用公共 BuildErrorResponseMessage 后 SendMessage,不再
自己构造 ResponseMessage。
- Step 4: 运行 GREEN 验证并提交
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
Expected: PASS;错误码和 ID 与测试一致。
Commit:
git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_core_server.py
git diff --cached --check
git commit -m ":bug: fix(core): return JSON-RPC protocol errors"
Task 3: 异步执行普通请求并精确取消
Files:
-
Modify:
lsp-server/test/test_provider/core_server_fixture.cppm -
Modify:
lsp-server/test/test_core_server.py -
Modify:
lsp-server/src/core/dispatcher.cppm -
Modify:
lsp-server/src/core/server.cppm -
Step 1: 增加阻塞、异常 Provider 和取消 RED 测试
Fixture 注册两个请求 Provider 和一个通知 Provider:
class FixtureBlock final : public core::IRequestProvider
{
public:
std::string GetMethod() const override { return "test/block"; }
std::string GetProviderName() const override { return "FixtureBlock"; }
std::string ProvideResponse(const protocol::RequestMessage& request,
core::ExecutionContext& context) override
{
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (!context.GetStopToken().stop_requested() &&
std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
protocol::ResponseMessage response;
response.id = request.id;
response.result = protocol::LSPAny(protocol::string("completed"));
return codec::Serialize(response).value();
}
};
class FixtureThrow final : public core::IRequestProvider
{
public:
std::string GetMethod() const override { return "test/throw"; }
std::string GetProviderName() const override { return "FixtureThrow"; }
std::string ProvideResponse(const protocol::RequestMessage&,
core::ExecutionContext&) override
{
throw std::runtime_error("fixture request failure");
}
};
class FixtureThrowNotification final : public core::INotificationProvider
{
public:
std::string GetMethod() const override { return "test/throwNotification"; }
std::string GetProviderName() const override
{
return "FixtureThrowNotification";
}
void HandleNotification(const protocol::NotificationMessage&,
core::ExecutionContext&) override
{
throw std::runtime_error("fixture notification failure");
}
};
registrar 中加入:
dispatcher.RegisterRequestProvider(std::make_shared<FixtureBlock>());
dispatcher.RegisterRequestProvider(std::make_shared<FixtureThrow>());
dispatcher.RegisterNotificationProvider(
std::make_shared<FixtureThrowNotification>());
Python 交互测试必须覆盖:
client = LspClient(server)
client.send({"jsonrpc": "2.0", "id": 1,
"method": "initialize", "params": {}})
assert client.read()["id"] == 1
client.send({"jsonrpc": "2.0", "id": 2, "method": "test/block"})
client.send({"jsonrpc": "2.0", "id": "2", "method": "test/block"})
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
"params": {"id": 2}})
cancelled = client.read(timeout=1)
assert cancelled["id"] == 2
assert cancelled["error"]["code"] == -32800
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
"params": {"id": "2"}})
string_cancelled = client.read(timeout=1)
assert string_cancelled["id"] == "2"
assert string_cancelled["error"]["code"] == -32800
client.send({"jsonrpc": "2.0", "id": 3, "method": "test/throw"})
failed = client.read(timeout=1)
assert failed["error"]["code"] == -32603
client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"})
client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"})
duplicate = client.read(timeout=1)
assert duplicate["id"] == 4
assert duplicate["error"]["code"] == -32600
client.send({"jsonrpc": "2.0", "method": "$/cancelRequest",
"params": {"id": 4}})
cancelled_original = client.read(timeout=1)
assert cancelled_original["error"]["code"] == -32800
client.send({"jsonrpc": "2.0", "method": "test/throwNotification"})
client.send({"jsonrpc": "2.0", "id": 5, "method": "shutdown"})
assert client.read(timeout=1)["id"] == 5
client.send({"jsonrpc": "2.0", "method": "exit"})
assert client.close_input() == 0
把该交互过程封装为 assert_cancellation_and_failures(server) 并加入脚本
main()。
- Step 2: 运行测试并确认 RED
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
Expected: FAIL;GetStopToken 尚不存在,或同步 block 请求使取消响应超过 1 秒。
- Step 3: 向 ExecutionContext 传递 stop token
ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler,
manager::ManagerHub& manager_hub,
std::stop_token stop_token = {})
: async_executor_(scheduler),
manager_hub_(manager_hub),
stop_token_(stop_token) {}
std::stop_token GetStopToken() const { return stop_token_; }
RequestDispatcher::Dispatch 改为接收默认值为空的 std::stop_token 并用它
构造 context;同步 initialize 和 notification 使用空 token。
- Step 4: 实现类型安全请求表和异步 callback
server 增加:
struct ActiveRequest
{
scheduler::async_executor::TaskHandle handle;
};
std::mutex requests_mutex_;
std::unordered_map<std::string, std::shared_ptr<ActiveRequest>> active_requests_;
请求键必须带类型:
std::string RequestKey(const protocol::RequestId& id)
{
return std::visit([](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, protocol::integer>)
return "i:" + std::to_string(value);
else
return "s:" + value;
}, id);
}
SubmitRequest 先登记 shared state,解锁后 Submit,再绑定 handle:
auto state = std::make_shared<ActiveRequest>();
bool duplicate = false;
{
std::lock_guard lock(requests_mutex_);
if (active_requests_.contains(key))
duplicate = true;
else
active_requests_.emplace(key, state);
}
if (duplicate)
{
SendError(request, protocol::ErrorCodes::InvalidRequest,
"Duplicate active request id");
return;
}
state->handle = async_executor_.Submit(
"lsp-request:" + key,
[this, request](std::stop_token stop_token) -> std::optional<std::string> {
return dispatcher_.Dispatch(request, stop_token);
},
[this, request, key, state](const auto& result) {
FinishRequest(request, key, state, result);
});
FinishRequest 先仅当映射仍指向同一 state 时删除登记并释放请求表锁,再对
completed/cancelled/failed 分别发送结果、RequestCancelled、InternalError。
这样输出异常不会留下陈旧登记,callback 也不会在请求表锁内发送消息。
HandleCancelRequest 严格读取 CancelParams,复制 handle 后解锁并调用
Cancel()。DrainActiveRequests 锁内复制 handles、锁外 cancel/wait;shutdown、
exit、EOF/framing 收尾都调用它。shutdown 随后调用
async_executor_.WaitAll(),保证 initialize/Provider 提交的后台任务也在
ManagerHub::Shutdown() 前结束。
- Step 5: 捕获 notification 异常并运行 GREEN
notification dispatch 外围只记录异常:
try
{
dispatcher_.Dispatch(notification);
}
catch (const std::exception& error)
{
spdlog::error("Notification {} failed: {}",
notification.method, error.what());
}
catch (...)
{
spdlog::error("Notification {} failed with unknown exception",
notification.method);
}
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server)$' --output-on-failure
Expected: PASS;两种 ID 分别取消,异常请求返回 -32603,异常通知不终止进程。
- Step 6: 提交异步取消改动
git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_core_server.py
git diff --cached --check
git commit -m ":bug: fix(core): dispatch cancellable requests asynchronously"
Task 4: 严格验证 LSP framing
Files:
-
Modify:
lsp-server/test/test_core_server.py -
Modify:
lsp-server/src/core/server.cppm -
Step 1: 写 framing RED 测试
对每个 payload 启动独立 fixture 进程并要求非零退出、stdout 无 LSP 响应:
invalid_payloads = [
b"Content-Length: 2junk\r\n\r\n{}",
b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}",
b"Content-Length: 0\r\n\r\n",
b"Content-Length: 16777217\r\n\r\n",
b"Content-Length: 10\r\n\r\n{}",
b"Content-Length: 2\n\n{}",
b"X-Length: 2\r\n\r\n{}",
b"Content-Length: 2\r\nContent-Type: application/json\r\n\r\n{}",
]
for payload in invalid_payloads:
result = run_raw(server, payload)
assert result.returncode == 1
assert result.stdout == b""
另加规范 Content-Type: application/vscode-jsonrpc; charset=utf-8 和连续两条
消息的通过用例,并把 assert_framing(server) 加入脚本 main()。
- Step 2: 运行测试并确认 RED
Run:
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure
Expected: FAIL;部分数字、重复长度或 LF-only header 被旧读取器接受。
- Step 3: 用明确读取结果和 from_chars 实现严格 framing
enum class ReadStatus
{
kMessage,
kEndOfStream,
kFramingError,
};
struct ReadResult
{
ReadStatus status;
std::string message;
};
static constexpr std::size_t kMaxMessageSize = 16U * 1024U * 1024U;
每个 header 行必须以 \r 结束;移除该字符后解析。Content-Length: 后的值
非空,from_chars 必须满足 error == std::errc{} 且 pointer 到达字符串末尾。
只允许一个 Content-Length 和最多一个精确规范 Content-Type。未知 header、空
header、零/超限长度和短 body 返回 kFramingError。
主循环处理:
const auto read = ReadMessage();
if (read.status != ReadStatus::kMessage)
{
exit_code_ = 1;
break;
}
HandleMessage(read.message);
删除 stoul、nullopt 重试、5ms sleep 和 framing 错误后的继续读取。
- Step 4: 运行 GREEN 并提交
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate
Expected: PASS;非法 framing 全部非零退出且无响应,规范连续消息通过。
Commit:
git add lsp-server/src/core/server.cppm lsp-server/test/test_core_server.py
git diff --cached --check
git commit -m ":bug: fix(core): reject invalid LSP framing"
Task 5: 将 diagnostics 字节列转换为 UTF-16
Files:
-
Modify:
lsp-server/test/test_provider/text_coordinates_test.cppm -
Modify:
lsp-server/src/utils/text_coordinates.cppm -
Modify:
lsp-server/src/core/server.cppm -
Step 1: 写反向坐标 RED 测试
TestResult TextCoordinatesTests::TestBytePointsToUtf16Positions()
{
const protocol::string content = "A中😀Z\n😀x";
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 0U}, content),
0U, 0U);
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 4U}, content),
0U, 2U);
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 8U}, content),
0U, 4U);
ExpectLspPosition(utils::text_coordinates::ToPosition({1U, 4U}, content),
1U, 2U);
ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 6U}, content),
0U, 2U);
return {"", true, "ok"};
}
在测试类声明和注册入口分别加入:
static TestResult TestBytePointsToUtf16Positions();
runner.addTest("text coordinates convert byte points to UTF-16 positions",
TestBytePointsToUtf16Positions);
测试 helper 只比较字段:
void ExpectLspPosition(const protocol::Position& actual,
protocol::uinteger line,
protocol::uinteger character)
{
assertEqual(line, actual.line, "LSP line should match");
assertEqual(character, actual.character, "LSP UTF-16 character should match");
}
- Step 2: 运行测试并确认 RED
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1
Expected: FAIL,ToPosition(TSPoint, string) 尚未定义。
- Step 3: 实现单一 UTF-8 解码路径的反向转换
导出:
protocol::Position ToPosition(TSPoint point,
const protocol::string& content);
实现先扫描到 point.row 行首,再逐个调用已有 DecodeCharacter。只有完整字符的
末尾不超过目标 byte column 时才累计 utf16_units;目标落在字符内部时停止,
超出行尾时收敛到行尾。返回实际行号和累计 UTF-16 units,不复制第二套 UTF-8
判定代码。
PublishDiagnostics 转换起止点:
diagnostic.range.start = utils::text_coordinates::ToPosition(
TSPoint{error.location.start_line, error.location.start_column}, content);
diagnostic.range.end = utils::text_coordinates::ToPosition(
TSPoint{error.location.end_line, error.location.end_column}, content);
- Step 4: 运行 GREEN 并提交
Run:
cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure
Expected: PASS;中文和 emoji 坐标符合 UTF-16。
Commit:
git add lsp-server/src/utils/text_coordinates.cppm lsp-server/src/core/server.cppm lsp-server/test/test_provider/text_coordinates_test.cppm
git diff --cached --check
git commit -m ":bug: fix(core): convert diagnostics to UTF-16 positions"
Task 6: 格式化、全量验证和无效兜底专项复审
Files:
-
Modify:
memory-bank/progress.md -
Format: 本 Plan 已修改的 C++ 文件
-
Step 1: 格式化本 Plan 修改的 C++ 文件
Run:
clang-format -i lsp-server/src/core/dispatcher.cppm lsp-server/src/core/server.cppm lsp-server/src/provider/base/interface.cppm lsp-server/src/provider/initialize/initialize.cppm lsp-server/src/provider/manifest.cppm lsp-server/src/utils/text_coordinates.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_provider/test_main.cppm lsp-server/test/test_provider/provider_misc_test.cppm lsp-server/test/test_provider/provider_surface_test.cppm lsp-server/test/test_provider/json_provider_coverage_test.cppm lsp-server/test/test_provider/text_coordinates_test.cppm
检查格式化没有改动用户的无关 hunk;若有,恢复那些 hunk 到格式化前内容,仅 保留本 Plan 行。
- Step 2: 构建所有影响目标
Run:
cmake --build lsp-server/build/codex43-clean/Release --target tsl-server test_scheduler test_provider -j1
Expected: exit 0,无编译错误。
- Step 3: 运行完整相关测试门
Run:
ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server|test_cli_startup)$' --output-on-failure
python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate
Expected: 4/4 CTest PASS,LSP transport smoke exit 0。已知
test_ast_script、test_symbol_script、test_semantic_script 以及 rename
结果校验不在本 Plan gate 中。
- Step 4: 专项搜索无效兜底、兼容层和死代码
Run:
rg -n 'return "\{\}"|value_or\("\{\}"\)|TO[D]O|RequiresSyncProcessing|CanProcessRequest|SetRequestScheduler|SetManagerHub|ServerLifecycleEvent|LifecycleCallback|provider\.(shutdown|exit|cancel_request)' lsp-server/src/core lsp-server/src/provider lsp-server/test/test_provider
Expected: 无命中。再分别检查生产和测试 sleep:
rg -n 'sleep_for' lsp-server/src/core
rg -n 'sleep_for' lsp-server/test/test_provider/core_server_fixture.cppm
Expected: core 无命中;fixture 恰好命中确定性阻塞 Provider。再运行:
rg -n 'catch\s*\(\.\.\.\)|catch\s*\(const std::exception' lsp-server/src/core
Expected: 每个 catch 都能对应“请求转 InternalError”“通知只记录”或“主循环致命 错误退出”,不存在捕获后 sleep/继续的分支。
- Step 5: 检查最终 diff 和提交遗漏
Run:
git diff --check
git status --short
git log -6 --oneline
Expected: 本 Plan 产品/测试改动已提交;用户原有未提交文件仍存在且未混入提交; 新增提交均带匹配 type 的 emoji。
若格式化或复审产生必要修正,先重新执行 Step 2/3,再提交:
git commit -m ":art: style(core): normalize lifecycle implementation"
- Step 6: 写回主循环状态
Run:
python docs/standards/playbook/scripts/main_loop.py finish -plan docs/superpowers/plans/2026-07-13-lsp-core-lifecycle.md -status done -progress memory-bank/progress.md
更新 memory-bank/progress.md 上半部分摘要,记录生命周期状态机、请求取消、
framing、UTF-16 diagnostics 和验证结果;只暂存该文件并提交:
git add memory-bank/progress.md
git diff --cached --check
git commit -m ":memo: docs(progress): finish LSP core lifecycle plan"