From 2fba67f2310aa3d41bae8569175ae8540bdd2eab Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:41:52 +0200 Subject: [PATCH] LS: hacky solution for waiting till pyright and ts servers are ready --- .../pyright_language_server/pyright_server.py | 64 ++++++++++++++++--- .../typescript_language_server.py | 22 +++++-- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py index 2a8ee1b..4885583 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/multilspy/language_servers/pyright_language_server/pyright_server.py @@ -2,10 +2,12 @@ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ +import asyncio import json import logging import os import pathlib +import re from contextlib import asynccontextmanager from typing import AsyncIterator, Tuple @@ -38,6 +40,10 @@ class PyrightServer(LanguageServer): "python", ) + # Event to signal when initial workspace analysis is complete + self.analysis_complete = asyncio.Event() + self.found_source_files = False + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -145,16 +151,19 @@ class PyrightServer(LanguageServer): @asynccontextmanager async def start_server(self) -> AsyncIterator["PyrightServer"]: """ - Starts the Pyright Language Server, waits for the server to be ready and yields the LanguageServer instance. + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. Usage: ``` async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests + # LanguageServer has been initialized and workspace analysis is complete await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + # LanguageServer has been shutdown cleanly ``` """ @@ -164,13 +173,33 @@ class PyrightServer(LanguageServer): async def do_nothing(params): return - async def check_experimental_status(params): - if params["quiescent"] == True: + async def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + Pyright logs "Found X source files" when it finishes scanning the workspace. + """ + message_text = msg.get("message", "") + self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + + # Look for "Found X source files" which indicates workspace scanning is complete + # Unfortunately, pyright is unreliable and there seems to be no better way + if re.search(r"Found \d+ source files?", message_text): + self.logger.log("Pyright workspace scanning complete", logging.INFO) + self.found_source_files = True + self.analysis_complete.set() self.completions_available.set() - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + # Set up notification handlers self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) @@ -195,6 +224,23 @@ class PyrightServer(LanguageServer): self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) # Verify that the server supports our required features - self.server.notify.initialized({}) + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - yield self + # Complete the initialization handshake + self.server.notify.initialized({}) + + # Wait for Pyright to complete its initial workspace analysis + # This prevents zombie processes by ensuring background tasks finish + self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) + try: + await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() + + yield self \ No newline at end of file diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 122de61..479936c 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -186,12 +186,22 @@ class TypeScriptLanguageServer(LanguageServer): async def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) async with super().start_server(): self.logger.log("Starting TypeScript server process", logging.INFO) @@ -213,11 +223,13 @@ class TypeScriptLanguageServer(LanguageServer): } self.server.notify.initialized({}) - self.completions_available.set() - - # TypeScript server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + try: + await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() yield self