build_handler.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Tornado handlers for frontend config storage."""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import json
  5. from concurrent.futures import ThreadPoolExecutor
  6. from threading import Event
  7. from jupyter_server.base.handlers import APIHandler
  8. from jupyter_server.extension.handler import ExtensionHandlerMixin
  9. from tornado import gen, web
  10. from tornado.concurrent import run_on_executor
  11. from jupyterlab.commands import AppOptions, _ensure_options, build, build_check, clean
  12. class Builder:
  13. building = False
  14. executor = ThreadPoolExecutor(max_workers=5)
  15. canceled = False
  16. _canceling = False
  17. _kill_event = None
  18. _future = None
  19. def __init__(self, core_mode, app_options=None):
  20. app_options = _ensure_options(app_options)
  21. self.log = app_options.logger
  22. self.core_mode = core_mode
  23. self.app_dir = app_options.app_dir
  24. self.core_config = app_options.core_config
  25. self.labextensions_path = app_options.labextensions_path
  26. @gen.coroutine
  27. def get_status(self):
  28. if self.core_mode:
  29. raise gen.Return({"status": "stable", "message": ""})
  30. if self.building:
  31. raise gen.Return({"status": "building", "message": ""})
  32. try:
  33. messages = yield self._run_build_check(
  34. self.app_dir, self.log, self.core_config, self.labextensions_path
  35. )
  36. status = "needed" if messages else "stable"
  37. if messages:
  38. self.log.warning("Build recommended")
  39. [self.log.warning(m) for m in messages]
  40. else:
  41. self.log.info("Build is up to date")
  42. except ValueError:
  43. self.log.warning("Could not determine jupyterlab build status without nodejs")
  44. status = "stable"
  45. messages = []
  46. raise gen.Return({"status": status, "message": "\n".join(messages)})
  47. @gen.coroutine
  48. def build(self):
  49. if self._canceling:
  50. msg = "Cancel in progress"
  51. raise ValueError(msg)
  52. if not self.building:
  53. self.canceled = False
  54. self._future = future = gen.Future()
  55. self.building = True
  56. self._kill_event = evt = Event()
  57. try:
  58. yield self._run_build(
  59. self.app_dir, self.log, evt, self.core_config, self.labextensions_path
  60. )
  61. future.set_result(True)
  62. except Exception as e:
  63. if str(e) == "Aborted":
  64. future.set_result(False)
  65. else:
  66. future.set_exception(e)
  67. finally:
  68. self.building = False
  69. try:
  70. yield self._future
  71. except Exception as e:
  72. raise e
  73. @gen.coroutine
  74. def cancel(self):
  75. if not self.building:
  76. msg = "No current build"
  77. raise ValueError(msg)
  78. self._canceling = True
  79. yield self._future
  80. self._canceling = False
  81. self.canceled = True
  82. @run_on_executor
  83. def _run_build_check(self, app_dir, logger, core_config, labextensions_path):
  84. return build_check(
  85. app_options=AppOptions(
  86. app_dir=app_dir,
  87. logger=logger,
  88. core_config=core_config,
  89. labextensions_path=labextensions_path,
  90. )
  91. )
  92. @run_on_executor
  93. def _run_build(self, app_dir, logger, kill_event, core_config, labextensions_path):
  94. app_options = AppOptions(
  95. app_dir=app_dir,
  96. logger=logger,
  97. kill_event=kill_event,
  98. core_config=core_config,
  99. labextensions_path=labextensions_path,
  100. )
  101. try:
  102. return build(app_options=app_options)
  103. except Exception:
  104. if self._kill_event.is_set():
  105. return
  106. self.log.warning("Build failed, running a clean and rebuild")
  107. clean(app_options=app_options)
  108. return build(app_options=app_options)
  109. class BuildHandler(ExtensionHandlerMixin, APIHandler):
  110. def initialize(self, builder=None, name=None):
  111. super().initialize(name=name)
  112. self.builder = builder
  113. @web.authenticated
  114. @gen.coroutine
  115. def get(self):
  116. data = yield self.builder.get_status()
  117. self.finish(json.dumps(data))
  118. @web.authenticated
  119. @gen.coroutine
  120. def delete(self):
  121. self.log.warning("Canceling build")
  122. try:
  123. yield self.builder.cancel()
  124. except Exception as e:
  125. raise web.HTTPError(500, str(e)) from None
  126. self.set_status(204)
  127. @web.authenticated
  128. @gen.coroutine
  129. def post(self):
  130. self.log.debug("Starting build")
  131. try:
  132. yield self.builder.build()
  133. except Exception as e:
  134. raise web.HTTPError(500, str(e)) from None
  135. if self.builder.canceled:
  136. raise web.HTTPError(400, "Build canceled")
  137. self.log.debug("Build succeeded")
  138. self.set_status(200)
  139. # The path for lab build.
  140. build_path = r"/lab/api/build"