base_request.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # Copyright (C) 2013 Google Inc.
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. import requests
  18. import urlparse
  19. from base64 import b64decode, b64encode
  20. from retries import retries
  21. from requests_futures.sessions import FuturesSession
  22. from ycm.unsafe_thread_pool_executor import UnsafeThreadPoolExecutor
  23. from ycm import vimsupport
  24. from ycmd.utils import ToUtf8Json
  25. from ycmd.hmac_utils import CreateRequestHmac, CreateHmac, SecureStringsEqual
  26. from ycmd.responses import ServerError, UnknownExtraConf
  27. _HEADERS = {'content-type': 'application/json'}
  28. _EXECUTOR = UnsafeThreadPoolExecutor( max_workers = 30 )
  29. # Setting this to None seems to screw up the Requests/urllib3 libs.
  30. _DEFAULT_TIMEOUT_SEC = 30
  31. _HMAC_HEADER = 'x-ycm-hmac'
  32. class BaseRequest( object ):
  33. def __init__( self ):
  34. pass
  35. def Start( self ):
  36. pass
  37. def Done( self ):
  38. return True
  39. def Response( self ):
  40. return {}
  41. # This method blocks
  42. # |timeout| is num seconds to tolerate no response from server before giving
  43. # up; see Requests docs for details (we just pass the param along).
  44. @staticmethod
  45. def GetDataFromHandler( handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  46. return JsonFromFuture( BaseRequest._TalkToHandlerAsync( '',
  47. handler,
  48. 'GET',
  49. timeout ) )
  50. # This is the blocking version of the method. See below for async.
  51. # |timeout| is num seconds to tolerate no response from server before giving
  52. # up; see Requests docs for details (we just pass the param along).
  53. @staticmethod
  54. def PostDataToHandler( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  55. return JsonFromFuture( BaseRequest.PostDataToHandlerAsync( data,
  56. handler,
  57. timeout ) )
  58. # This returns a future! Use JsonFromFuture to get the value.
  59. # |timeout| is num seconds to tolerate no response from server before giving
  60. # up; see Requests docs for details (we just pass the param along).
  61. @staticmethod
  62. def PostDataToHandlerAsync( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  63. return BaseRequest._TalkToHandlerAsync( data, handler, 'POST', timeout )
  64. # This returns a future! Use JsonFromFuture to get the value.
  65. # |method| is either 'POST' or 'GET'.
  66. # |timeout| is num seconds to tolerate no response from server before giving
  67. # up; see Requests docs for details (we just pass the param along).
  68. @staticmethod
  69. def _TalkToHandlerAsync( data,
  70. handler,
  71. method,
  72. timeout = _DEFAULT_TIMEOUT_SEC ):
  73. def SendRequest( data, handler, method, timeout ):
  74. request_uri = _BuildUri( handler )
  75. if method == 'POST':
  76. sent_data = ToUtf8Json( data )
  77. return BaseRequest.session.post(
  78. request_uri,
  79. data = sent_data,
  80. headers = BaseRequest._ExtraHeaders( method,
  81. request_uri,
  82. sent_data ),
  83. timeout = timeout )
  84. if method == 'GET':
  85. return BaseRequest.session.get(
  86. request_uri,
  87. headers = BaseRequest._ExtraHeaders( method, request_uri ),
  88. timeout = timeout )
  89. @retries( 5, delay = 0.5, backoff = 1.5 )
  90. def DelayedSendRequest( data, handler, method ):
  91. request_uri = _BuildUri( handler )
  92. if method == 'POST':
  93. sent_data = ToUtf8Json( data )
  94. return requests.post(
  95. request_uri,
  96. data = sent_data,
  97. headers = BaseRequest._ExtraHeaders( method,
  98. request_uri,
  99. sent_data ) )
  100. if method == 'GET':
  101. return requests.get(
  102. request_uri,
  103. headers = BaseRequest._ExtraHeaders( method, request_uri ) )
  104. if not _CheckServerIsHealthyWithCache():
  105. return _EXECUTOR.submit( DelayedSendRequest, data, handler, method )
  106. return SendRequest( data, handler, method, timeout )
  107. @staticmethod
  108. def _ExtraHeaders( method, request_uri, request_body = None ):
  109. if not request_body:
  110. request_body = ''
  111. headers = dict( _HEADERS )
  112. headers[ _HMAC_HEADER ] = b64encode(
  113. CreateRequestHmac( method,
  114. urlparse.urlparse( request_uri ).path,
  115. request_body,
  116. BaseRequest.hmac_secret ) )
  117. return headers
  118. session = FuturesSession( executor = _EXECUTOR )
  119. server_location = ''
  120. hmac_secret = ''
  121. def BuildRequestData( include_buffer_data = True ):
  122. line, column = vimsupport.CurrentLineAndColumn()
  123. filepath = vimsupport.GetCurrentBufferFilepath()
  124. request_data = {
  125. 'line_num': line + 1,
  126. 'column_num': column + 1,
  127. 'filepath': filepath
  128. }
  129. if include_buffer_data:
  130. request_data[ 'file_data' ] = vimsupport.GetUnsavedAndCurrentBufferData()
  131. return request_data
  132. def JsonFromFuture( future ):
  133. response = future.result()
  134. _ValidateResponseObject( response )
  135. if response.status_code == requests.codes.server_error:
  136. raise MakeServerException( response.json() )
  137. # We let Requests handle the other status types, we only handle the 500
  138. # error code.
  139. response.raise_for_status()
  140. if response.text:
  141. return response.json()
  142. return None
  143. def HandleServerException( exception ):
  144. serialized_exception = str( exception )
  145. # We ignore the exception about the file already being parsed since it comes
  146. # up often and isn't something that's actionable by the user.
  147. if 'already being parsed' in serialized_exception:
  148. return
  149. vimsupport.PostVimMessage( serialized_exception )
  150. def _ValidateResponseObject( response ):
  151. hmac = CreateHmac( response.content, BaseRequest.hmac_secret )
  152. if not SecureStringsEqual( hmac,
  153. b64decode( response.headers[ _HMAC_HEADER ] ) ):
  154. raise RuntimeError( 'Received invalid HMAC for response!' )
  155. return True
  156. def _BuildUri( handler ):
  157. return urlparse.urljoin( BaseRequest.server_location, handler )
  158. SERVER_HEALTHY = False
  159. def _CheckServerIsHealthyWithCache():
  160. global SERVER_HEALTHY
  161. def _ServerIsHealthy():
  162. request_uri = _BuildUri( 'healthy' )
  163. response = requests.get( request_uri,
  164. headers = BaseRequest._ExtraHeaders(
  165. 'GET', request_uri, '' ) )
  166. _ValidateResponseObject( response )
  167. response.raise_for_status()
  168. return response.json()
  169. if SERVER_HEALTHY:
  170. return True
  171. try:
  172. SERVER_HEALTHY = _ServerIsHealthy()
  173. return SERVER_HEALTHY
  174. except:
  175. return False
  176. def MakeServerException( data ):
  177. if data[ 'exception' ][ 'TYPE' ] == UnknownExtraConf.__name__:
  178. return UnknownExtraConf( data[ 'exception' ][ 'extra_conf_file' ] )
  179. return ServerError( '{0}: {1}'.format( data[ 'exception' ][ 'TYPE' ],
  180. data[ 'message' ] ) )