diff --git a/python/ycm/server/handlers.py b/python/ycm/server/handlers.py new file mode 100644 index 00000000..2ebf1c68 --- /dev/null +++ b/python/ycm/server/handlers.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +import atexit +import logging +import json +import bottle +import httplib +from bottle import request, response +import server_state +from ycm import user_options_store +from ycm.server.responses import BuildExceptionResponse +from ycm import extra_conf_store + +# num bytes for the request body buffer; request.json only works if the request +# size is less than this +bottle.Request.MEMFILE_MAX = 300 * 1024 + +# TODO: rename these to _lower_case +SERVER_STATE = None +LOGGER = logging.getLogger( __name__ ) +app = bottle.Bottle() + + +@app.post( '/event_notification' ) +def EventNotification(): + LOGGER.info( 'Received event notification' ) + request_data = request.json + event_name = request_data[ 'event_name' ] + LOGGER.debug( 'Event name: %s', event_name ) + + event_handler = 'On' + event_name + getattr( SERVER_STATE.GetGeneralCompleter(), event_handler )( request_data ) + + filetypes = request_data[ 'filetypes' ] + response_data = None + if SERVER_STATE.FiletypeCompletionUsable( filetypes ): + response_data = getattr( SERVER_STATE.GetFiletypeCompleter( filetypes ), + event_handler )( request_data ) + + if response_data: + return _JsonResponse( response_data ) + + +@app.post( '/run_completer_command' ) +def RunCompleterCommand(): + LOGGER.info( 'Received command request' ) + request_data = request.json + completer = _GetCompleterForRequestData( request_data ) + + return _JsonResponse( completer.OnUserCommand( + request_data[ 'command_arguments' ], + request_data ) ) + + +@app.post( '/completions' ) +def GetCompletions(): + LOGGER.info( 'Received completion request' ) + request_data = request.json + do_filetype_completion = SERVER_STATE.ShouldUseFiletypeCompleter( + request_data ) + LOGGER.debug( 'Using filetype completion: %s', do_filetype_completion ) + filetypes = request_data[ 'filetypes' ] + completer = ( SERVER_STATE.GetFiletypeCompleter( filetypes ) if + do_filetype_completion else + SERVER_STATE.GetGeneralCompleter() ) + + return _JsonResponse( completer.ComputeCandidates( request_data ) ) + + +@app.get( '/user_options' ) +def GetUserOptions(): + LOGGER.info( 'Received user options GET request' ) + return _JsonResponse( dict( SERVER_STATE.user_options ) ) + + +@app.get( '/healthy' ) +def GetHealthy(): + LOGGER.info( 'Received health request' ) + return _JsonResponse( True ) + + +@app.post( '/user_options' ) +def SetUserOptions(): + LOGGER.info( 'Received user options POST request' ) + UpdateUserOptions( request.json ) + + +@app.post( '/semantic_completion_available' ) +def FiletypeCompletionAvailable(): + LOGGER.info( 'Received filetype completion available request' ) + return _JsonResponse( SERVER_STATE.FiletypeCompletionAvailable( + request.json[ 'filetypes' ] ) ) + + +@app.post( '/defined_subcommands' ) +def DefinedSubcommands(): + LOGGER.info( 'Received defined subcommands request' ) + completer = _GetCompleterForRequestData( request.json ) + + return _JsonResponse( completer.DefinedSubcommands() ) + + +@app.post( '/detailed_diagnostic' ) +def GetDetailedDiagnostic(): + LOGGER.info( 'Received detailed diagnostic request' ) + request_data = request.json + completer = _GetCompleterForRequestData( request_data ) + + return _JsonResponse( completer.GetDetailedDiagnostic( request_data ) ) + + +@app.post( '/load_extra_conf_file' ) +def LoadExtraConfFile(): + LOGGER.info( 'Received extra conf load request' ) + request_data = request.json + extra_conf_store.Load( request_data[ 'filepath' ], force = True ) + + +@app.post( '/debug_info' ) +def DebugInfo(): + # This can't be at the top level because of possible extra conf preload + import ycm_core + LOGGER.info( 'Received debug info request' ) + + output = [] + has_clang_support = ycm_core.HasClangSupport() + output.append( 'Server has Clang support compiled in: {0}'.format( + has_clang_support ) ) + + if has_clang_support: + output.append( 'Clang version: ' + ycm_core.ClangVersion() ) + + request_data = request.json + try: + output.append( + _GetCompleterForRequestData( request_data ).DebugInfo( request_data) ) + except: + pass + return _JsonResponse( '\n'.join( output ) ) + + +# The type of the param is Bottle.HTTPError +@app.error( httplib.INTERNAL_SERVER_ERROR ) +def ErrorHandler( httperror ): + return _JsonResponse( BuildExceptionResponse( httperror.exception, + httperror.traceback ) ) + + +def _JsonResponse( data ): + response.set_header( 'Content-Type', 'application/json' ) + return json.dumps( data, default = _UniversalSerialize ) + + +def _UniversalSerialize( obj ): + serialized = obj.__dict__.copy() + serialized[ 'TYPE' ] = type( obj ).__name__ + return serialized + + +def _GetCompleterForRequestData( request_data ): + completer_target = request_data.get( 'completer_target', None ) + + if completer_target == 'identifier': + return SERVER_STATE.GetGeneralCompleter().GetIdentifierCompleter() + elif completer_target == 'filetype_default' or not completer_target: + return SERVER_STATE.GetFiletypeCompleter( request_data[ 'filetypes' ] ) + else: + return SERVER_STATE.GetFiletypeCompleter( [ completer_target ] ) + + +@atexit.register +def _ServerShutdown(): + if SERVER_STATE: + SERVER_STATE.Shutdown() + extra_conf_store.Shutdown() + + +def UpdateUserOptions( options ): + global SERVER_STATE + + if not options: + return + + user_options_store.SetAll( options ) + SERVER_STATE = server_state.ServerState( options ) + + +def SetServerStateToDefaults(): + global SERVER_STATE, LOGGER + LOGGER = logging.getLogger( __name__ ) + user_options_store.LoadDefaults() + SERVER_STATE = server_state.ServerState( user_options_store.GetAll() ) + extra_conf_store.Reset() diff --git a/python/ycm/server/server_state.py b/python/ycm/server/server_state.py index 0e831d33..39cd1243 100644 --- a/python/ycm/server/server_state.py +++ b/python/ycm/server/server_state.py @@ -19,7 +19,6 @@ import imp import os -from ycm import extra_conf_store from ycm.utils import ForceSemanticCompletion from ycm.completers.general.general_completer_store import GeneralCompleterStore from ycm.completers.completer_utils import PathToFiletypeCompleterPluginLoader @@ -30,7 +29,6 @@ class ServerState( object ): self._user_options = user_options self._filetype_completers = {} self._gencomp = GeneralCompleterStore( self._user_options ) - extra_conf_store.CallGlobalExtraConfYcmCorePreloadIfExists() @property @@ -44,7 +42,6 @@ class ServerState( object ): completer.Shutdown() self._gencomp.Shutdown() - extra_conf_store.Shutdown() def _GetFiletypeCompleterForFiletype( self, filetype ): diff --git a/python/ycm/server/server_utils.py b/python/ycm/server/server_utils.py new file mode 100644 index 00000000..96d566fe --- /dev/null +++ b/python/ycm/server/server_utils.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# This file is part of YouCompleteMe. +# +# YouCompleteMe is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# YouCompleteMe is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with YouCompleteMe. If not, see . + +import sys +import os + +def SetUpPythonPath(): + # We want to have the YouCompleteMe/python directory on the Python PATH + # because all the code already assumes that it's there. This is a relic from + # before the client/server architecture. + # TODO: Fix things so that this is not needed anymore when we split ycmd into + # a separate repository. + sys.path.insert( 0, os.path.join( + os.path.dirname( os.path.abspath( __file__ ) ), + '../..' ) ) + + from ycm import utils + utils.AddThirdPartyFoldersToSysPath() diff --git a/python/ycm/server/tests/basic_test.py b/python/ycm/server/tests/basic_test.py index 9618bc7a..6d226324 100644 --- a/python/ycm/server/tests/basic_test.py +++ b/python/ycm/server/tests/basic_test.py @@ -20,8 +20,10 @@ import os import httplib import time +from ..server_utils import SetUpPythonPath +SetUpPythonPath() from webtest import TestApp -from .. import ycmd +from .. import handlers from ..responses import BuildCompletionData, UnknownExtraConf from nose.tools import ok_, eq_, with_setup from hamcrest import ( assert_that, has_items, has_entry, contains, @@ -73,7 +75,7 @@ def CompletionEntryMatcher( insertion_text ): def Setup(): - ycmd.SetServerStateToDefaults() + handlers.SetServerStateToDefaults() def PathToTestDataDir(): @@ -87,7 +89,7 @@ def PathToTestFile( test_basename ): @with_setup( Setup ) def GetCompletions_IdentifierCompleter_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) event_data = BuildRequest( contents = 'foo foogoo ba', event_name = 'FileReadyToParse' ) @@ -104,7 +106,7 @@ def GetCompletions_IdentifierCompleter_Works_test(): @with_setup( Setup ) def GetCompletions_CsCompleter_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) filepath = PathToTestFile( 'testy/Program.cs' ) contents = open( filepath ).read() event_data = BuildRequest( filepath = filepath, @@ -138,7 +140,7 @@ def GetCompletions_CsCompleter_Works_test(): @with_setup( Setup ) def GetCompletions_ClangCompleter_WorksWithExplicitFlags_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ struct Foo { int x; @@ -170,7 +172,7 @@ int main() @with_setup( Setup ) def GetCompletions_ClangCompleter_UnknownExtraConfException_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) filepath = PathToTestFile( 'basic.cpp' ) completion_data = BuildRequest( filepath = filepath, filetype = 'cpp', @@ -189,7 +191,7 @@ def GetCompletions_ClangCompleter_UnknownExtraConfException_test(): @with_setup( Setup ) def GetCompletions_ClangCompleter_WorksWhenExtraConfExplicitlyAllowed_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) app.post_json( '/load_extra_conf_file', { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } ) @@ -209,7 +211,7 @@ def GetCompletions_ClangCompleter_WorksWhenExtraConfExplicitlyAllowed_test(): @with_setup( Setup ) def GetCompletions_ClangCompleter_ForceSemantic_OnlyFileteredCompletions_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ int main() { @@ -241,7 +243,7 @@ int main() @with_setup( Setup ) def GetCompletions_ForceSemantic_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) completion_data = BuildRequest( filetype = 'python', force_semantic = True ) @@ -254,7 +256,7 @@ def GetCompletions_ForceSemantic_Works_test(): @with_setup( Setup ) def GetCompletions_IdentifierCompleter_SyntaxKeywordsAdded_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) event_data = BuildRequest( event_name = 'FileReadyToParse', syntax_keywords = ['foo', 'bar', 'zoo'] ) @@ -271,7 +273,7 @@ def GetCompletions_IdentifierCompleter_SyntaxKeywordsAdded_test(): @with_setup( Setup ) def GetCompletions_UltiSnipsCompleter_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) event_data = BuildRequest( event_name = 'BufferVisit', ultisnips_snippets = [ @@ -292,7 +294,7 @@ def GetCompletions_UltiSnipsCompleter_Works_test(): @with_setup( Setup ) def RunCompleterCommand_GoTo_Jedi_ZeroBasedLineAndColumn_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ def foo(): pass @@ -318,7 +320,7 @@ foo() @with_setup( Setup ) def RunCompleterCommand_GoTo_Clang_ZeroBasedLineAndColumn_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ struct Foo { int x; @@ -352,7 +354,7 @@ int main() @with_setup( Setup ) def DefinedSubcommands_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) subcommands_data = BuildRequest( completer_target = 'python' ) eq_( [ 'GoToDefinition', @@ -363,7 +365,7 @@ def DefinedSubcommands_Works_test(): @with_setup( Setup ) def DefinedSubcommands_WorksWhenNoExplicitCompleterTargetSpecified_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) subcommands_data = BuildRequest( filetype = 'python' ) eq_( [ 'GoToDefinition', @@ -374,7 +376,7 @@ def DefinedSubcommands_WorksWhenNoExplicitCompleterTargetSpecified_test(): @with_setup( Setup ) def Diagnostics_ClangCompleter_ZeroBasedLineAndColumn_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ struct Foo { int x // semicolon missing here! @@ -399,7 +401,7 @@ struct Foo { @with_setup( Setup ) def GetDetailedDiagnostic_ClangCompleter_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) contents = """ struct Foo { int x // semicolon missing here! @@ -427,7 +429,7 @@ struct Foo { @with_setup( Setup ) def FiletypeCompletionAvailable_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) request_data = { 'filetypes': ['python'] } @@ -438,7 +440,7 @@ def FiletypeCompletionAvailable_Works_test(): @with_setup( Setup ) def UserOptions_Works_test(): - app = TestApp( ycmd.app ) + app = TestApp( handlers.app ) options = app.get( '/user_options' ).json ok_( len( options ) ) diff --git a/python/ycm/server/ycmd.py b/python/ycm/server/ycmd.py index ec20875c..a0c78247 100755 --- a/python/ycm/server/ycmd.py +++ b/python/ycm/server/ycmd.py @@ -17,214 +17,24 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +from server_utils import SetUpPythonPath +SetUpPythonPath() + import sys -import os -import atexit - -# We want to have the YouCompleteMe/python directory on the Python PATH because -# all the code already assumes that it's there. This is a relic from before the -# client/server architecture. -# TODO: Fix things so that this is not needed anymore when we split ycmd into a -# separate repository. -sys.path.insert( 0, os.path.join( - os.path.dirname( os.path.abspath( __file__ ) ), - '../..' ) ) - -from ycm import utils -utils.AddThirdPartyFoldersToSysPath() - import logging import json -import bottle import argparse -import httplib import waitress -from bottle import request, response -import server_state from ycm import user_options_store -from ycm.server.responses import BuildExceptionResponse from ycm import extra_conf_store -# num bytes for the request body buffer; request.json only works if the request -# size is less than this -bottle.Request.MEMFILE_MAX = 300 * 1024 -# TODO: rename these to _lower_case -SERVER_STATE = None -LOGGER = None -app = bottle.Bottle() - - -@app.post( '/event_notification' ) -def EventNotification(): - LOGGER.info( 'Received event notification' ) - request_data = request.json - event_name = request_data[ 'event_name' ] - LOGGER.debug( 'Event name: %s', event_name ) - - event_handler = 'On' + event_name - getattr( SERVER_STATE.GetGeneralCompleter(), event_handler )( request_data ) - - filetypes = request_data[ 'filetypes' ] - response_data = None - if SERVER_STATE.FiletypeCompletionUsable( filetypes ): - response_data = getattr( SERVER_STATE.GetFiletypeCompleter( filetypes ), - event_handler )( request_data ) - - if response_data: - return _JsonResponse( response_data ) - - -@app.post( '/run_completer_command' ) -def RunCompleterCommand(): - LOGGER.info( 'Received command request' ) - request_data = request.json - completer = _GetCompleterForRequestData( request_data ) - - return _JsonResponse( completer.OnUserCommand( - request_data[ 'command_arguments' ], - request_data ) ) - - -@app.post( '/completions' ) -def GetCompletions(): - LOGGER.info( 'Received completion request' ) - request_data = request.json - do_filetype_completion = SERVER_STATE.ShouldUseFiletypeCompleter( - request_data ) - LOGGER.debug( 'Using filetype completion: %s', do_filetype_completion ) - filetypes = request_data[ 'filetypes' ] - completer = ( SERVER_STATE.GetFiletypeCompleter( filetypes ) if - do_filetype_completion else - SERVER_STATE.GetGeneralCompleter() ) - - return _JsonResponse( completer.ComputeCandidates( request_data ) ) - - -@app.get( '/user_options' ) -def GetUserOptions(): - LOGGER.info( 'Received user options GET request' ) - return _JsonResponse( dict( SERVER_STATE.user_options ) ) - - -@app.get( '/healthy' ) -def GetHealthy(): - LOGGER.info( 'Received health request' ) - return _JsonResponse( True ) - - -@app.post( '/user_options' ) -def SetUserOptions(): - LOGGER.info( 'Received user options POST request' ) - _SetUserOptions( request.json ) - - -@app.post( '/semantic_completion_available' ) -def FiletypeCompletionAvailable(): - LOGGER.info( 'Received filetype completion available request' ) - return _JsonResponse( SERVER_STATE.FiletypeCompletionAvailable( - request.json[ 'filetypes' ] ) ) - - -@app.post( '/defined_subcommands' ) -def DefinedSubcommands(): - LOGGER.info( 'Received defined subcommands request' ) - completer = _GetCompleterForRequestData( request.json ) - - return _JsonResponse( completer.DefinedSubcommands() ) - - -@app.post( '/detailed_diagnostic' ) -def GetDetailedDiagnostic(): - LOGGER.info( 'Received detailed diagnostic request' ) - request_data = request.json - completer = _GetCompleterForRequestData( request_data ) - - return _JsonResponse( completer.GetDetailedDiagnostic( request_data ) ) - - -@app.post( '/load_extra_conf_file' ) -def LoadExtraConfFile(): - LOGGER.info( 'Received extra conf load request' ) - request_data = request.json - extra_conf_store.Load( request_data[ 'filepath' ], force = True ) - - -@app.post( '/debug_info' ) -def DebugInfo(): - # This can't be at the top level because of possible extra conf preload - import ycm_core - LOGGER.info( 'Received debug info request' ) - - output = [] - has_clang_support = ycm_core.HasClangSupport() - output.append( 'Server has Clang support compiled in: {0}'.format( - has_clang_support ) ) - - if has_clang_support: - output.append( 'Clang version: ' + ycm_core.ClangVersion() ) - - request_data = request.json - try: - output.append( - _GetCompleterForRequestData( request_data ).DebugInfo( request_data) ) - except: - pass - return _JsonResponse( '\n'.join( output ) ) - - -# The type of the param is Bottle.HTTPError -@app.error( httplib.INTERNAL_SERVER_ERROR ) -def ErrorHandler( httperror ): - return _JsonResponse( BuildExceptionResponse( httperror.exception, - httperror.traceback ) ) - - -def _JsonResponse( data ): - response.set_header( 'Content-Type', 'application/json' ) - return json.dumps( data, default = _UniversalSerialize ) - - -def _UniversalSerialize( obj ): - serialized = obj.__dict__.copy() - serialized[ 'TYPE' ] = type( obj ).__name__ - return serialized - - -def _GetCompleterForRequestData( request_data ): - completer_target = request_data.get( 'completer_target', None ) - - if completer_target == 'identifier': - return SERVER_STATE.GetGeneralCompleter().GetIdentifierCompleter() - elif completer_target == 'filetype_default' or not completer_target: - return SERVER_STATE.GetFiletypeCompleter( request_data[ 'filetypes' ] ) - else: - return SERVER_STATE.GetFiletypeCompleter( [ completer_target ] ) - - -@atexit.register -def _ServerShutdown(): - if SERVER_STATE: - SERVER_STATE.Shutdown() - - -def _SetUserOptions( options ): - global SERVER_STATE - - user_options_store.SetAll( options ) - SERVER_STATE = server_state.ServerState( options ) - - -def SetServerStateToDefaults(): - global SERVER_STATE, LOGGER - LOGGER = logging.getLogger( __name__ ) - user_options_store.LoadDefaults() - SERVER_STATE = server_state.ServerState( user_options_store.GetAll() ) - extra_conf_store.Reset() +def YcmCoreSanityCheck(): + if 'ycm_core' in sys.modules: + raise RuntimeError( 'ycm_core already imported, ycmd has a bug!' ) def Main(): - global LOGGER parser = argparse.ArgumentParser() parser.add_argument( '--host', type = str, default = 'localhost', help = 'server hostname') @@ -237,18 +47,31 @@ def Main(): help = 'file with user options, in JSON format' ) args = parser.parse_args() - if args.options_file: - _SetUserOptions( json.load( open( args.options_file, 'r' ) ) ) - numeric_level = getattr( logging, args.log.upper(), None ) if not isinstance( numeric_level, int ): raise ValueError( 'Invalid log level: %s' % args.log ) + # Has to be called before any call to logging.getLogger() logging.basicConfig( format = '%(asctime)s - %(levelname)s - %(message)s', level = numeric_level ) - LOGGER = logging.getLogger( __name__ ) - waitress.serve( app, host = args.host, port = args.port, threads = 10 ) + options = None + if args.options_file: + options = json.load( open( args.options_file, 'r' ) ) + user_options_store.SetAll( options ) + # This ensures that ycm_core is not loaded before extra conf preload + # was run. + YcmCoreSanityCheck() + extra_conf_store.CallGlobalExtraConfYcmCorePreloadIfExists() + + # This can't be a top-level import because it transitively imports ycm_core + # which we want to be imported ONLY after extra conf preload has executed. + import handlers + handlers.UpdateUserOptions( options ) + waitress.serve( handlers.app, + host = args.host, + port = args.port, + threads = 10 ) if __name__ == "__main__":