handlers.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. """ tornado handler for managing and communicating with language servers
  2. """
  3. from typing import Optional, Text
  4. from jupyter_core.utils import ensure_async
  5. from jupyter_server.base.handlers import APIHandler, JupyterHandler
  6. from jupyter_server.utils import url_path_join as ujoin
  7. from tornado import web
  8. from tornado.websocket import WebSocketHandler
  9. try:
  10. from jupyter_server.auth.decorator import authorized
  11. except ImportError:
  12. def authorized(method): # type: ignore
  13. """A no-op fallback for `jupyter_server 1.x`"""
  14. return method
  15. try:
  16. from jupyter_server.base.websocket import WebSocketMixin
  17. except ImportError:
  18. from jupyter_server.base.zmqhandlers import WebSocketMixin
  19. from .manager import LanguageServerManager
  20. from .schema import SERVERS_RESPONSE
  21. from .specs.utils import censored_spec
  22. AUTH_RESOURCE = "lsp"
  23. class BaseHandler(APIHandler):
  24. manager = None # type: LanguageServerManager
  25. def initialize(self, manager: LanguageServerManager):
  26. self.manager = manager
  27. class BaseJupyterHandler(JupyterHandler):
  28. manager = None # type: LanguageServerManager
  29. def initialize(self, manager: LanguageServerManager):
  30. self.manager = manager
  31. class LanguageServerWebSocketHandler( # type: ignore
  32. WebSocketMixin, WebSocketHandler, BaseJupyterHandler
  33. ):
  34. """Setup tornado websocket to route to language server sessions.
  35. The logic of `get` and `pre_get` methods is derived from jupyter-server ws handlers,
  36. and should be kept in sync to follow best practice established by upstream; see:
  37. https://github.com/jupyter-server/jupyter_server/blob/v2.12.5/jupyter_server/services/kernels/websocket.py#L36
  38. """
  39. auth_resource = AUTH_RESOURCE
  40. language_server: Optional[Text] = None
  41. async def pre_get(self):
  42. """Handle a pre_get."""
  43. # authenticate first
  44. # authenticate the request before opening the websocket
  45. user = self.current_user
  46. if user is None:
  47. self.log.warning("Couldn't authenticate WebSocket connection")
  48. raise web.HTTPError(403)
  49. if not hasattr(self, "authorizer"):
  50. return
  51. # authorize the user.
  52. is_authorized = await ensure_async(
  53. self.authorizer.is_authorized(self, user, "execute", AUTH_RESOURCE)
  54. )
  55. if not is_authorized:
  56. raise web.HTTPError(403)
  57. async def get(self, *args, **kwargs):
  58. """Get an event socket."""
  59. await self.pre_get()
  60. res = super().get(*args, **kwargs)
  61. if res is not None:
  62. await res
  63. async def open(self, language_server):
  64. await self.manager.ready()
  65. self.language_server = language_server
  66. self.manager.subscribe(self)
  67. self.log.debug("[{}] Opened a handler".format(self.language_server))
  68. super().open()
  69. async def on_message(self, message):
  70. self.log.debug("[{}] Handling a message".format(self.language_server))
  71. await self.manager.on_client_message(message, self)
  72. def on_close(self):
  73. self.manager.unsubscribe(self)
  74. self.log.debug("[{}] Closed a handler".format(self.language_server))
  75. class LanguageServersHandler(BaseHandler):
  76. """Reports the status of all current servers
  77. Response should conform to schema in schema/servers.schema.json
  78. """
  79. auth_resource = AUTH_RESOURCE
  80. validator = SERVERS_RESPONSE
  81. @web.authenticated
  82. @authorized
  83. async def get(self):
  84. """finish with the JSON representations of the sessions"""
  85. await self.manager.ready()
  86. response = {
  87. "version": 2,
  88. "sessions": {
  89. language_server: session.to_json()
  90. for language_server, session in self.manager.sessions.items()
  91. },
  92. "specs": {
  93. key: censored_spec(spec)
  94. for key, spec in self.manager.all_language_servers.items()
  95. },
  96. }
  97. errors = list(self.validator.iter_errors(response))
  98. if errors: # pragma: no cover
  99. self.log.warning("{} validation errors: {}".format(len(errors), errors))
  100. self.finish(response)
  101. def add_handlers(nbapp):
  102. """Add Language Server routes to the notebook server web application"""
  103. lsp_url = ujoin(nbapp.base_url, "lsp")
  104. re_langservers = "(?P<language_server>.*)"
  105. opts = {"manager": nbapp.language_server_manager}
  106. nbapp.web_app.add_handlers(
  107. ".*",
  108. [
  109. (ujoin(lsp_url, "status"), LanguageServersHandler, opts),
  110. (
  111. ujoin(lsp_url, "ws", re_langservers),
  112. LanguageServerWebSocketHandler,
  113. opts,
  114. ),
  115. ],
  116. )