diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index a680b2e4e..b3dda7662 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -23,6 +23,7 @@ import threading from typing import cast +import psutil from overrides import override from solidlsp.ls import ( @@ -554,3 +555,49 @@ def progress_handler(params: dict) -> None: def _get_wait_time_for_cross_file_referencing(self) -> float: """Small safety buffer since we already waited for indexing to complete in _start_server.""" return 1.0 + + @override + def stop(self, shutdown_timeout: float = 2.0) -> None: + assert isinstance(self._dependency_provider, self.DependencyProvider) + java_home = self._dependency_provider._java_home_path + assert java_home is not None + super().stop(shutdown_timeout=shutdown_timeout) + self._kill_gradle_daemons(java_home) + + @staticmethod + def _kill_gradle_daemons(java_home: str) -> None: + """Kill Gradle daemons spawned by KLS during project indexing. + + KLS triggers Gradle during indexing, which spawns persistent daemon processes that + outlive the KLS process (3-hour idle timeout, ~500MB RSS each). These daemons are + not children of KLS and are invisible to normal process-tree cleanup. + + We identify them by matching the java executable in their command line against + the JAVA_HOME used by this KLS instance. + """ + java_bin_resolved = os.path.realpath(os.path.join(java_home, "bin", "java")) + + killed: list[psutil.Process] = [] + for proc in psutil.process_iter(["pid", "cmdline"]): + try: + cmdline = proc.info["cmdline"] + if not cmdline: + continue + if not any("GradleDaemon" in arg for arg in cmdline): + continue + # cmdline[0] is the java binary path used to start the daemon + if os.path.realpath(cmdline[0]) == java_bin_resolved: + log.info("Terminating Gradle daemon (PID %d) spawned by KLS", proc.pid) + proc.terminate() + killed.append(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + + if killed: + _, alive = psutil.wait_procs(killed, timeout=5) + for proc in alive: + log.warning("Gradle daemon (PID %d) did not terminate gracefully, killing", proc.pid) + try: + proc.kill() + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass