From 00e0f90b69d26920f282eeacc2e778fe9ceade77 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Wed, 13 May 2026 21:19:47 +0000 Subject: [PATCH] fix(lsp): cleanup subprocess on failed initialization in StdioTransport.start()\n\nWrap post-Popen code in try/except to ensure subprocess resources are\nproperly cleaned up if the spawned process dies immediately or if any\nunexpected exception occurs. Detect already-dead processes via poll() and\nraise LspError with exit code details. Use suppress to prevent stop() errors\from masking the original exception. --- src/cleveragents/lsp/transport.py | 36 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/cleveragents/lsp/transport.py b/src/cleveragents/lsp/transport.py index c85262cff..ad700f27a 100644 --- a/src/cleveragents/lsp/transport.py +++ b/src/cleveragents/lsp/transport.py @@ -18,6 +18,7 @@ and issue #826. from __future__ import annotations +import contextlib import json import os import select @@ -126,11 +127,36 @@ class StdioTransport: details={"command": self._command, "error": str(exc)}, ) from exc - logger.info( - "lsp.transport.started", - pid=self._process.pid, - command=self._command, - ) + # If anything goes wrong after Popen succeeds (process already dies + # immediately, unexpected exception), make sure the subprocess is + # cleaned up before propagating. A dead process with open pipe fds + # would otherwise leak file descriptors until garbage collection. + try: + logger.info( + "lsp.transport.started", + pid=self._process.pid, + command=self._command, + ) + + # If the child already exited during or right after spawn + # (e.g. missing shared library, runtime crash), clean it up + # immediately rather than leaving a dead process with open fds. + if self._process.poll() is not None: + code = self._process.returncode + self.stop() # closes pipes and reaps child + raise LspError( + f"LSP server exited immediately (exit code {code})", + details={"command": self._command, "args": self._args}, + ) + + except BaseException: + # Ensure the subprocess is terminated before propagating. + # Use suppress to prevent stop() errors from masking the active + # exception — either way we always re-raise whatever was caught. + if self._process is not None: + with contextlib.suppress(BaseException): + self.stop() + raise def stop(self, timeout: float = _GRACEFUL_SHUTDOWN_TIMEOUT) -> int | None: """Terminate the subprocess gracefully, then force-kill if needed. -- 2.52.0