From 87eaa405531cdff4cd80a8f59c00b1c501189cba Mon Sep 17 00:00:00 2001 From: Erik Jacobs Date: Mon, 18 May 2026 07:08:27 -0400 Subject: [PATCH] fix(gdscript): detect broken TCP connection and fail fast on send error (#1493) fix(gdscript): detect broken TCP connection and fail fast on reconnect When the Godot editor restarts or the LSP connection drops, the TCPLanguageServer read loop exits but previously left self._sock set, causing is_running() to return True. Subsequent LSP requests would then send to a dead socket, log a broken-pipe error silently, and block for the full tool timeout (235 s by default) before raising. Two fixes in TCPLanguageServer: - _send_payload: on OSError, clear self._sock/self._file and immediately cancel all pending requests so callers fail fast instead of waiting out the timeout. - _read_loop: after unexpected exit, clear self._sock/self._file so is_running() returns False and _ensure_functional_ls can trigger an automatic reconnect on the next tool call. Also fixes GodotLanguageServer so that ls_specific_settings.request_timeout is respected as a cap on the per-request timeout. Previously the value was read in _create_language_server_interface but then overridden by the tool-timeout-derived value passed to set_request_timeout(). Now set_request_timeout() applies min(incoming, configured) so users can set a low request_timeout in their project.yml to fail fast without lowering the global tool_timeout. Co-Authored-By: Claude Sonnet 4.6 --- src/solidlsp/language_servers/godot_language_server.py | 9 +++++++++ src/solidlsp/ls_process.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/solidlsp/language_servers/godot_language_server.py b/src/solidlsp/language_servers/godot_language_server.py index b7ed3996..3186a123 100644 --- a/src/solidlsp/language_servers/godot_language_server.py +++ b/src/solidlsp/language_servers/godot_language_server.py @@ -46,9 +46,17 @@ class GodotLanguageServer(SolidLanguageServer): else: log.warning("Could not detect Godot version for project at %s", repository_root_path) + self._configured_request_timeout: float | None = None + # Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings) + def set_request_timeout(self, timeout: float | None) -> None: + """Cap the timeout at the value configured in ls_specific_settings, if set.""" + if timeout is not None and self._configured_request_timeout is not None: + timeout = min(timeout, self._configured_request_timeout) + super().set_request_timeout(timeout) + @staticmethod def _detect_godot_version(repo_path: str) -> int | None: """Read project.godot to determine the major Godot version. @@ -75,6 +83,7 @@ class GodotLanguageServer(SolidLanguageServer): settings: dict = self._custom_settings or {} port = settings.get("port", DEFAULT_GODOT_LS_PORT) request_timeout = settings.get("request_timeout", DEFAULT_GODOT_REQUEST_TIMEOUT) + self._configured_request_timeout = settings.get("request_timeout") self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=port) return TCPLanguageServer( connection_info=self._conn_info, diff --git a/src/solidlsp/ls_process.py b/src/solidlsp/ls_process.py index 2bc5849d..5ac06c0e 100644 --- a/src/solidlsp/ls_process.py +++ b/src/solidlsp/ls_process.py @@ -727,6 +727,9 @@ class TCPLanguageServer(LanguageServerInterface): sock.sendall(data) except OSError as e: log.error("Failed to write to TCP language server: %s", e) + self._sock = None + self._file = None + self._cancel_pending_requests(LanguageServerTerminatedException("TCP send error", self.language, cause=e)) def _read_loop(self) -> None: """Read Content-Length-framed LSP messages from the TCP socket and dispatch them.""" @@ -775,3 +778,7 @@ class TCPLanguageServer(LanguageServerInterface): exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.language) log.error(str(exception)) self._cancel_pending_requests(exception) + # Clear the socket so is_running() returns False, allowing _ensure_functional_ls + # to detect the broken connection and restart the language server. + self._sock = None + self._file = None