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