From 06a0da51244c42b1cc6f6876349d076f78611dd0 Mon Sep 17 00:00:00 2001 From: csh Date: Mon, 13 Jul 2026 21:35:40 +0800 Subject: [PATCH] :bug: fix(core): enforce strict LSP lifecycle --- lsp-server/src/cli/launcher.cppm | 15 +- lsp-server/src/core/dispatcher.cppm | 236 ++++------ lsp-server/src/core/server.cppm | 431 ++++++++---------- lsp-server/src/provider/base/interface.cppm | 3 - .../cancel_request/cancel_request.cppm | 45 -- lsp-server/src/provider/exit/exit.cppm | 47 -- .../src/provider/initialize/initialize.cppm | 6 +- lsp-server/src/provider/manifest.cppm | 7 - .../src/provider/shutdown/shutdown.cppm | 53 --- lsp-server/test/CMakeLists.txt | 4 + lsp-server/test/test_core_server.py | 182 ++++++++ lsp-server/test/test_provider/CMakeLists.txt | 9 +- .../test/test_provider/completion_test.cppm | 2 +- .../test_provider/core_server_fixture.cppm | 56 +++ .../test/test_provider/definitions_test.cppm | 2 +- .../test/test_provider/interpreter_test.cppm | 3 +- .../test/test_provider/json_flow_test.cppm | 2 +- .../json_provider_coverage_test.cppm | 4 +- .../test_provider/provider_misc_test.cppm | 97 +--- .../test_provider/provider_surface_test.cppm | 10 +- lsp-server/test/test_provider/test_main.cppm | 5 +- 21 files changed, 555 insertions(+), 664 deletions(-) delete mode 100644 lsp-server/src/provider/cancel_request/cancel_request.cppm delete mode 100644 lsp-server/src/provider/exit/exit.cppm delete mode 100644 lsp-server/src/provider/shutdown/shutdown.cppm create mode 100644 lsp-server/test/test_core_server.py create mode 100644 lsp-server/test/test_provider/core_server_fixture.cppm diff --git a/lsp-server/src/cli/launcher.cppm b/lsp-server/src/cli/launcher.cppm index 0c4b735..a38e4b9 100644 --- a/lsp-server/src/cli/launcher.cppm +++ b/lsp-server/src/cli/launcher.cppm @@ -6,6 +6,7 @@ import spdlog; import std; import lsp.core.server; +import lsp.provider.manifest; import lsp.utils.args_parser; namespace @@ -46,11 +47,17 @@ export int Run(int argc, char* argv[]) return 1; } + int exit_code = 1; try { spdlog::info("TSL-LSP server starting..."); - lsp::core::LspServer server(config.thread_count, config.interpreter_path); - server.Run(); + lsp::core::LspServer server( + std::cin, + std::cout, + lsp::provider::RegisterAllProviders, + config.thread_count, + config.interpreter_path); + exit_code = server.Run(); } catch (const std::exception& error) { @@ -67,7 +74,7 @@ export int Run(int argc, char* argv[]) return 1; } - spdlog::info("TSL-LSP server stopped normally"); + spdlog::info("TSL-LSP server stopped with exit code {}", exit_code); spdlog::shutdown(); - return 0; + return exit_code; } diff --git a/lsp-server/src/core/dispatcher.cppm b/lsp-server/src/core/dispatcher.cppm index 37ca790..21b5343 100644 --- a/lsp-server/src/core/dispatcher.cppm +++ b/lsp-server/src/core/dispatcher.cppm @@ -1,29 +1,19 @@ module; export module lsp.core.dispatcher; -import spdlog; +import spdlog; import std; -import lsp.protocol.types; + import lsp.codec.facade; -import lsp.scheduler.async_executor; import lsp.manager.manager_hub; +import lsp.protocol.types; +import lsp.scheduler.async_executor; namespace transform = lsp::codec; export namespace lsp::core { - enum class ServerLifecycleEvent - { - kInitializing, - kInitialized, - kInitializeFailed, - kShuttingDown, - kShutdown - }; - - using LifecycleCallback = std::function; - class IProvider { public: @@ -32,61 +22,65 @@ export namespace lsp::core virtual std::string GetProviderName() const = 0; }; + class ExecutionContext; + class IRequestProvider : public IProvider { public: virtual ~IRequestProvider() = default; - virtual std::string ProvideResponse(const protocol::RequestMessage& request, class ExecutionContext& execution_context) = 0; + virtual std::string ProvideResponse(const protocol::RequestMessage& request, + ExecutionContext& execution_context) = 0; }; class INotificationProvider : public IProvider { public: virtual ~INotificationProvider() = default; - virtual void HandleNotification(const protocol::NotificationMessage& notification, class ExecutionContext& execution_context) = 0; + virtual void HandleNotification(const protocol::NotificationMessage& notification, + ExecutionContext& execution_context) = 0; }; class ExecutionContext { public: - ExecutionContext(LifecycleCallback lifecycle_callback, - scheduler::async_executor::AsyncExecutor& scheduler, - manager::ManagerHub& manager_hub) : - lifecycle_callback_(lifecycle_callback), - async_executor_(scheduler), - manager_hub_(manager_hub) {} - - scheduler::async_executor::AsyncExecutor& GetScheduler() const { return async_executor_; } - manager::ManagerHub& GetManagerHub() const { return manager_hub_; } - - void TriggerLifecycleEvent(ServerLifecycleEvent event) const + ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler, + manager::ManagerHub& manager_hub) + : async_executor_(scheduler), manager_hub_(manager_hub) { - if (lifecycle_callback_) - lifecycle_callback_(event); + } + + scheduler::async_executor::AsyncExecutor& GetScheduler() const + { + return async_executor_; + } + + manager::ManagerHub& GetManagerHub() const + { + return manager_hub_; } private: - LifecycleCallback lifecycle_callback_; scheduler::async_executor::AsyncExecutor& async_executor_; manager::ManagerHub& manager_hub_; }; - std::string BuildErrorResponseMessage(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message); + std::string BuildErrorResponseMessage(std::optional id, + protocol::ErrorCodes code, + std::string_view message); + std::string BuildErrorResponseMessage(const protocol::RequestMessage& request, + protocol::ErrorCodes code, + std::string_view message); class RequestDispatcher { public: - RequestDispatcher(); + RequestDispatcher(scheduler::async_executor::AsyncExecutor& scheduler, + manager::ManagerHub& manager_hub); ~RequestDispatcher() = default; - void SetRequestScheduler(scheduler::async_executor::AsyncExecutor* scheduler); - void SetManagerHub(manager::ManagerHub* manager_hub); - void RegisterRequestProvider(std::shared_ptr provider); void RegisterNotificationProvider(std::shared_ptr provider); - void RegisterLifecycleCallback(LifecycleCallback callback); - std::string Dispatch(const protocol::RequestMessage& request); void Dispatch(const protocol::NotificationMessage& notification); @@ -97,99 +91,71 @@ export namespace lsp::core std::vector GetAllSupportedMethods() const; private: - void NotifyAllLifecycleListeners(ServerLifecycleEvent event); std::string HandleUnknownRequest(const protocol::RequestMessage& request); void HandleUnknownNotification(const protocol::NotificationMessage& notification); - private: mutable std::shared_mutex providers_mutex_; std::unordered_map> providers_; mutable std::shared_mutex notification_providers_mutex_; - std::unordered_map> notification_providers_; + std::unordered_map> + notification_providers_; - std::mutex callbacks_mutex_; - std::vector lifecycle_callbacks_; - - LifecycleCallback context_lifecycle_callback_; - - scheduler::async_executor::AsyncExecutor* async_executor_ = nullptr; - manager::ManagerHub* manager_hub_ = nullptr; + scheduler::async_executor::AsyncExecutor& async_executor_; + manager::ManagerHub& manager_hub_; }; } namespace lsp::core { - RequestDispatcher::RequestDispatcher() + RequestDispatcher::RequestDispatcher( + scheduler::async_executor::AsyncExecutor& scheduler, + manager::ManagerHub& manager_hub) + : async_executor_(scheduler), manager_hub_(manager_hub) { - context_lifecycle_callback_ = [this](ServerLifecycleEvent event) { - NotifyAllLifecycleListeners(event); - }; } - void RequestDispatcher::SetRequestScheduler(scheduler::async_executor::AsyncExecutor* scheduler) + void RequestDispatcher::RegisterRequestProvider( + std::shared_ptr provider) { - async_executor_ = scheduler; - spdlog::debug("Request scheduler set"); - } - - void RequestDispatcher::SetManagerHub(manager::ManagerHub* manager_hub) - { - manager_hub_ = manager_hub; - spdlog::debug("Manager hub bound to dispatcher"); - } - - void RequestDispatcher::RegisterRequestProvider(std::shared_ptr provider) - { - std::unique_lock lock(providers_mutex_); + std::unique_lock lock(providers_mutex_); std::string method = provider->GetMethod(); - providers_[method] = provider; + providers_[method] = std::move(provider); } - void RequestDispatcher::RegisterNotificationProvider(std::shared_ptr provider) + void RequestDispatcher::RegisterNotificationProvider( + std::shared_ptr provider) { - std::unique_lock lock(notification_providers_mutex_); + std::unique_lock lock(notification_providers_mutex_); std::string method = provider->GetMethod(); - notification_providers_[method] = provider; - } - - void RequestDispatcher::RegisterLifecycleCallback(LifecycleCallback callback) - { - std::lock_guard lock(callbacks_mutex_); - lifecycle_callbacks_.push_back(std::move(callback)); + notification_providers_[method] = std::move(provider); } std::string RequestDispatcher::Dispatch(const protocol::RequestMessage& request) { - std::shared_ptr provider = nullptr; + std::shared_ptr provider; { - std::shared_lock lock(providers_mutex_); - auto it = providers_.find(request.method); - if (it != providers_.end()) - provider = it->second; + std::shared_lock lock(providers_mutex_); + auto provider_it = providers_.find(request.method); + if (provider_it != providers_.end()) + provider = provider_it->second; } if (!provider) return HandleUnknownRequest(request); - if (!async_executor_ || !manager_hub_) - { - spdlog::error("RequestDispatcher dependencies not set"); - return "{}"; - } - - ExecutionContext context(context_lifecycle_callback_, *async_executor_, *manager_hub_); + ExecutionContext context(async_executor_, manager_hub_); return provider->ProvideResponse(request, context); } void RequestDispatcher::Dispatch(const protocol::NotificationMessage& notification) { - std::shared_ptr provider = nullptr; + std::shared_ptr provider; { - std::shared_lock lock(notification_providers_mutex_); - auto it = notification_providers_.find(notification.method); - if (it != notification_providers_.end()) - provider = it->second; + std::shared_lock lock(notification_providers_mutex_); + auto provider_it = notification_providers_.find(notification.method); + if (provider_it != notification_providers_.end()) + provider = provider_it->second; } if (!provider) @@ -198,45 +164,45 @@ namespace lsp::core return; } - if (!async_executor_ || !manager_hub_) - { - spdlog::error("NotificationDispatcher dependencies not set"); - return; - } - - ExecutionContext context(context_lifecycle_callback_, *async_executor_, *manager_hub_); + ExecutionContext context(async_executor_, manager_hub_); provider->HandleNotification(notification, context); } bool RequestDispatcher::SupportsRequest(const std::string& method) const { - std::shared_lock lock(providers_mutex_); + std::shared_lock lock(providers_mutex_); return providers_.contains(method); } bool RequestDispatcher::SupportsNotification(const std::string& method) const { - std::shared_lock lock(notification_providers_mutex_); + std::shared_lock lock(notification_providers_mutex_); return notification_providers_.contains(method); } std::vector RequestDispatcher::GetSupportedRequests() const { std::vector methods; - std::shared_lock lock(providers_mutex_); + std::shared_lock lock(providers_mutex_); methods.reserve(providers_.size()); - for (const auto& [method, _] : providers_) + for (const auto& [method, provider] : providers_) + { + static_cast(provider); methods.push_back(method); + } return methods; } std::vector RequestDispatcher::GetSupportedNotifications() const { std::vector methods; - std::shared_lock lock(notification_providers_mutex_); + std::shared_lock lock(notification_providers_mutex_); methods.reserve(notification_providers_.size()); - for (const auto& [method, _] : notification_providers_) + for (const auto& [method, provider] : notification_providers_) + { + static_cast(provider); methods.push_back(method); + } return methods; } @@ -248,48 +214,40 @@ namespace lsp::core return methods; } - void RequestDispatcher::NotifyAllLifecycleListeners(ServerLifecycleEvent event) - { - std::lock_guard lock(callbacks_mutex_); - for (const auto& callback : lifecycle_callbacks_) - { - if (callback) - callback(event); - } - } - - std::string RequestDispatcher::HandleUnknownRequest(const protocol::RequestMessage& request) + std::string RequestDispatcher::HandleUnknownRequest( + const protocol::RequestMessage& request) { spdlog::warn("No request provider registered for method: {}", request.method); + return BuildErrorResponseMessage( + request, protocol::ErrorCodes::MethodNotFound, "Method not supported"); + } + + void RequestDispatcher::HandleUnknownNotification( + const protocol::NotificationMessage& notification) + { + spdlog::warn("No notification provider registered for method: {}", + notification.method); + } + + std::string BuildErrorResponseMessage(std::optional id, + protocol::ErrorCodes code, + std::string_view message) + { protocol::ResponseMessage response; - response.id = request.id; + response.id = std::move(id); response.error = protocol::ResponseError{ .jsonrpc = "2.0", - .code = static_cast(protocol::ErrorCodes::MethodNotFound), - .message = "Method not supported", - .data = std::nullopt + .code = static_cast(code), + .message = std::string(message), + .data = std::nullopt, }; - auto json = transform::Serialize(response); - return json.value_or("{}"); + return transform::Serialize(response).value(); } - void RequestDispatcher::HandleUnknownNotification(const protocol::NotificationMessage& notification) + std::string BuildErrorResponseMessage(const protocol::RequestMessage& request, + protocol::ErrorCodes code, + std::string_view message) { - spdlog::warn("No notification provider registered for method: {}", notification.method); - } - - std::string BuildErrorResponseMessage(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message) - { - protocol::ResponseMessage response; - response.id = request.id; - protocol::ResponseError error; - error.code = static_cast(code); - error.message = message; - response.error = error; - auto json = transform::Serialize(response); - if (json.has_value()) - return json.value(); - spdlog::error("Failed to serialize error response."); - return R"({"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Failed to serialize error response"}})"; + return BuildErrorResponseMessage(request.id, code, message); } } diff --git a/lsp-server/src/core/server.cppm b/lsp-server/src/core/server.cppm index fd2c516..5189704 100644 --- a/lsp-server/src/core/server.cppm +++ b/lsp-server/src/core/server.cppm @@ -1,65 +1,59 @@ module; export module lsp.core.server; + import spdlog; +import std; import tree_sitter; -import std; - import lsp.bridge.win32_stdio; -import lsp.core.dispatcher; -import lsp.protocol; import lsp.codec.facade; +import lsp.core.dispatcher; import lsp.language.ast; -import lsp.manager.manager_hub; import lsp.manager.bootstrap; import lsp.manager.events; -import lsp.scheduler.async_executor; -import lsp.provider.base.interface; +import lsp.manager.manager_hub; +import lsp.protocol; import lsp.provider.manifest; +import lsp.scheduler.async_executor; namespace transform = lsp::codec; export namespace lsp::core { + using ProviderRegistrar = std::function; + + enum class ServerState + { + kUninitialized, + kRunning, + kShutdownRequested, + kExiting, + }; + class LspServer { public: - explicit LspServer(std::size_t concurrency = std::thread::hardware_concurrency(), - std::string interpreter_path = ""); - ~LspServer(); - void Run(); + LspServer(std::istream& input, + std::ostream& output, + ProviderRegistrar registrar, + std::size_t concurrency = std::thread::hardware_concurrency(), + std::string interpreter_path = ""); + ~LspServer() = default; + + int Run(); private: - // 读取LSP消息 std::optional ReadMessage(); - - // 处理LSP请求 - 返回序列化的响应或空字符串(对于通知) void HandleMessage(const std::string& raw_message); - - // 发送LSP消息(响应/通知) void SendMessage(const std::string& message); - // 处理不同类型的消息 void HandleRequest(const protocol::RequestMessage& request); void HandleNotification(const protocol::NotificationMessage& notification); void HandleResponse(const protocol::ResponseMessage& response); - // 生命周期事件处理 - void OnLifecycleEvent(provider::ServerLifecycleEvent event); - - // 判断是否需要同步处理 - bool RequiresSyncProcessing(const std::string& method) const; - - // 检查是否可以处理请求 - bool CanProcessRequest(const std::string& method) const; - - // 处理取消请求 - void HandleCancelRequest(const protocol::NotificationMessage& notification); - - private: void InitializeManagerHub(); - void RegisterProviders(); + void RegisterProviders(ProviderRegistrar registrar); void RegisterDiagnosticsPublisher(); void PublishDiagnostics(const protocol::DocumentUri& uri, @@ -68,32 +62,42 @@ export namespace lsp::core const protocol::string& content); void ClearDiagnostics(const protocol::DocumentUri& uri); - // 错误处理 - void SendError(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message); - void SendStateError(const protocol::RequestMessage& request); + void SendError(const protocol::RequestMessage& request, + protocol::ErrorCodes code, + std::string_view message); - private: - RequestDispatcher dispatcher_; + std::istream& input_; + std::ostream& output_; manager::ManagerHub manager_hub_; scheduler::async_executor::AsyncExecutor async_executor_; + RequestDispatcher dispatcher_; std::string interpreter_path_; - std::atomic is_initialized_ = false; - std::atomic is_shutting_down_ = false; + ServerState state_ = ServerState::kUninitialized; + int exit_code_ = 1; + std::atomic fatal_io_error_ = false; std::mutex output_mutex_; }; } namespace lsp::core { - LspServer::LspServer(std::size_t concurrency, std::string interpreter_path) : manager_hub_(), - async_executor_(concurrency), - interpreter_path_(std::move(interpreter_path)) + LspServer::LspServer(std::istream& input, + std::ostream& output, + ProviderRegistrar registrar, + std::size_t concurrency, + std::string interpreter_path) + : input_(input), + output_(output), + manager_hub_(), + async_executor_(concurrency), + dispatcher_(async_executor_, manager_hub_), + interpreter_path_(std::move(interpreter_path)) { spdlog::info("Initializing LSP server with {} worker threads", concurrency); InitializeManagerHub(); - RegisterProviders(); + RegisterProviders(std::move(registrar)); if (provider::kEnableDiagnosticsPublisher) { RegisterDiagnosticsPublisher(); @@ -103,48 +107,48 @@ namespace lsp::core spdlog::debug("Diagnostics publisher disabled (staged rollout)"); } - spdlog::debug("LSP server initialized with {} providers.", dispatcher_.GetAllSupportedMethods().size()); + spdlog::debug("LSP server initialized with {} providers.", + dispatcher_.GetAllSupportedMethods().size()); } - LspServer::~LspServer() - { - is_shutting_down_ = true; - spdlog::info("LSP server shutting down..."); - } - - void LspServer::Run() + int LspServer::Run() { spdlog::info("LSP server starting main loop..."); spdlog::info("Waiting for LSP messages on stdin..."); bridge::win32_stdio::SetStdioBinaryMode(); - while (!is_shutting_down_) + while (state_ != ServerState::kExiting) { + if (fatal_io_error_) + break; + try { - std::optional message = ReadMessage(); + auto message = ReadMessage(); if (!message) { - if (std::cin.eof()) - { - spdlog::info("End of input stream, exiting main loop"); - break; // EOF - } - spdlog::debug("No message received, continuing..."); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - continue; + spdlog::info("End of input stream, exiting main loop"); + break; } - HandleMessage(*message); } - catch (const std::exception& e) + catch (const std::exception& error) { - spdlog::error("Error in main loop: {}", e.what()); - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + spdlog::error("Fatal error in main loop: {}", error.what()); + exit_code_ = 1; + break; + } + catch (...) + { + spdlog::error("Unknown fatal error in main loop"); + exit_code_ = 1; + break; } } + spdlog::info("LSP server main loop ended"); + return exit_code_; } std::optional LspServer::ReadMessage() @@ -152,56 +156,32 @@ namespace lsp::core std::string line; std::size_t content_length = 0; - // 读取 LSP Header - while (std::getline(std::cin, line)) + while (std::getline(input_, line)) { - // 去掉尾部 \\r if (!line.empty() && line.back() == '\r') - { line.pop_back(); - } if (line.empty()) - { - break; // 空行表示 header 结束 - } + break; - if (line.rfind("Content-Length:", 0) == 0) - { - std::string length_str = line.substr(15); // 跳过 "Content-Length:" - std::size_t start = length_str.find_first_not_of(' '); - if (start != std::string::npos) - { - length_str = length_str.substr(start); - try - { - content_length = std::stoul(length_str); - spdlog::trace("Content-Length: {}", content_length); - } - catch (const std::exception& e) - { - spdlog::error("Failed to parse Content-Length: {}", e.what()); - return std::nullopt; - } - } - } + if (line.rfind("Content-Length:", 0) != 0) + continue; + + std::string length = line.substr(std::string_view("Content-Length:").size()); + const auto start = length.find_first_not_of(' '); + if (start == std::string::npos) + return std::nullopt; + + content_length = std::stoul(length.substr(start)); } if (content_length == 0) - { - spdlog::debug("No Content-Length found in header"); return std::nullopt; - } - // 读取内容体 std::string body(content_length, '\0'); - std::cin.read(&body[0], content_length); - - if (std::cin.gcount() != static_cast(content_length)) - { - spdlog::error("Failed to read expected content length: {} bytes, got {} bytes", content_length, std::cin.gcount()); + input_.read(body.data(), static_cast(content_length)); + if (input_.gcount() != static_cast(content_length)) return std::nullopt; - } spdlog::trace("Received message: {}", body); return body; @@ -216,11 +196,11 @@ namespace lsp::core return; } - const auto& obj = any->Get(); - const bool has_id = obj.find("id") != obj.end(); - const bool has_method = obj.find("method") != obj.end(); - const bool has_result = obj.find("result") != obj.end(); - const bool has_error = obj.find("error") != obj.end(); + const auto& object = any->Get(); + const bool has_id = object.contains("id"); + const bool has_method = object.contains("method"); + const bool has_result = object.contains("result"); + const bool has_error = object.contains("error"); if (has_method && has_id) { @@ -233,10 +213,15 @@ namespace lsp::core if (has_method) { - if (auto notification = transform::Deserialize(raw_message)) + if (auto notification = + transform::Deserialize(raw_message)) + { HandleNotification(*notification); + } else + { spdlog::warn("Failed to parse notification message"); + } return; } @@ -254,35 +239,84 @@ namespace lsp::core void LspServer::SendMessage(const std::string& message) { - if (message.empty()) - return; - - std::lock_guard lock(output_mutex_); - std::cout << "Content-Length: " << message.size() << "\r\n\r\n" - << message << std::flush; + 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"); + } } void LspServer::HandleRequest(const protocol::RequestMessage& request) { spdlog::debug("Handling request: {}", request.method); + if (request.method == "initialize") + { + if (state_ != ServerState::kUninitialized) + { + SendError(request, + protocol::ErrorCodes::InvalidRequest, + "Server is already initialized"); + return; + } + + const auto response = dispatcher_.Dispatch(request); + SendMessage(response); + const auto parsed = transform::Deserialize(response); + if (!parsed) + throw std::runtime_error("Initialize provider returned an invalid response"); + if (!parsed->error) + state_ = ServerState::kRunning; + return; + } + if (request.method == "shutdown") { - auto response = dispatcher_.Dispatch(request); - SendMessage(response); - is_shutting_down_ = true; + if (state_ == ServerState::kUninitialized) + { + SendError(request, + protocol::ErrorCodes::ServerNotInitialized, + "Server not initialized"); + return; + } + if (state_ != ServerState::kRunning) + { + SendError(request, + protocol::ErrorCodes::InvalidRequest, + "Shutdown already requested"); + return; + } + + 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; } - // 未初始化时的特殊处理 - if (!is_initialized_ && request.method != "initialize") + if (state_ == ServerState::kUninitialized) { - SendStateError(request); + SendError(request, + protocol::ErrorCodes::ServerNotInitialized, + "Server not initialized"); + return; + } + if (state_ == ServerState::kShutdownRequested) + { + SendError(request, + protocol::ErrorCodes::InvalidRequest, + "Server is shutting down"); return; } - auto response = dispatcher_.Dispatch(request); - SendMessage(response); + SendMessage(dispatcher_.Dispatch(request)); } void LspServer::HandleNotification(const protocol::NotificationMessage& notification) @@ -291,21 +325,17 @@ namespace lsp::core if (notification.method == "exit") { - is_shutting_down_ = true; + const bool orderly = state_ == ServerState::kShutdownRequested; + state_ = ServerState::kExiting; + exit_code_ = orderly ? 0 : 1; return; } - // 处理取消请求 - if (notification.method == "$/cancelRequest") + if (state_ != ServerState::kRunning) { - HandleCancelRequest(notification); - return; - } - - // 未初始化时只接受 initialized/exit - if (!is_initialized_ && notification.method != "initialized" && notification.method != "exit") - { - spdlog::warn("Server not initialized; ignoring notification: {}", notification.method); + spdlog::warn("Ignoring notification {} in server state {}", + notification.method, + static_cast(state_)); return; } @@ -315,108 +345,37 @@ namespace lsp::core void LspServer::HandleResponse(const protocol::ResponseMessage& response) { std::string id = ""; - if (response.id.has_value()) - id = transform::debug::GetIdString(response.id.value()); + if (response.id) + id = transform::debug::GetIdString(*response.id); spdlog::debug("Received response: {}", id); - // 当前服务器作为 client 的场景较少,这里暂时不处理 - } - - void LspServer::OnLifecycleEvent(provider::ServerLifecycleEvent event) - { - switch (event) - { - case provider::ServerLifecycleEvent::kInitialized: - is_initialized_ = true; - spdlog::info("Server initialized"); - break; - case provider::ServerLifecycleEvent::kShutdown: - is_shutting_down_ = true; - spdlog::info("Server shutting down"); - break; - default: - break; - } - } - - bool LspServer::RequiresSyncProcessing(const std::string& method) const - { - static const std::unordered_set kSyncMethods = { - "initialize", - "shutdown", - "exit", - "$/setTrace", - }; - return kSyncMethods.contains(method); - } - - bool LspServer::CanProcessRequest(const std::string& method) const - { - if (is_shutting_down_) - return false; - - // 初始化前,只处理 initialize 请求 - if (!is_initialized_ && method != "initialize") - return false; - - return true; - } - - void LspServer::HandleCancelRequest(const protocol::NotificationMessage& notification) - { - if (!notification.params.has_value()) - return; - - protocol::CancelParams params = - transform::FromLSPAny.template operator()(notification.params.value()); - - const std::string id_string = std::visit([](const auto& value) -> std::string { - if constexpr (std::is_same_v, int>) - return std::to_string(value); - else - return value; - }, - params.id); - - spdlog::debug("Cancel request received for id: {}", id_string); - // TODO: 实现请求取消逻辑 } void LspServer::InitializeManagerHub() { manager_hub_.Initialize(); - if (!interpreter_path_.empty()) + if (interpreter_path_.empty()) + return; + + const std::filesystem::path funcext_path = + std::filesystem::path(interpreter_path_) / "funcext"; + if (!std::filesystem::exists(funcext_path)) { - std::filesystem::path base = interpreter_path_; - std::filesystem::path funcext_path = base / "funcext"; - if (std::filesystem::exists(funcext_path)) - { - manager::bootstrap::InitializeManagerHub( - manager_hub_, - async_executor_, - { funcext_path.string() }); - } - else - { - spdlog::warn("Interpreter funcext path does not exist: {}", funcext_path.string()); - } + spdlog::warn("Interpreter funcext path does not exist: {}", + funcext_path.string()); + return; } + + manager::bootstrap::InitializeManagerHub( + manager_hub_, async_executor_, {funcext_path.string()}); } - void LspServer::RegisterProviders() + void LspServer::RegisterProviders(ProviderRegistrar registrar) { - dispatcher_.SetRequestScheduler(&async_executor_); - dispatcher_.SetManagerHub(&manager_hub_); - - dispatcher_.RegisterLifecycleCallback([this](ServerLifecycleEvent event) { - OnLifecycleEvent(event); - }); - spdlog::info("Registering LSP providers..."); - - provider::RegisterAllProviders(dispatcher_); - - spdlog::info("Registered {} LSP providers", dispatcher_.GetAllSupportedMethods().size()); + registrar(dispatcher_); + spdlog::info("Registered {} LSP providers", + dispatcher_.GetAllSupportedMethods().size()); } void LspServer::RegisterDiagnosticsPublisher() @@ -425,12 +384,14 @@ namespace lsp::core event_bus.Subscribe( [this](const manager::events::DocumentParsed& event) { - PublishDiagnostics(event.item.uri, event.item.version, event.tree, event.item.text); + PublishDiagnostics( + event.item.uri, event.item.version, event.tree, event.item.text); }); event_bus.Subscribe( [this](const manager::events::DocumentReparsed& event) { - PublishDiagnostics(event.item.uri, event.item.version, event.tree, event.item.text); + PublishDiagnostics( + event.item.uri, event.item.version, event.tree, event.item.text); }); event_bus.Subscribe( @@ -496,7 +457,6 @@ namespace lsp::core spdlog::warn("Failed to serialize diagnostics notification for {}", uri); return; } - SendMessage(*json); } @@ -517,28 +477,13 @@ namespace lsp::core spdlog::warn("Failed to serialize diagnostics clear notification for {}", uri); return; } - SendMessage(*json); } - void LspServer::SendError(const protocol::RequestMessage& request, protocol::ErrorCodes code, const std::string& message) + void LspServer::SendError(const protocol::RequestMessage& request, + protocol::ErrorCodes code, + std::string_view message) { - protocol::ResponseMessage response; - response.id = request.id; - protocol::ResponseError error; - error.code = static_cast(code); - error.message = message; - response.error = error; - auto json = transform::Serialize(response); - if (json) - { - SendMessage(*json); - } + SendMessage(BuildErrorResponseMessage(request, code, message)); } - - void LspServer::SendStateError(const protocol::RequestMessage& request) - { - SendError(request, protocol::ErrorCodes::ServerNotInitialized, "Server not initialized"); - } - } diff --git a/lsp-server/src/provider/base/interface.cppm b/lsp-server/src/provider/base/interface.cppm index aaa0b93..21dcc72 100644 --- a/lsp-server/src/provider/base/interface.cppm +++ b/lsp-server/src/provider/base/interface.cppm @@ -4,13 +4,10 @@ export module lsp.provider.base.interface; import std; -// Thin bridge to core dispatcher interfaces and aliases for backward compatibility. export import lsp.core.dispatcher; export namespace lsp::provider { - using ServerLifecycleEvent = lsp::core::ServerLifecycleEvent; - using LifecycleCallback = lsp::core::LifecycleCallback; using ExecutionContext = lsp::core::ExecutionContext; using IProvider = lsp::core::IProvider; using IRequestProvider = lsp::core::IRequestProvider; diff --git a/lsp-server/src/provider/cancel_request/cancel_request.cppm b/lsp-server/src/provider/cancel_request/cancel_request.cppm deleted file mode 100644 index 83ffbf6..0000000 --- a/lsp-server/src/provider/cancel_request/cancel_request.cppm +++ /dev/null @@ -1,45 +0,0 @@ -module; - - -export module lsp.provider.cancel_request.cancel_request; -import spdlog; - -import std; - -import lsp.protocol.types; -import lsp.codec.facade; -import lsp.provider.base.interface; - -namespace transform = lsp::codec; - -export namespace lsp::provider -{ - class CancelRequest : public AutoRegisterProvider - { - public: - static constexpr std::string_view kMethod = "$/cancelRequest"; - static constexpr std::string_view kProviderName = "CancelRequest"; - CancelRequest() = default; - void HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) override; - }; -} - -namespace lsp::provider -{ - - - - - void CancelRequest::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) - { - spdlog::debug("CancelRequestProvider received {}", notification.method); - - auto params = transform::FromLSPAny.template operator()(notification.params.value()); - std::string id_to_cancel = transform::debug::GetIdString(params.id); - spdlog::debug("Processing cancel request for ID: {}", id_to_cancel); - - auto& scheduler = context.GetScheduler(); - bool cancelled = scheduler.Cancel(id_to_cancel); - spdlog::debug("Cancel request {} {}", id_to_cancel, cancelled ? "succeeded" : "not found"); - } -} diff --git a/lsp-server/src/provider/exit/exit.cppm b/lsp-server/src/provider/exit/exit.cppm deleted file mode 100644 index 5783f71..0000000 --- a/lsp-server/src/provider/exit/exit.cppm +++ /dev/null @@ -1,47 +0,0 @@ -module; - - -export module lsp.provider.exit.exit; -import spdlog; - -import std; - -import lsp.provider.base.interface; -import lsp.protocol.types; - -export namespace lsp::provider -{ - class Exit : public AutoRegisterProvider - { - public: - static constexpr std::string_view kMethod = "exit"; - static constexpr std::string_view kProviderName = "Exit"; - Exit() = default; - - void HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) override; - }; -} - -namespace lsp::provider -{ - - - - - void Exit::HandleNotification(const protocol::NotificationMessage& notification, ExecutionContext& context) - { - spdlog::debug("Exit notification {}", notification.method); - spdlog::info("Exit notification received"); - - // 触发生命周期事件 - context.TriggerLifecycleEvent(ServerLifecycleEvent::kShuttingDown); - - // 给一些时间完成清理 - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - context.TriggerLifecycleEvent(ServerLifecycleEvent::kShutdown); - - std::exit(0); - } - -} diff --git a/lsp-server/src/provider/initialize/initialize.cppm b/lsp-server/src/provider/initialize/initialize.cppm index 465ed7a..0ab8dd8 100644 --- a/lsp-server/src/provider/initialize/initialize.cppm +++ b/lsp-server/src/provider/initialize/initialize.cppm @@ -60,11 +60,7 @@ namespace lsp::provider response.result = transform::ToLSPAny(BuildInitializeResult()); std::optional json = transform::Serialize(response); if (!json.has_value()) - { - context.TriggerLifecycleEvent(ServerLifecycleEvent::kInitializeFailed); - return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Internal error"); - } - context.TriggerLifecycleEvent(ServerLifecycleEvent::kInitialized); + throw std::runtime_error("Failed to serialize initialize response"); return json.value(); } diff --git a/lsp-server/src/provider/manifest.cppm b/lsp-server/src/provider/manifest.cppm index 541fa50..788d055 100644 --- a/lsp-server/src/provider/manifest.cppm +++ b/lsp-server/src/provider/manifest.cppm @@ -8,7 +8,6 @@ import lsp.provider.base.registry; import lsp.provider.completion_item.resolve; import lsp.provider.initialize.initialize; import lsp.provider.initialized.initialized; -import lsp.provider.shutdown.shutdown; import lsp.provider.text_document.definition; import lsp.provider.text_document.did_change; import lsp.provider.text_document.did_close; @@ -20,13 +19,11 @@ import lsp.provider.trace.set_trace; // Uncomment when re-enabling additional capabilities. // import lsp.provider.call_hierarchy.incoming_calls; // import lsp.provider.call_hierarchy.outgoing_calls; -// import lsp.provider.cancel_request.cancel_request; // import lsp.provider.client.register_capability; // import lsp.provider.client.unregister_capability; // import lsp.provider.code_action.resolve; // import lsp.provider.code_lens.resolve; // import lsp.provider.document_link.resolve; -// import lsp.provider.exit.exit; // import lsp.provider.inlay_hint.resolve; // import lsp.provider.telemetry.event; // import lsp.provider.text_document.code_action; @@ -94,7 +91,6 @@ export namespace lsp::provider completion_item::Resolve, Initialize, Initialized, - Shutdown, text_document::Completion, text_document::Definition, text_document::DidChange, @@ -107,18 +103,15 @@ export namespace lsp::provider // using AllProviders = ProviderRegistry< // call_hierarchy::IncomingCalls, // call_hierarchy::OutgoingCalls, - // CancelRequest, // client::RegisterCapability, // client::UnregisterCapability, // code_action::Resolve, // code_lens::Resolve, // completion_item::Resolve, // document_link::Resolve, - // Exit, // Initialize, // Initialized, // inlay_hint::Resolve, - // Shutdown, // telemetry::Event, // text_document::CodeAction, // text_document::CodeLens, diff --git a/lsp-server/src/provider/shutdown/shutdown.cppm b/lsp-server/src/provider/shutdown/shutdown.cppm deleted file mode 100644 index 2be1192..0000000 --- a/lsp-server/src/provider/shutdown/shutdown.cppm +++ /dev/null @@ -1,53 +0,0 @@ -module; - - -export module lsp.provider.shutdown.shutdown; -import spdlog; - -import std; - -import lsp.protocol; -import lsp.codec.facade; -import lsp.provider.base.interface; - -namespace transform = lsp::codec; - -export namespace lsp::provider -{ - class Shutdown : public AutoRegisterProvider - { - public: - static constexpr std::string_view kMethod = "shutdown"; - static constexpr std::string_view kProviderName = "Shutdown"; - Shutdown() = default; - - std::string ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) override; - }; -} - -namespace lsp::provider -{ - - - - - std::string Shutdown::ProvideResponse(const protocol::RequestMessage& request, ExecutionContext& context) - { - spdlog::debug("ShutdownProvider: Providing response for method {}", request.method); - - // 触发关闭事件 - context.TriggerLifecycleEvent(ServerLifecycleEvent::kShuttingDown); - - context.GetManagerHub().Shutdown(); - - // 构建响应 - shutdown 返回 null - protocol::ResponseMessage response; - response.id = request.id; - auto json = transform::Serialize(response); - if (!json.has_value()) - return BuildErrorResponseMessage(request, protocol::ErrorCodes::InternalError, "Failed to serialize response"); - spdlog::info("Shutdown request processed successfully"); - return json.value(); - } - -} diff --git a/lsp-server/test/CMakeLists.txt b/lsp-server/test/CMakeLists.txt index 8e1cf28..acfb595 100644 --- a/lsp-server/test/CMakeLists.txt +++ b/lsp-server/test/CMakeLists.txt @@ -56,6 +56,10 @@ if(BUILD_TESTS) COMMAND ${PYTHON3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/test_cli_startup.py --server $) + add_test(NAME test_core_server + COMMAND ${PYTHON3_EXECUTABLE} + ${CMAKE_CURRENT_LIST_DIR}/test_core_server.py + --server $) else() message(WARNING "python3 not found; skipping test_lsp_json and test_cli_startup registration") endif() diff --git a/lsp-server/test/test_core_server.py b/lsp-server/test/test_core_server.py new file mode 100644 index 0000000..3dbad83 --- /dev/null +++ b/lsp-server/test/test_core_server.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import select +import subprocess +import time +from pathlib import Path + + +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) + if header_end < 0: + raise RuntimeError("Incomplete LSP response header") + + 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 + if body_end > len(data): + raise RuntimeError("Incomplete LSP response body") + + 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]: + return run_raw(server, b"".join(frame(message) for message in messages)) + + +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: + if self.proc.stdin is None: + raise RuntimeError("Fixture stdin is unavailable") + self.proc.stdin.write(frame(message)) + self.proc.stdin.flush() + + def read(self, timeout: float = 1.0) -> dict: + if self.proc.stdout is None: + raise RuntimeError("Fixture stdout is unavailable") + + 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: + if self.proc.stdin is not None: + self.proc.stdin.close() + return self.proc.wait(timeout=5) + + def kill(self) -> None: + if self.proc.poll() is None: + self.proc.kill() + 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"}, + ]) + if before_init.returncode != 1: + raise RuntimeError("shutdown before initialize should exit with code 1") + if response_by_id(before_init.stdout, 1)["error"]["code"] != -32002: + raise RuntimeError("shutdown before initialize should return ServerNotInitialized") + + 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"}, + ]) + if repeated.returncode != 0: + raise RuntimeError("shutdown followed by exit should return code 0") + if response_by_id(repeated.stdout, 2)["error"]["code"] != -32600: + raise RuntimeError("repeated initialize should return InvalidRequest") + if response_by_id(repeated.stdout, 3).get("result", "missing") is not None: + raise RuntimeError("shutdown should return a null result") + + direct_exit = run_batch( + server, [{"jsonrpc": "2.0", "method": "exit"}]) + if direct_exit.returncode != 1: + raise RuntimeError("exit before shutdown should return code 1") + + client = LspClient(server) + try: + client.send({ + "jsonrpc": "2.0", + "id": 11, + "method": "initialize", + "params": {}, + }) + if client.read().get("id") != 11: + raise RuntimeError("missing initialize response") + + client.send({"jsonrpc": "2.0", "id": 12, "method": "shutdown"}) + if client.read().get("result", "missing") is not None: + raise RuntimeError("shutdown should return a null result") + + time.sleep(0.1) + if client.proc.poll() is not None: + raise RuntimeError("server exited before receiving exit notification") + + client.send({"jsonrpc": "2.0", "method": "exit"}) + if client.close_input() != 0: + raise RuntimeError("orderly lifecycle should return code 0") + finally: + client.kill() + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--server", type=Path, required=True) + args = parser.parse_args() + + assert_lifecycle(args.server) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/lsp-server/test/test_provider/CMakeLists.txt b/lsp-server/test/test_provider/CMakeLists.txt index 15f2665..5b51925 100644 --- a/lsp-server/test/test_provider/CMakeLists.txt +++ b/lsp-server/test/test_provider/CMakeLists.txt @@ -20,6 +20,7 @@ set(SOURCES main.cc test_main.cppm ../test_lsp_any/test_framework.cppm + core_server_fixture.cppm fixtures.cppm completion_test.cppm json_flow_test.cppm @@ -49,6 +50,7 @@ target_sources( ${CMAKE_CURRENT_SOURCE_DIR}/../../src FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cppm ${CMAKE_CURRENT_SOURCE_DIR}/../test_lsp_any/test_framework.cppm + ${CMAKE_CURRENT_SOURCE_DIR}/core_server_fixture.cppm ${CMAKE_CURRENT_SOURCE_DIR}/fixtures.cppm ${CMAKE_CURRENT_SOURCE_DIR}/completion_test.cppm ${CMAKE_CURRENT_SOURCE_DIR}/json_flow_test.cppm @@ -62,10 +64,14 @@ target_sources( ../../src/bridge/spdlog.cppm ../../src/bridge/taskflow.cppm ../../src/bridge/tree_sitter.cppm + ../../src/bridge/win32_stdio.cppm + ../../src/utils/args_parser.cppm ../../src/utils/string.cppm ../../src/utils/text_coordinates.cppm ../../src/core/dispatcher.cppm + ../../src/core/server.cppm ../../src/scheduler/async_executor.cppm + ../../src/manager/bootstrap.cppm ../../src/manager/event_bus.cppm ../../src/manager/events.cppm ../../src/manager/detail/text_document.cppm @@ -140,9 +146,6 @@ target_sources( ../../src/provider/completion_item/resolve.cppm ../../src/provider/initialize/initialize.cppm ../../src/provider/initialized/initialized.cppm - ../../src/provider/shutdown/shutdown.cppm - ../../src/provider/exit/exit.cppm - ../../src/provider/cancel_request/cancel_request.cppm ../../src/provider/trace/set_trace.cppm ../../src/provider/client/register_capability.cppm ../../src/provider/client/unregister_capability.cppm diff --git a/lsp-server/test/test_provider/completion_test.cppm b/lsp-server/test/test_provider/completion_test.cppm index 2a6b1b1..9382202 100644 --- a/lsp-server/test/test_provider/completion_test.cppm +++ b/lsp-server/test/test_provider/completion_test.cppm @@ -69,7 +69,7 @@ namespace lsp::test::provider core::ExecutionContext context; ProviderEnv() - : context([](core::ServerLifecycleEvent) {}, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } diff --git a/lsp-server/test/test_provider/core_server_fixture.cppm b/lsp-server/test/test_provider/core_server_fixture.cppm new file mode 100644 index 0000000..cfbaeb5 --- /dev/null +++ b/lsp-server/test/test_provider/core_server_fixture.cppm @@ -0,0 +1,56 @@ +module; + +export module lsp.test.provider.core_server_fixture; + +import spdlog; +import std; + +import lsp.codec.facade; +import lsp.core.server; +import lsp.protocol; +import lsp.provider.base.interface; + +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()); + }, + 2, + ""); + return server.Run(); + } +} diff --git a/lsp-server/test/test_provider/definitions_test.cppm b/lsp-server/test/test_provider/definitions_test.cppm index a9fc977..40c4cc8 100644 --- a/lsp-server/test/test_provider/definitions_test.cppm +++ b/lsp-server/test/test_provider/definitions_test.cppm @@ -38,7 +38,7 @@ namespace lsp::test::provider core::ExecutionContext context; ProviderEnv() - : context([](core::ServerLifecycleEvent) {}, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } diff --git a/lsp-server/test/test_provider/interpreter_test.cppm b/lsp-server/test/test_provider/interpreter_test.cppm index abc70a7..e2c79bd 100644 --- a/lsp-server/test/test_provider/interpreter_test.cppm +++ b/lsp-server/test/test_provider/interpreter_test.cppm @@ -39,7 +39,7 @@ namespace lsp::test::provider core::ExecutionContext context; ProviderEnv() - : context([](core::ServerLifecycleEvent) {}, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } @@ -207,4 +207,3 @@ namespace lsp::test::provider return result; } } - diff --git a/lsp-server/test/test_provider/json_flow_test.cppm b/lsp-server/test/test_provider/json_flow_test.cppm index f578231..31faaa7 100644 --- a/lsp-server/test/test_provider/json_flow_test.cppm +++ b/lsp-server/test/test_provider/json_flow_test.cppm @@ -40,7 +40,7 @@ namespace lsp::test::provider core::ExecutionContext context; ProviderEnv() - : context([](core::ServerLifecycleEvent) {}, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } diff --git a/lsp-server/test/test_provider/json_provider_coverage_test.cppm b/lsp-server/test/test_provider/json_provider_coverage_test.cppm index d8a3f8e..5f5d018 100644 --- a/lsp-server/test/test_provider/json_provider_coverage_test.cppm +++ b/lsp-server/test/test_provider/json_provider_coverage_test.cppm @@ -51,13 +51,11 @@ namespace lsp::test::provider { scheduler::async_executor::AsyncExecutor scheduler{ 1 }; manager::ManagerHub hub{}; - core::RequestDispatcher dispatcher{}; + core::RequestDispatcher dispatcher{ scheduler, hub }; ProviderEnv() { hub.Initialize(); - dispatcher.SetRequestScheduler(&scheduler); - dispatcher.SetManagerHub(&hub); provider::RegisterAllProviders(dispatcher); } }; diff --git a/lsp-server/test/test_provider/provider_misc_test.cppm b/lsp-server/test/test_provider/provider_misc_test.cppm index 64200e4..56dc308 100644 --- a/lsp-server/test/test_provider/provider_misc_test.cppm +++ b/lsp-server/test/test_provider/provider_misc_test.cppm @@ -77,10 +77,7 @@ import lsp.provider.workspace.will_delete_files; import lsp.provider.workspace.will_rename_files; import lsp.provider.workspace_symbol.resolve; import lsp.provider.text_document.publish_diagnostics; -import lsp.provider.shutdown.shutdown; -import lsp.provider.cancel_request.cancel_request; import lsp.provider.trace.set_trace; -import lsp.provider.exit.exit; import lsp.core.dispatcher; import lsp.manager.manager_hub; import lsp.manager.symbol; @@ -160,13 +157,8 @@ export namespace lsp::test::provider static TestResult TestExecuteCommandProvider(); static TestResult TestWillFileOperationsProviders(); static TestResult TestWorkspaceSymbolResolveProvider(); - static TestResult TestShutdownProvider(); - static TestResult TestCancelRequestProvider(); static TestResult TestSetTraceProvider(); - static TestResult TestExitProvider(); }; - - int RunExitProviderChild(); } namespace lsp::test::provider @@ -175,13 +167,12 @@ namespace lsp::test::provider { struct ProviderEnv { - std::vector events; scheduler::async_executor::AsyncExecutor scheduler{ 1 }; manager::ManagerHub hub{}; core::ExecutionContext context; ProviderEnv() - : context([this](core::ServerLifecycleEvent event) { events.push_back(event); }, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } @@ -365,10 +356,7 @@ namespace lsp::test::provider runner.addTest("workspace executeCommand provider", TestExecuteCommandProvider); runner.addTest("workspace will file operations providers", TestWillFileOperationsProviders); runner.addTest("workspaceSymbol/resolve provider", TestWorkspaceSymbolResolveProvider); - runner.addTest("shutdown provider", TestShutdownProvider); - runner.addTest("cancel request provider", TestCancelRequestProvider); runner.addTest("setTrace provider", TestSetTraceProvider); - runner.addTest("exit provider", TestExitProvider); } TestResult ProviderMiscTests::TestInitializeProvider() @@ -434,8 +422,6 @@ namespace lsp::test::provider }); assertTrue(found_workspace, "Workspace symbols should be indexed"); - assertTrue(!env.events.empty(), "Initialize should emit lifecycle event"); - assertTrue(env.events.back() == core::ServerLifecycleEvent::kInitialized, "Initialize should emit initialized"); return result; } @@ -2233,7 +2219,6 @@ namespace lsp::test::provider const auto& stored_obj = stored.Get(); assertTrue(stored_obj.contains("tsl"), "stored settings should include tsl section"); - assertTrue(env.events.empty(), "didChangeConfiguration should not trigger lifecycle events"); return result; } @@ -2761,7 +2746,6 @@ namespace lsp::test::provider provider.HandleNotification(notification, env.context); } - assertTrue(env.events.empty(), "window message notifications should not trigger lifecycle events"); return result; } @@ -2781,7 +2765,6 @@ namespace lsp::test::provider ::lsp::provider::telemetry::Event provider; provider.HandleNotification(notification, env.context); - assertTrue(env.events.empty(), "telemetry/event should not trigger lifecycle events"); return result; } @@ -2816,7 +2799,6 @@ namespace lsp::test::provider ::lsp::provider::text_document::PublishDiagnostics provider; provider.HandleNotification(notification, env.context); - assertTrue(env.events.empty(), "publishDiagnostics should not trigger lifecycle events"); return result; } @@ -3396,63 +3378,6 @@ namespace lsp::test::provider return result; } - TestResult ProviderMiscTests::TestShutdownProvider() - { - TestResult result{ "", true, "ok" }; - ProviderEnv env; - - auto path = FixturePath("rename_case.tsl"); - auto content = ReadTextFile(path); - auto uri = ToUri(path); - OpenDocument(env.hub, uri, content, 1); - - protocol::RequestMessage request; - request.id = "shutdown"; - request.method = "shutdown"; - - ::lsp::provider::Shutdown provider; - auto json = provider.ProvideResponse(request, env.context); - auto response = ParseResponse(json); - assertTrue(!response.error.has_value(), "Shutdown should not return error"); - assertTrue(env.events.size() >= 1, "Shutdown should emit lifecycle event"); - assertTrue(env.events.back() == core::ServerLifecycleEvent::kShuttingDown, "Shutdown should emit shutting down"); - assertFalse(env.hub.documents().GetContent(uri).has_value(), "Shutdown should clear documents"); - return result; - } - - TestResult ProviderMiscTests::TestCancelRequestProvider() - { - TestResult result{ "", true, "ok" }; - ProviderEnv env; - - std::atomic started{ false }; - env.scheduler.Submit("cancel_me", [&started](std::stop_token) -> std::optional { - started.store(true); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - return std::string("done"); - }); - - while (!started.load()) - { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - - protocol::CancelParams params; - params.id = std::string("cancel_me"); - protocol::NotificationMessage notification; - notification.method = "$/cancelRequest"; - notification.params = codec::ToLSPAny(params); - - ::lsp::provider::CancelRequest provider; - provider.HandleNotification(notification, env.context); - env.scheduler.WaitAll(); - - auto stats = env.scheduler.GetStatistics(); - assertEqual(std::size_t(1), static_cast(stats.cancelled), - "CancelRequest should mark task cancelled"); - return result; - } - TestResult ProviderMiscTests::TestSetTraceProvider() { TestResult result{ "", true, "ok" }; @@ -3484,24 +3409,4 @@ namespace lsp::test::provider return result; } - TestResult ProviderMiscTests::TestExitProvider() - { - TestResult result{ "", true, "ok" }; - auto exe = ExecutablePath(); - assertTrue(!exe.empty(), "ExecutablePath should be set"); - std::string command = "\"" + exe + "\" --exit-provider"; - int code = std::system(command.c_str()); - assertEqual(0, code, "Exit should return code 0"); - return result; - } - - int RunExitProviderChild() - { - ProviderEnv env; - ::lsp::provider::Exit provider; - protocol::NotificationMessage notification; - notification.method = "exit"; - provider.HandleNotification(notification, env.context); - return 1; - } } diff --git a/lsp-server/test/test_provider/provider_surface_test.cppm b/lsp-server/test/test_provider/provider_surface_test.cppm index 6ffe179..226539c 100644 --- a/lsp-server/test/test_provider/provider_surface_test.cppm +++ b/lsp-server/test/test_provider/provider_surface_test.cppm @@ -12,16 +12,13 @@ import lsp.manager.manager_hub; import lsp.scheduler.async_executor; import lsp.test.provider.fixtures; -import lsp.provider.cancel_request.cancel_request; import lsp.provider.code_action.resolve; import lsp.provider.code_lens.resolve; import lsp.provider.completion_item.resolve; import lsp.provider.document_link.resolve; -import lsp.provider.exit.exit; import lsp.provider.initialize.initialize; import lsp.provider.initialized.initialized; import lsp.provider.inlay_hint.resolve; -import lsp.provider.shutdown.shutdown; import lsp.provider.trace.set_trace; import lsp.provider.call_hierarchy.incoming_calls; import lsp.provider.call_hierarchy.outgoing_calls; @@ -118,7 +115,7 @@ namespace lsp::test::provider core::ExecutionContext context; ProviderEnv() - : context([](core::ServerLifecycleEvent) {}, scheduler, hub) + : context(scheduler, hub) { hub.Initialize(); } @@ -174,7 +171,6 @@ namespace lsp::test::provider TestResult result{ "", true, "ok" }; CheckProviderMetadata("initialize", "Initialize"); - CheckProviderMetadata("shutdown", "Shutdown"); CheckProviderMetadata("completionItem/resolve", "CompletionItemResolve"); CheckProviderMetadata("textDocument/completion", "TextDocumentCompletion"); CheckProviderMetadata("textDocument/definition", "TextDocumentDefinition"); @@ -273,8 +269,6 @@ namespace lsp::test::provider CheckProviderMetadata("workspace/didDeleteFiles", "WorkspaceDidDeleteFiles"); CheckProviderMetadata("workspace/didRenameFiles", "WorkspaceDidRenameFiles"); CheckProviderMetadata("initialized", "Initialized"); - CheckProviderMetadata("exit", "Exit"); - CheckProviderMetadata("$/cancelRequest", "CancelRequest"); CheckProviderMetadata("$/setTrace", "SetTrace"); CheckProviderMetadata("textDocument/didOpen", "TextDocumentDidOpen"); CheckProviderMetadata("textDocument/didChange", "TextDocumentDidChange"); @@ -287,7 +281,6 @@ namespace lsp::test::provider { TestResult result{ "", true, "ok" }; - CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); CheckRequestResponse(); @@ -357,7 +350,6 @@ namespace lsp::test::provider TestResult result{ "", true, "ok" }; CheckNotificationHandler(std::nullopt); - CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); CheckNotificationHandler(std::nullopt); diff --git a/lsp-server/test/test_provider/test_main.cppm b/lsp-server/test/test_provider/test_main.cppm index d740e84..6693a79 100644 --- a/lsp-server/test/test_provider/test_main.cppm +++ b/lsp-server/test/test_provider/test_main.cppm @@ -6,6 +6,7 @@ import std; import lsp.test.framework; import lsp.test.provider.completion; +import lsp.test.provider.core_server_fixture; import lsp.test.provider.definitions; import lsp.test.provider.interpreter; import lsp.test.provider.json_flow; @@ -26,9 +27,9 @@ export int Run(int argc, char** argv) { std::string_view arg(argv[i]); - if (arg == "--exit-provider") + if (arg == "--core-server-fixture") { - return lsp::test::provider::RunExitProviderChild(); + return lsp::test::provider::RunCoreServerFixture(); } constexpr std::string_view kInterpreterPrefix = "--interpreter=";