handlers.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. """Tornado handlers for the sessions web service.
  2. Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#sessions-api
  3. """
  4. # Copyright (c) Jupyter Development Team.
  5. # Distributed under the terms of the Modified BSD License.
  6. import asyncio
  7. import json
  8. try:
  9. from jupyter_client.jsonutil import json_default
  10. except ImportError:
  11. from jupyter_client.jsonutil import date_default as json_default
  12. from jupyter_client.kernelspec import NoSuchKernel
  13. from jupyter_core.utils import ensure_async
  14. from tornado import web
  15. from jupyter_server.auth.decorator import authorized
  16. from jupyter_server.utils import url_path_join
  17. from ...base.handlers import APIHandler
  18. AUTH_RESOURCE = "sessions"
  19. class SessionsAPIHandler(APIHandler):
  20. """A Sessions API handler."""
  21. auth_resource = AUTH_RESOURCE
  22. class SessionRootHandler(SessionsAPIHandler):
  23. """A Session Root API handler."""
  24. @web.authenticated
  25. @authorized
  26. async def get(self):
  27. """Get a list of running sessions."""
  28. sm = self.session_manager
  29. sessions = await ensure_async(sm.list_sessions())
  30. self.finish(json.dumps(sessions, default=json_default))
  31. @web.authenticated
  32. @authorized
  33. async def post(self):
  34. """Create a new session."""
  35. # (unless a session already exists for the named session)
  36. sm = self.session_manager
  37. model = self.get_json_body()
  38. if model is None:
  39. raise web.HTTPError(400, "No JSON data provided")
  40. if "notebook" in model:
  41. self.log.warning("Sessions API changed, see updated swagger docs")
  42. model["type"] = "notebook"
  43. if "name" in model["notebook"]:
  44. model["path"] = model["notebook"]["name"]
  45. elif "path" in model["notebook"]:
  46. model["path"] = model["notebook"]["path"]
  47. try:
  48. # There is a high chance here that `path` is not a path but
  49. # a unique session id
  50. path = model["path"]
  51. except KeyError as e:
  52. raise web.HTTPError(400, "Missing field in JSON data: path") from e
  53. try:
  54. mtype = model["type"]
  55. except KeyError as e:
  56. raise web.HTTPError(400, "Missing field in JSON data: type") from e
  57. name = model.get("name", None)
  58. kernel = model.get("kernel", {})
  59. kernel_name = kernel.get("name", None)
  60. kernel_id = kernel.get("id", None)
  61. if not kernel_id and not kernel_name:
  62. self.log.debug("No kernel specified, using default kernel")
  63. kernel_name = None
  64. exists = await ensure_async(sm.session_exists(path=path))
  65. if exists:
  66. s_model = await sm.get_session(path=path)
  67. else:
  68. try:
  69. s_model = await sm.create_session(
  70. path=path,
  71. kernel_name=kernel_name,
  72. kernel_id=kernel_id,
  73. name=name,
  74. type=mtype,
  75. )
  76. except NoSuchKernel:
  77. msg = (
  78. "The '%s' kernel is not available. Please pick another "
  79. "suitable kernel instead, or install that kernel." % kernel_name
  80. )
  81. status_msg = "%s not found" % kernel_name
  82. self.log.warning("Kernel not found: %s" % kernel_name)
  83. self.set_status(501)
  84. self.finish(json.dumps({"message": msg, "short_message": status_msg}))
  85. return
  86. except Exception as e:
  87. raise web.HTTPError(500, str(e)) from e
  88. location = url_path_join(self.base_url, "api", "sessions", s_model["id"])
  89. self.set_header("Location", location)
  90. self.set_status(201)
  91. self.finish(json.dumps(s_model, default=json_default))
  92. class SessionHandler(SessionsAPIHandler):
  93. """A handler for a single session."""
  94. @web.authenticated
  95. @authorized
  96. async def get(self, session_id):
  97. """Get the JSON model for a single session."""
  98. sm = self.session_manager
  99. model = await sm.get_session(session_id=session_id)
  100. self.finish(json.dumps(model, default=json_default))
  101. @web.authenticated
  102. @authorized
  103. async def patch(self, session_id):
  104. """Patch updates sessions:
  105. - path updates session to track renamed paths
  106. - kernel.name starts a new kernel with a given kernelspec
  107. """
  108. sm = self.session_manager
  109. km = self.kernel_manager
  110. model = self.get_json_body()
  111. if model is None:
  112. raise web.HTTPError(400, "No JSON data provided")
  113. # get the previous session model
  114. before = await sm.get_session(session_id=session_id)
  115. changes = {}
  116. if "notebook" in model and "path" in model["notebook"]:
  117. self.log.warning("Sessions API changed, see updated swagger docs")
  118. model["path"] = model["notebook"]["path"]
  119. model["type"] = "notebook"
  120. if "path" in model:
  121. changes["path"] = model["path"]
  122. if "name" in model:
  123. changes["name"] = model["name"]
  124. if "type" in model:
  125. changes["type"] = model["type"]
  126. if "kernel" in model:
  127. # Kernel id takes precedence over name.
  128. if model["kernel"].get("id") is not None:
  129. kernel_id = model["kernel"]["id"]
  130. if kernel_id not in km:
  131. raise web.HTTPError(400, "No such kernel: %s" % kernel_id)
  132. changes["kernel_id"] = kernel_id
  133. elif model["kernel"].get("name") is not None:
  134. kernel_name = model["kernel"]["name"]
  135. try:
  136. kernel_id = await sm.start_kernel_for_session(
  137. session_id,
  138. kernel_name=kernel_name,
  139. name=before["name"],
  140. path=before["path"],
  141. type=before["type"],
  142. )
  143. changes["kernel_id"] = kernel_id
  144. except Exception as e:
  145. # the error message may contain sensitive information, so we want to
  146. # be careful with it, thus we only give the short repr of the exception
  147. # and the full traceback.
  148. # this should be fine as we are exposing here the same info as when we start a new kernel
  149. msg = "The '%s' kernel could not be started: %s" % (
  150. kernel_name,
  151. repr(str(e)),
  152. )
  153. status_msg = "Error starting kernel %s" % kernel_name
  154. self.log.error("Error starting kernel: %s", kernel_name)
  155. self.set_status(501)
  156. self.finish(json.dumps({"message": msg, "short_message": status_msg}))
  157. return
  158. await sm.update_session(session_id, **changes)
  159. s_model = await sm.get_session(session_id=session_id)
  160. if s_model["kernel"]["id"] != before["kernel"]["id"]:
  161. # kernel_id changed because we got a new kernel
  162. # shutdown the old one
  163. fut = asyncio.ensure_future(ensure_async(km.shutdown_kernel(before["kernel"]["id"])))
  164. # If we are not using pending kernels, wait for the kernel to shut down
  165. if not getattr(km, "use_pending_kernels", None):
  166. await fut
  167. self.finish(json.dumps(s_model, default=json_default))
  168. @web.authenticated
  169. @authorized
  170. async def delete(self, session_id):
  171. """Delete the session with given session_id."""
  172. sm = self.session_manager
  173. try:
  174. await sm.delete_session(session_id)
  175. except KeyError as e:
  176. # the kernel was deleted but the session wasn't!
  177. raise web.HTTPError(410, "Kernel deleted before session") from e
  178. self.set_status(204)
  179. self.finish()
  180. # -----------------------------------------------------------------------------
  181. # URL to handler mappings
  182. # -----------------------------------------------------------------------------
  183. _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
  184. default_handlers = [
  185. (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
  186. (r"/api/sessions", SessionRootHandler),
  187. ]