server.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2013 Strahinja Val Markovic <val@markovic.io>
  4. #
  5. # This file is part of YouCompleteMe.
  6. #
  7. # YouCompleteMe is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # YouCompleteMe is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  19. import sys
  20. import os
  21. import threading
  22. # We want to have the YouCompleteMe/python directory on the Python PATH because
  23. # all the code already assumes that it's there. This is a relic from before the
  24. # client/server architecture.
  25. # TODO: Fix things so that this is not needed anymore when we split ycmd into a
  26. # separate repository.
  27. sys.path.insert( 0, os.path.join(
  28. os.path.dirname( os.path.abspath( __file__ ) ),
  29. '../..' ) )
  30. import logging
  31. import time
  32. import httplib
  33. import json
  34. import bottle
  35. from bottle import run, request, response
  36. import server_state
  37. from ycm import extra_conf_store
  38. from ycm import user_options_store
  39. from ycm import utils
  40. import argparse
  41. # num bytes for the request body buffer; request.json only works if the request
  42. # size is less than this
  43. bottle.Request.MEMFILE_MAX = 300 * 1024
  44. user_options_store.LoadDefaults()
  45. SERVER_STATE = server_state.ServerState( user_options_store.GetAll() )
  46. LOGGER = logging.getLogger( __name__ )
  47. app = bottle.Bottle()
  48. @app.post( '/event_notification' )
  49. def EventNotification():
  50. LOGGER.info( 'Received event notification')
  51. request_data = request.json
  52. event_name = request_data[ 'event_name' ]
  53. LOGGER.debug( 'Event name: %s', event_name )
  54. event_handler = 'On' + event_name
  55. getattr( SERVER_STATE.GetGeneralCompleter(), event_handler )( request_data )
  56. filetypes = request_data[ 'filetypes' ]
  57. if SERVER_STATE.FiletypeCompletionUsable( filetypes ):
  58. getattr( SERVER_STATE.GetFiletypeCompleter( filetypes ),
  59. event_handler )( request_data )
  60. try:
  61. if hasattr( extra_conf_store, event_handler ):
  62. getattr( extra_conf_store, event_handler )( request_data )
  63. except OSError as e:
  64. LOGGER.exception( e )
  65. if event_name == 'VimLeave':
  66. _ScheduleServerShutdown()
  67. @app.post( '/run_completer_command' )
  68. def RunCompleterCommand():
  69. LOGGER.info( 'Received command request')
  70. request_data = request.json
  71. completer_target = request_data[ 'completer_target' ]
  72. if completer_target == 'identifier':
  73. completer = SERVER_STATE.GetGeneralCompleter()
  74. else:
  75. completer = SERVER_STATE.GetFiletypeCompleter()
  76. return _JsonResponse(
  77. completer.OnUserCommand( request_data[ 'command_arguments' ],
  78. request_data ) )
  79. @app.post( '/get_completions' )
  80. def GetCompletions():
  81. LOGGER.info( 'Received completion request')
  82. request_data = request.json
  83. do_filetype_completion = SERVER_STATE.ShouldUseFiletypeCompleter(
  84. request_data )
  85. LOGGER.debug( 'Using filetype completion: %s', do_filetype_completion )
  86. filetypes = request_data[ 'filetypes' ]
  87. completer = ( SERVER_STATE.GetFiletypeCompleter( filetypes ) if
  88. do_filetype_completion else
  89. SERVER_STATE.GetGeneralCompleter() )
  90. # This is necessary so that general_completer_store fills up
  91. # _current_query_completers.
  92. # TODO: Fix this.
  93. completer.ShouldUseNow( request_data )
  94. # TODO: This should not be async anymore, server is multi-threaded
  95. completer.CandidatesForQueryAsync( request_data )
  96. while not completer.AsyncCandidateRequestReady():
  97. time.sleep( 0.03 )
  98. return _JsonResponse( completer.CandidatesFromStoredRequest() )
  99. @app.route( '/user_options' )
  100. def UserOptions():
  101. global SERVER_STATE
  102. if request.method == 'GET':
  103. LOGGER.info( 'Received user options GET request')
  104. return SERVER_STATE.user_options
  105. elif request.method == 'POST':
  106. LOGGER.info( 'Received user options POST request')
  107. data = request.json
  108. SERVER_STATE = server_state.ServerState( data )
  109. user_options_store.SetAll( data )
  110. else:
  111. response.status = httplib.BAD_REQUEST
  112. @app.post( '/filetype_completion_available')
  113. def FiletypeCompletionAvailable():
  114. LOGGER.info( 'Received filetype completion available request')
  115. return _JsonResponse( SERVER_STATE.FiletypeCompletionAvailable(
  116. request.json[ 'filetypes' ] ) )
  117. def _JsonResponse( data ):
  118. response.set_header( 'Content-Type', 'application/json' )
  119. return json.dumps( data )
  120. def _ScheduleServerShutdown():
  121. # The reason why we want to schedule a shutdown in the near future instead of
  122. # just shutting down right now is because we want the current request (the one
  123. # that made us want to shutdown) to complete successfully first.
  124. def Shutdown():
  125. # sys.exit() doesn't work because we're not in the main thread.
  126. utils.TerminateProcess( os.getpid() )
  127. killer_thread = threading.Timer( 2, Shutdown )
  128. killer_thread.start()
  129. def Main():
  130. global LOGGER
  131. parser = argparse.ArgumentParser()
  132. parser.add_argument( '--host', type = str, default = 'localhost',
  133. help='server hostname')
  134. parser.add_argument( '--port', type = int, default = 6666,
  135. help='server port')
  136. parser.add_argument( '--log', type = str, default = 'info',
  137. help='log level, one of '
  138. '[debug|info|warning|error|critical]')
  139. args = parser.parse_args()
  140. numeric_level = getattr( logging, args.log.upper(), None )
  141. if not isinstance( numeric_level, int ):
  142. raise ValueError( 'Invalid log level: %s' % args.log )
  143. logging.basicConfig( format = '%(asctime)s - %(levelname)s - %(message)s',
  144. level = numeric_level )
  145. LOGGER = logging.getLogger( __name__ )
  146. run( app = app, host = args.host, port = args.port, server='cherrypy' )
  147. if __name__ == "__main__":
  148. Main()