66import hashlib
77import json
88import logging
9+ import math
910import os
1011import pathlib
1112import platform
@@ -145,6 +146,8 @@ class EclipseJDTLS(SolidLanguageServer):
145146 - jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m")
146147 - intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G")
147148 - intellicode_xms: Initial heap size for the IntelliCode embedded JVM (default: "100m")
149+ - startup_timeout: Maximum seconds to wait for each required JDTLS startup signal
150+ (IntelliCode command registration and ServiceReady; default: 600)
148151 - lombok_show_generated: Show Lombok-generated methods (getX/setX/builder()/...) in document
149152 symbols by sending java.symbols.includeGeneratedCode=true to JDTLS (default: true).
150153 Set to false for @Data-heavy projects where the extra getters/setters are noise.
@@ -192,6 +195,7 @@ class EclipseJDTLS(SolidLanguageServer):
192195 jdtls_xms: "100m" # initial heap size for the JDTLS server JVM
193196 intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM
194197 intellicode_xms: "100m" # initial heap size for the IntelliCode embedded JVM
198+ startup_timeout: 600 # maximum wait for each required startup signal
195199 lombok_show_generated: true # show Lombok-generated methods in document symbols (default true)
196200 gradle_version: "8.14.2"
197201 vscode_java_version: "1.54.0-923" # also accepts pinned legacy "1.42.0-561"
@@ -203,6 +207,9 @@ class EclipseJDTLS(SolidLanguageServer):
203207 ```
204208 """
205209
210+ STARTUP_TIMEOUT = 600.0
211+ STARTUP_SHUTDOWN_TIMEOUT = 5.0
212+
206213 @classmethod
207214 def supports_implementation_request (cls ) -> bool :
208215 return True
@@ -221,6 +228,60 @@ def __init__(self, config: LanguageServerConfig, repository_root_path: str, soli
221228 self ._service_ready_event = threading .Event ()
222229 self ._project_ready_event = threading .Event ()
223230 self ._intellicode_enable_command_available = threading .Event ()
231+ self ._startup_phase = "not_started"
232+ self ._last_language_status : tuple [str | None , str | None ] | None = None
233+ self ._get_startup_timeout () # validate before a server process can be started
234+
235+ def _get_startup_timeout (self ) -> float :
236+ """Return the maximum seconds to wait for each required JDTLS startup signal."""
237+ configured_timeout = self ._custom_settings .get ("startup_timeout" , self .STARTUP_TIMEOUT )
238+ try :
239+ timeout = float (configured_timeout )
240+ except (TypeError , ValueError ) as exc :
241+ raise SolidLSPException ("java.startup_timeout must be a positive finite number" ) from exc
242+
243+ if not math .isfinite (timeout ) or timeout <= 0 :
244+ raise SolidLSPException ("java.startup_timeout must be a positive finite number" )
245+ return timeout
246+
247+ def _set_startup_phase (self , phase : str ) -> None :
248+ self ._startup_phase = phase
249+ log .info ("JDTLS startup phase: %s" , phase )
250+
251+ def _handle_language_status (self , params : dict ) -> None :
252+ log .info ("Language status update: %s" , params )
253+ status_type = params .get ("type" )
254+ status_message = params .get ("message" )
255+ self ._last_language_status = (status_type , status_message )
256+
257+ if status_type == "ServiceReady" and status_message == "ServiceReady" :
258+ self ._service_ready_event .set ()
259+ if status_type == "ProjectStatus" and status_message == "OK" :
260+ self ._project_ready_event .set ()
261+
262+ def _describe_last_language_status (self ) -> str :
263+ if self ._last_language_status is None :
264+ return "none received"
265+ status_type , status_message = self ._last_language_status
266+ return f"type={ status_type !r} , message={ status_message !r} "
267+
268+ def _wait_for_startup_signal (self , event : threading .Event , signal_name : str ) -> None :
269+ timeout = self ._get_startup_timeout ()
270+ phase = f"waiting_for_{ signal_name } "
271+ self ._set_startup_phase (phase )
272+ log .info ("Waiting up to %g seconds for JDTLS %s" , timeout , signal_name )
273+
274+ if event .wait (timeout = timeout ):
275+ self ._set_startup_phase (f"{ signal_name } _received" )
276+ return
277+
278+ message = (
279+ f"JDTLS startup timed out after { timeout :g} seconds while waiting for { signal_name } "
280+ f"(phase={ phase } , last_language_status={ self ._describe_last_language_status ()} )"
281+ )
282+ log .error (message )
283+ self .stop (shutdown_timeout = self .STARTUP_SHUTDOWN_TIMEOUT )
284+ raise SolidLSPException (message )
224285
225286 def _create_dependency_provider (self ) -> LanguageServerDependencyProvider :
226287 ls_resources_dir = self .ls_resources_dir (self ._solidlsp_settings )
@@ -1383,14 +1444,6 @@ def register_capability_handler(params: dict) -> None:
13831444 self ._intellicode_enable_command_available .set ()
13841445 return
13851446
1386- def lang_status_handler (params : dict ) -> None :
1387- log .info ("Language status update: %s" , params )
1388- if params ["type" ] == "ServiceReady" and params ["message" ] == "ServiceReady" :
1389- self ._service_ready_event .set ()
1390- if params ["type" ] == "ProjectStatus" :
1391- if params ["message" ] == "OK" :
1392- self ._project_ready_event .set ()
1393-
13941447 def execute_client_command_handler (params : dict ) -> list :
13951448 assert params ["command" ] == "_java.reloadBundles.command"
13961449 assert params ["arguments" ] == []
@@ -1403,18 +1456,18 @@ def do_nothing(params: dict) -> None:
14031456 return
14041457
14051458 self .server .on_request ("client/registerCapability" , register_capability_handler )
1406- self .server .on_notification ("language/status" , lang_status_handler )
1459+ self .server .on_notification ("language/status" , self . _handle_language_status )
14071460 self .server .on_notification ("window/logMessage" , window_log_message )
14081461 self .server .on_request ("workspace/executeClientCommand" , execute_client_command_handler )
14091462 self .server .on_notification ("$/progress" , do_nothing )
14101463 self .server .on_notification ("textDocument/publishDiagnostics" , do_nothing )
14111464 self .server .on_notification ("language/actionableNotification" , do_nothing )
14121465
1413- log . info ( "Starting EclipseJDTLS server process " )
1466+ self . _set_startup_phase ( "starting_process " )
14141467 self .server .start ()
14151468 initialize_params = self ._create_initialize_params ()
14161469
1417- log . info ( "Sending initialize request from LSP client to LSP server and awaiting response " )
1470+ self . _set_startup_phase ( "waiting_for_initialize_response " )
14181471 init_response = self .server .send .initialize (initialize_params )
14191472 assert init_response ["capabilities" ]["textDocumentSync" ]["change" ] == 2 # type: ignore
14201473 assert "completionProvider" not in init_response ["capabilities" ]
@@ -1428,7 +1481,10 @@ def do_nothing(params: dict) -> None:
14281481 # IntelliCode bundle is shipped. In upstream-jdtls mode it's absent and the
14291482 # 'java.intellicode.enable' command will never be registered, so we skip the wait/call.
14301483 if self .runtime_dependency_paths .intellicode_jar_path is not None :
1431- self ._intellicode_enable_command_available .wait ()
1484+ self ._wait_for_startup_signal (
1485+ self ._intellicode_enable_command_available ,
1486+ "intellicode_command_registration" ,
1487+ )
14321488
14331489 java_intellisense_members_path = self .runtime_dependency_paths .intellisense_members_path
14341490 assert java_intellisense_members_path is not None
@@ -1440,13 +1496,12 @@ def do_nothing(params: dict) -> None:
14401496 }
14411497 )
14421498 assert intellicode_enable_result
1499+ self ._set_startup_phase ("intellicode_enabled" )
14431500
1444- if not self ._service_ready_event .is_set ():
1445- log .info ("Waiting for service to be ready ..." )
1446- self ._service_ready_event .wait ()
1447- log .info ("Service is ready" )
1501+ self ._wait_for_startup_signal (self ._service_ready_event , "service_ready" )
14481502
14491503 if not self ._project_ready_event .is_set ():
1504+ self ._set_startup_phase ("waiting_for_project_status" )
14501505 log .info ("Waiting for project to be ready ..." )
14511506 project_ready_timeout = 20 # Hotfix: Using timeout until we figure out why sometimes we don't get the project ready event
14521507 if self ._project_ready_event .wait (timeout = project_ready_timeout ):
@@ -1456,7 +1511,7 @@ def do_nothing(params: dict) -> None:
14561511 else :
14571512 log .info ("Project is ready" )
14581513
1459- log . info ( "Startup complete" )
1514+ self . _set_startup_phase ( " complete" )
14601515
14611516 @override
14621517 def _request_hover (self , file_buffer : LSPFileBuffer , line : int , column : int ) -> ls_types .Hover | None :
0 commit comments