Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/solidlsp/language_servers/kotlin_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import threading
from typing import cast

import psutil
from overrides import override

from solidlsp.ls import (
Expand Down Expand Up @@ -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
Loading