diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index d8a7ee7222..552a9d63c8 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -1016,7 +1016,6 @@ def __init__( self, user_options, connection_type = 'stdio' ): self._server_keep_logfiles = user_options[ 'server_keep_logfiles' ] self._stdout_file = None self._stderr_file = None - self._server_started = False self._Reset() @@ -1053,6 +1052,7 @@ def ServerReset( self ): self._extra_conf_dir = None self._semantic_token_atlas = None self._server_workspace_dirs = set() + self._server_started = False def GetCompleterName( self ): @@ -1912,21 +1912,29 @@ def _StartAndInitializeServer( self, request_data, *args, **kwargs ): StartServer. In general, completers don't need to call this as it is called automatically in OnFileReadyToParse, but this may be used in completer subcommands that require restarting the underlying server.""" - self._server_started = False - self._extra_conf_dir = self._GetSettingsFromExtraConf( request_data ) + try: + # Only attempt to start the server once - _server_started must be + # checked and set in a mutex to prevent race conditions. + with self._server_info_mutex: + if self._server_started: + LOGGER.debug( 'Server %s already started from another thread', + self.GetServerName() ) + return + self._server_started = True - # Only attempt to start the server once. Set this after above call as it may - # throw an exception - self._server_started = True + self._extra_conf_dir = self._GetSettingsFromExtraConf( request_data ) - if self.StartServer( request_data, *args, **kwargs ): - self._SendInitialize( request_data ) + if self.StartServer( request_data, *args, **kwargs ): + self._SendInitialize( request_data ) + + except Exception: + LOGGER.exception( 'Error while starting %s', self.GetServerName() ) + # reset _server_started + self.Shutdown() def OnFileReadyToParse( self, request_data ): - if not self.ServerIsHealthy() and not self._server_started: - # We have to get the settings before starting the server, as this call - # might throw UnknownExtraConf. + if not self.ServerIsHealthy(): self._StartAndInitializeServer( request_data ) if not self.ServerIsHealthy(): @@ -2033,10 +2041,9 @@ def _AwaitServerMessages( self, request_data, timeout ): # restarted while this loop is running. self._initialize_event.wait( timeout=timeout ) - # If the timeout is hit waiting for the server to be ready, after we - # tried to start the server, we return False and kill the message - # poll. - return not self._server_started or self._initialize_event.is_set() + # If the timeout is hit waiting for the server to be ready and + # initialized, we return False and kill the message poll. + return self._initialize_event.is_set() if not self.GetConnection(): # The server isn't running or something. Don't re-poll, as this will diff --git a/ycmd/tests/language_server/language_server_completer_test.py b/ycmd/tests/language_server/language_server_completer_test.py index 8db91ea36b..4550e1b708 100644 --- a/ycmd/tests/language_server/language_server_completer_test.py +++ b/ycmd/tests/language_server/language_server_completer_test.py @@ -1482,11 +1482,11 @@ def test_LanguageServerCompleter_Diagnostics_PercentEncodeCannonical( @IsolatedYcmd() @patch.object( completer, 'MESSAGE_POLL_TIMEOUT', 0.01 ) - def test_LanguageServerCompleter_PollForMessages_ServerNotStarted( + def test_LanguageServerCompleter_PollForMessages_ServerStartTooLong( self, app ): server = MockCompleter() request_data = RequestWrap( BuildRequest() ) - assert_that( server.PollForMessages( request_data ), equal_to( True ) ) + assert_that( server.PollForMessages( request_data ), equal_to( False ) ) @IsolatedYcmd()