holoviz / panel
1
"""
2
Utilities for creating bokeh Server instances.
3
"""
4 7
import datetime as dt
5 7
import html
6 7
import inspect
7 7
import os
8 7
import pathlib
9 7
import signal
10 7
import sys
11 7
import traceback
12 7
import threading
13 7
import uuid
14

15 7
from collections import OrderedDict
16 7
from contextlib import contextmanager
17 7
from functools import partial, wraps
18 7
from types import FunctionType, MethodType
19 7
from urllib.parse import urljoin, urlparse
20

21 7
import param
22 7
import bokeh
23 7
import bokeh.command.util
24

25
# Bokeh imports
26 7
from bokeh.application import Application as BkApplication
27 7
from bokeh.application.handlers.code import CodeHandler
28 7
from bokeh.application.handlers.function import FunctionHandler
29 7
from bokeh.command.util import build_single_handler_application
30 7
from bokeh.core.templates import AUTOLOAD_JS
31 7
from bokeh.document.events import ModelChangedEvent
32 7
from bokeh.embed.bundle import Script
33 7
from bokeh.embed.elements import html_page_for_render_items, script_for_render_items
34 7
from bokeh.embed.util import RenderItem
35 7
from bokeh.io import curdoc
36 7
from bokeh.server.server import Server
37 7
from bokeh.server.urls import per_app_patterns
38 7
from bokeh.server.views.autoload_js_handler import AutoloadJsHandler as BkAutoloadJsHandler
39 7
from bokeh.server.views.doc_handler import DocHandler as BkDocHandler
40

41
# Tornado imports
42 7
from tornado.ioloop import IOLoop
43 7
from tornado.websocket import WebSocketHandler
44 7
from tornado.web import RequestHandler, StaticFileHandler, authenticated
45 7
from tornado.wsgi import WSGIContainer
46

47
# Internal imports
48 7
from ..util import edit_readonly
49 7
from .reload import autoreload_watcher
50 7
from .resources import BASE_TEMPLATE, Resources, bundle_resources
51 7
from .state import state
52

53
#---------------------------------------------------------------------
54
# Private API
55
#---------------------------------------------------------------------
56

57 7
INDEX_HTML = os.path.join(os.path.dirname(__file__), '..', '_templates', "index.html")
58

59 7
def _origin_url(url):
60 0
    if url.startswith("http"):
61 0
        url = url.split("//")[1]
62 0
    return url
63

64 7
def _server_url(url, port):
65 0
    if url.startswith("http"):
66 0
        return '%s:%d%s' % (url.rsplit(':', 1)[0], port, "/")
67
    else:
68 0
        return 'http://%s:%d%s' % (url.split(':')[0], port, "/")
69

70 7
def _eval_panel(panel, server_id, title, location, doc):
71 7
    from ..template import BaseTemplate
72 7
    from ..pane import panel as as_panel
73

74 7
    with set_curdoc(doc):
75 7
        if isinstance(panel, (FunctionType, MethodType)):
76 0
            panel = panel()
77 7
        if isinstance(panel, BaseTemplate):
78 7
            doc = panel._modify_doc(server_id, title, doc, location)
79
        else:
80 7
            doc = as_panel(panel)._modify_doc(server_id, title, doc, location)
81 7
        return doc
82

83 7
def async_execute(func):
84
    """
85
    Wrap async event loop scheduling to ensure that with_lock flag
86
    is propagated from function to partial wrapping it.
87
    """
88 7
    if not state.curdoc or not state.curdoc.session_context:
89 0
        ioloop = IOLoop.current()
90 0
        event_loop = ioloop.asyncio_loop
91 0
        if event_loop.is_running():
92 0
            ioloop.add_callback(func)
93
        else:
94 0
            event_loop.run_until_complete(func())
95 0
        return
96

97 7
    if isinstance(func, partial) and hasattr(func.func, 'lock'):
98 0
        unlock = not func.func.lock
99
    else:
100 7
        unlock = not getattr(func, 'lock', False)
101 7
    if unlock:
102 7
        @wraps(func)
103 2
        async def wrapper(*args, **kw):
104 7
            return await func(*args, **kw)
105 7
        wrapper.nolock = True
106
    else:
107 0
        wrapper = func
108 7
    state.curdoc.add_next_tick_callback(wrapper)
109

110 7
param.parameterized.async_executor = async_execute
111

112 7
def _initialize_session_info(session_context):
113 7
    from ..config import config
114 7
    session_id = session_context.id
115 7
    sessions = state.session_info['sessions']
116 7
    if config.session_history == 0 or session_id in sessions:
117 7
        return
118

119 7
    state.session_info['total'] += 1
120 7
    if config.session_history > 0 and len(sessions) >= config.session_history:
121 0
        old_history = list(sessions.items())
122 0
        sessions = OrderedDict(old_history[-(config.session_history-1):])
123 0
        state.session_info['sessions'] = sessions
124 7
    sessions[session_id] = {
125
        'launched': dt.datetime.now().timestamp(),
126
        'started': None,
127
        'rendered': None,
128
        'ended': None,
129
        'user_agent': session_context.request.headers.get('User-Agent')
130
    }
131

132 7
state.on_session_created(_initialize_session_info)
133

134
#---------------------------------------------------------------------
135
# Bokeh patches
136
#---------------------------------------------------------------------
137

138 7
def server_html_page_for_session(session, resources, title, template=BASE_TEMPLATE,
139
                                 template_variables=None):
140 7
    render_item = RenderItem(
141
        token = session.token,
142
        roots = session.document.roots,
143
        use_for_title = False,
144
    )
145

146 7
    if template_variables is None:
147 0
        template_variables = {}
148

149 7
    bundle = bundle_resources(resources)
150 7
    return html_page_for_render_items(bundle, {}, [render_item], title,
151
        template=template, template_variables=template_variables)
152

153 7
def autoload_js_script(resources, token, element_id, app_path, absolute_url):
154 0
    resources = Resources.from_bokeh(resources)
155 0
    bundle = bundle_resources(resources)
156

157 0
    render_items = [RenderItem(token=token, elementid=element_id, use_for_title=False)]
158 0
    bundle.add(Script(script_for_render_items({}, render_items, app_path=app_path, absolute_url=absolute_url)))
159

160 0
    return AUTOLOAD_JS.render(bundle=bundle, elementid=element_id)
161

162
# Patch Application to handle session callbacks
163 7
class Application(BkApplication):
164

165 7
    async def on_session_created(self, session_context):
166 7
        for cb in state._on_session_created:
167 7
            cb(session_context)
168 7
        await super().on_session_created(session_context)
169

170 7
bokeh.command.util.Application = Application
171

172

173 7
class SessionPrefixHandler:
174

175 7
    @contextmanager
176 2
    def _session_prefix(self):
177 7
        prefix = self.request.uri.replace(self.application_context._url, '')
178 7
        if not prefix.endswith('/'):
179 7
            prefix += '/'
180 7
        base_url = urljoin('/', prefix)
181 7
        rel_path = '/'.join(['..'] * self.application_context._url.strip('/').count('/'))
182 7
        old_url, old_rel = state.base_url, state.rel_path
183 7
        with edit_readonly(state):
184 7
            state.base_url = base_url
185 7
            state.rel_path = rel_path
186 7
        try:
187 7
            yield
188
        finally:
189 7
            with edit_readonly(state):
190 7
                state.base_url = old_url
191 7
                state.rel_path = old_rel
192

193
# Patch Bokeh DocHandler URL
194 7
class DocHandler(BkDocHandler, SessionPrefixHandler):
195

196 7
    @authenticated
197 2
    async def get(self, *args, **kwargs):
198 7
        with self._session_prefix():
199 7
            session = await self.get_session()
200 7
            resources = Resources.from_bokeh(self.application.resources())
201 7
            page = server_html_page_for_session(
202
                session, resources=resources, title=session.document.title,
203
                template=session.document.template,
204
                template_variables=session.document.template_variables
205
            )
206 7
        self.set_header("Content-Type", 'text/html')
207 7
        self.write(page)
208

209 7
per_app_patterns[0] = (r'/?', DocHandler)
210

211
# Patch Bokeh Autoload handler
212 7
class AutoloadJsHandler(BkAutoloadJsHandler, SessionPrefixHandler):
213
    ''' Implements a custom Tornado handler for the autoload JS chunk
214

215
    '''
216

217 7
    async def get(self, *args, **kwargs):
218 0
        element_id = self.get_argument("bokeh-autoload-element", default=None)
219 0
        if not element_id:
220 0
            self.send_error(status_code=400, reason='No bokeh-autoload-element query parameter')
221 0
            return
222

223 0
        app_path = self.get_argument("bokeh-app-path", default="/")
224 0
        absolute_url = self.get_argument("bokeh-absolute-url", default=None)
225

226 0
        if absolute_url:
227 0
            server_url = '{uri.scheme}://{uri.netloc}'.format(uri=urlparse(absolute_url))
228
        else:
229 0
            server_url = None
230

231 0
        with self._session_prefix():
232 0
            session = await self.get_session()
233 0
            resources = Resources.from_bokeh(self.application.resources(server_url))
234 0
            js = autoload_js_script(resources, session.token, element_id, app_path, absolute_url)
235

236 0
        self.set_header("Content-Type", 'application/javascript')
237 0
        self.write(js)
238

239 7
per_app_patterns[3] = (r'/autoload.js', AutoloadJsHandler)
240

241 7
def modify_document(self, doc):
242 0
    from bokeh.io.doc import set_curdoc as bk_set_curdoc
243 0
    from ..config import config
244

245 0
    if config.autoreload:
246 0
        path = self._runner.path
247 0
        argv = self._runner._argv
248 0
        handler = type(self)(filename=path, argv=argv)
249 0
        self._runner = handler._runner
250

251 0
    module = self._runner.new_module()
252

253
    # If no module was returned it means the code runner has some permanent
254
    # unfixable problem, e.g. the configured source code has a syntax error
255 0
    if module is None:
256 0
        return
257

258
    # One reason modules are stored is to prevent the module
259
    # from being gc'd before the document is. A symptom of a
260
    # gc'd module is that its globals become None. Additionally
261
    # stored modules are used to provide correct paths to
262
    # custom models resolver.
263 0
    sys.modules[module.__name__] = module
264 0
    doc._modules.append(module)
265

266 0
    old_doc = curdoc()
267 0
    bk_set_curdoc(doc)
268 0
    old_io = self._monkeypatch_io()
269

270 0
    if config.autoreload:
271 0
        set_curdoc(doc)
272 0
        state.onload(autoreload_watcher)
273

274 0
    try:
275 0
        def post_check():
276 0
            newdoc = curdoc()
277
            # script is supposed to edit the doc not replace it
278 0
            if newdoc is not doc:
279 0
                raise RuntimeError("%s at '%s' replaced the output document" % (self._origin, self._runner.path))
280

281 0
        def handle_exception(handler, e):
282 0
            from bokeh.application.handlers.handler import handle_exception
283 0
            from ..pane import HTML
284

285
            # Clean up
286 0
            del sys.modules[module.__name__]
287 0
            doc._modules.remove(module)
288 0
            bokeh.application.handlers.code_runner.handle_exception = handle_exception
289 0
            tb = html.escape(traceback.format_exc())
290

291
            # Serve error
292 0
            HTML(
293
                f'<b>{type(e).__name__}</b>: {e}</br><pre style="overflow-y: scroll">{tb}</pre>',
294
                css_classes=['alert', 'alert-danger'], sizing_mode='stretch_width'
295
            ).servable()
296

297 0
        if config.autoreload:
298 0
            bokeh.application.handlers.code_runner.handle_exception = handle_exception
299 0
        self._runner.run(module, post_check)
300
    finally:
301 0
        self._unmonkeypatch_io(old_io)
302 0
        bk_set_curdoc(old_doc)
303

304 7
CodeHandler.modify_document = modify_document
305

306
#---------------------------------------------------------------------
307
# Public API
308
#---------------------------------------------------------------------
309

310 7
def init_doc(doc):
311 7
    doc = doc or curdoc()
312 7
    if not doc.session_context:
313 7
        return doc
314

315 7
    session_id = doc.session_context.id
316 7
    sessions = state.session_info['sessions']
317 7
    if session_id not in sessions:
318 7
        return doc
319

320 7
    sessions[session_id].update({
321
        'started': dt.datetime.now().timestamp()
322
    })
323 7
    doc.on_event('document_ready', state._init_session)
324 7
    return doc
325

326 7
@contextmanager
327 2
def set_curdoc(doc):
328 7
    state.curdoc = doc
329 7
    yield
330 7
    state.curdoc = None
331

332 7
def with_lock(func):
333
    """
334
    Wrap a callback function to execute with a lock allowing the
335
    function to modify bokeh models directly.
336

337
    Arguments
338
    ---------
339
    func: callable
340
      The callable to wrap
341

342
    Returns
343
    -------
344
    wrapper: callable
345
      Function wrapped to execute without a Document lock.
346
    """
347 0
    if inspect.iscoroutinefunction(func):
348 0
        @wraps(func)
349 0
        async def wrapper(*args, **kw):
350 0
            return await func(*args, **kw)
351
    else:
352 0
        @wraps(func)
353 0
        def wrapper(*args, **kw):
354 0
            return func(*args, **kw)
355 0
    wrapper.lock = True
356 0
    return wrapper
357

358

359 7
@contextmanager
360 2
def unlocked():
361
    """
362
    Context manager which unlocks a Document and dispatches
363
    ModelChangedEvents triggered in the context body to all sockets
364
    on current sessions.
365
    """
366 7
    curdoc = state.curdoc
367 7
    if curdoc is None or curdoc.session_context is None or curdoc.session_context.session is None:
368 7
        yield
369 7
        return
370 0
    connections = curdoc.session_context.session._subscribed_connections
371

372 0
    hold = curdoc._hold
373 0
    if hold:
374 0
        old_events = list(curdoc._held_events)
375
    else:
376 0
        old_events = []
377 0
        curdoc.hold()
378 0
    try:
379 0
        yield
380 0
        events = []
381 0
        for conn in connections:
382 0
            socket = conn._socket
383 0
            if hasattr(socket, 'write_lock') and socket.write_lock._block._value == 0:
384 0
                state._locks.add(socket)
385 0
            locked = socket in state._locks
386 0
            for event in curdoc._held_events:
387 0
                if (isinstance(event, ModelChangedEvent) and event not in old_events
388
                    and hasattr(socket, 'write_message') and not locked):
389 0
                    msg = conn.protocol.create('PATCH-DOC', [event])
390 0
                    WebSocketHandler.write_message(socket, msg.header_json)
391 0
                    WebSocketHandler.write_message(socket, msg.metadata_json)
392 0
                    WebSocketHandler.write_message(socket, msg.content_json)
393 0
                    for header, payload in msg._buffers:
394 0
                        WebSocketHandler.write_message(socket, header)
395 0
                        WebSocketHandler.write_message(socket, payload, binary=True)
396 0
                elif event not in events:
397 0
                    events.append(event)
398 0
        curdoc._held_events = events
399
    finally:
400 0
        if not hold:
401 0
            curdoc.unhold()
402

403

404 7
def serve(panels, port=0, address=None, websocket_origin=None, loop=None,
405
          show=True, start=True, title=None, verbose=True, location=True,
406
          threaded=False, **kwargs):
407
    """
408
    Allows serving one or more panel objects on a single server.
409
    The panels argument should be either a Panel object or a function
410
    returning a Panel object or a dictionary of these two. If a
411
    dictionary is supplied the keys represent the slugs at which
412
    each app is served, e.g. `serve({'app': panel1, 'app2': panel2})`
413
    will serve apps at /app and /app2 on the server.
414

415
    Arguments
416
    ---------
417
    panel: Viewable, function or {str: Viewable or function}
418
      A Panel object, a function returning a Panel object or a
419
      dictionary mapping from the URL slug to either.
420
    port: int (optional, default=0)
421
      Allows specifying a specific port
422
    address : str
423
      The address the server should listen on for HTTP requests.
424
    websocket_origin: str or list(str) (optional)
425
      A list of hosts that can connect to the websocket.
426

427
      This is typically required when embedding a server app in
428
      an external web site.
429

430
      If None, "localhost" is used.
431
    loop : tornado.ioloop.IOLoop (optional, default=IOLoop.current())
432
      The tornado IOLoop to run the Server on
433
    show : boolean (optional, default=False)
434
      Whether to open the server in a new browser tab on start
435
    start : boolean(optional, default=False)
436
      Whether to start the Server
437
    title: str or {str: str} (optional, default=None)
438
      An HTML title for the application or a dictionary mapping
439
      from the URL slug to a customized title
440
    verbose: boolean (optional, default=True)
441
      Whether to print the address and port
442
    location : boolean or panel.io.location.Location
443
      Whether to create a Location component to observe and
444
      set the URL location.
445
    threaded: boolean (default=False)
446
      Whether to start the server on a new Thread
447
    kwargs: dict
448
      Additional keyword arguments to pass to Server instance
449
    """
450 7
    kwargs = dict(kwargs, **dict(
451
        port=port, address=address, websocket_origin=websocket_origin,
452
        loop=loop, show=show, start=start, title=title, verbose=verbose,
453
        location=location
454
    ))
455 7
    if threaded:
456 7
        from tornado.ioloop import IOLoop
457 7
        kwargs['loop'] = loop = IOLoop() if loop is None else loop
458 7
        server = StoppableThread(
459
            target=get_server, io_loop=loop, args=(panels,), kwargs=kwargs
460
        )
461 7
        server.start()
462
    else:
463 7
        server = get_server(panels, **kwargs)
464 7
    return server
465

466

467 7
class ProxyFallbackHandler(RequestHandler):
468
    """A `RequestHandler` that wraps another HTTP server callback and
469
    proxies the subpath.
470
    """
471

472 7
    def initialize(self, fallback, proxy=None):
473 0
        self.fallback = fallback
474 0
        self.proxy = proxy
475

476 7
    def prepare(self):
477 0
        if self.proxy:
478 0
            self.request.path = self.request.path.replace(self.proxy, '')
479 0
        self.fallback(self.request)
480 0
        self._finished = True
481 0
        self.on_finish()
482

483

484 7
def get_static_routes(static_dirs):
485
    """
486
    Returns a list of tornado routes of StaticFileHandlers given a
487
    dictionary of slugs and file paths to serve.
488
    """
489 7
    patterns = []
490 7
    for slug, path in static_dirs.items():
491 7
        if not slug.startswith('/'):
492 7
            slug = '/' + slug
493 7
        if slug == '/static':
494 0
            raise ValueError("Static file route may not use /static "
495
                             "this is reserved for internal use.")
496 7
        path = os.path.abspath(path)
497 7
        if not os.path.isdir(path):
498 0
            raise ValueError("Cannot serve non-existent path %s" % path)
499 7
        patterns.append(
500
            (r"%s/(.*)" % slug, StaticFileHandler, {"path": path})
501
        )
502 7
    return patterns
503

504

505 7
def get_server(panel, port=0, address=None, websocket_origin=None,
506
               loop=None, show=False, start=False, title=None,
507
               verbose=False, location=True, static_dirs={},
508
               oauth_provider=None, oauth_key=None, oauth_secret=None,
509
               oauth_extra_params={}, cookie_secret=None,
510
               oauth_encryption_key=None, session_history=None, **kwargs):
511
    """
512
    Returns a Server instance with this panel attached as the root
513
    app.
514

515
    Arguments
516
    ---------
517
    panel: Viewable, function or {str: Viewable}
518
      A Panel object, a function returning a Panel object or a
519
      dictionary mapping from the URL slug to either.
520
    port: int (optional, default=0)
521
      Allows specifying a specific port
522
    address : str
523
      The address the server should listen on for HTTP requests.
524
    websocket_origin: str or list(str) (optional)
525
      A list of hosts that can connect to the websocket.
526

527
      This is typically required when embedding a server app in
528
      an external web site.
529

530
      If None, "localhost" is used.
531
    loop : tornado.ioloop.IOLoop (optional, default=IOLoop.current())
532
      The tornado IOLoop to run the Server on.
533
    show : boolean (optional, default=False)
534
      Whether to open the server in a new browser tab on start.
535
    start : boolean(optional, default=False)
536
      Whether to start the Server.
537
    title : str or {str: str} (optional, default=None)
538
      An HTML title for the application or a dictionary mapping
539
      from the URL slug to a customized title.
540
    verbose: boolean (optional, default=False)
541
      Whether to report the address and port.
542
    location : boolean or panel.io.location.Location
543
      Whether to create a Location component to observe and
544
      set the URL location.
545
    static_dirs: dict (optional, default={})
546
      A dictionary of routes and local paths to serve as static file
547
      directories on those routes.
548
    oauth_provider: str
549
      One of the available OAuth providers
550
    oauth_key: str (optional, default=None)
551
      The public OAuth identifier
552
    oauth_secret: str (optional, default=None)
553
      The client secret for the OAuth provider
554
    oauth_extra_params: dict (optional, default={})
555
      Additional information for the OAuth provider
556
    cookie_secret: str (optional, default=None)
557
      A random secret string to sign cookies (required for OAuth)
558
    oauth_encryption_key: str (optional, default=False)
559
      A random encryption key used for encrypting OAuth user
560
      information and access tokens.
561
    session_history: int (optional, default=None)
562
      The amount of session history to accumulate. If set to non-zero
563
      and non-None value will launch a REST endpoint at
564
      /rest/session_info, which returns information about the session
565
      history.
566
    kwargs: dict
567
      Additional keyword arguments to pass to Server instance.
568

569
    Returns
570
    -------
571
    server : bokeh.server.server.Server
572
      Bokeh Server instance running this panel
573
    """
574 7
    from ..config import config
575 7
    from .rest import REST_PROVIDERS
576

577 7
    server_id = kwargs.pop('server_id', uuid.uuid4().hex)
578 7
    kwargs['extra_patterns'] = extra_patterns = kwargs.get('extra_patterns', [])
579 7
    if isinstance(panel, dict):
580 7
        apps = {}
581 7
        for slug, app in panel.items():
582 7
            if isinstance(title, dict):
583 7
                try:
584 7
                    title_ = title[slug]
585 7
                except KeyError:
586 7
                    raise KeyError(
587
                        "Keys of the title dictionnary and of the apps "
588
                        f"dictionary must match. No {slug} key found in the "
589
                        "title dictionary.")
590
            else:
591 7
                title_ = title
592 7
            slug = slug if slug.startswith('/') else '/'+slug
593 7
            if 'flask' in sys.modules:
594 0
                from flask import Flask
595 0
                if isinstance(app, Flask):
596 0
                    wsgi = WSGIContainer(app)
597 0
                    if slug == '/':
598 0
                        raise ValueError('Flask apps must be served on a subpath.')
599 0
                    if not slug.endswith('/'):
600 0
                        slug += '/'
601 0
                    extra_patterns.append(('^'+slug+'.*', ProxyFallbackHandler,
602
                                           dict(fallback=wsgi, proxy=slug)))
603 0
                    continue
604 7
            if isinstance(app, pathlib.Path):
605 7
                app = str(app) # enables serving apps from Paths
606 7
            if (isinstance(app, str) and (app.endswith(".py") or app.endswith(".ipynb"))
607
                and os.path.isfile(app)):
608 7
                apps[slug] = build_single_handler_application(app)
609
            else:
610 7
                handler = FunctionHandler(partial(_eval_panel, app, server_id, title_, location))
611 7
                apps[slug] = Application(handler)
612
    else:
613 7
        handler = FunctionHandler(partial(_eval_panel, panel, server_id, title, location))
614 7
        apps = {'/': Application(handler)}
615

616 7
    extra_patterns += get_static_routes(static_dirs)
617

618 7
    if session_history is not None:
619 0
        config.session_history = session_history
620 7
    if config.session_history != 0:
621 7
        pattern = REST_PROVIDERS['param']([], 'rest')
622 7
        extra_patterns.extend(pattern)
623 7
        state.publish('session_info', state, ['session_info'])
624

625 7
    opts = dict(kwargs)
626 7
    if loop:
627 7
        loop.make_current()
628 7
        opts['io_loop'] = loop
629 7
    elif opts.get('num_procs', 1) == 1:
630 7
        opts['io_loop'] = IOLoop.current()
631

632 7
    if 'index' not in opts:
633 7
        opts['index'] = INDEX_HTML
634

635 7
    if address is not None:
636 0
        opts['address'] = address
637

638 7
    if websocket_origin:
639 0
        if not isinstance(websocket_origin, list):
640 0
            websocket_origin = [websocket_origin]
641 0
        opts['allow_websocket_origin'] = websocket_origin
642

643
    # Configure OAuth
644 7
    from ..config import config
645 7
    if config.oauth_provider:
646 0
        from ..auth import OAuthProvider
647 0
        opts['auth_provider'] = OAuthProvider()
648 7
    if oauth_provider:
649 0
        config.oauth_provider = oauth_provider
650 7
    if oauth_key:
651 0
        config.oauth_key = oauth_key
652 7
    if oauth_extra_params:
653 0
        config.oauth_extra_params = oauth_extra_params
654 7
    if cookie_secret:
655 0
        config.cookie_secret = cookie_secret
656 7
    opts['cookie_secret'] = config.cookie_secret
657

658 7
    server = Server(apps, port=port, **opts)
659 7
    if verbose:
660 7
        address = server.address or 'localhost'
661 7
        url = f"http://{address}:{server.port}{server.prefix}"
662 7
        print(f"Launching server at {url}")
663

664 7
    state._servers[server_id] = (server, panel, [])
665

666 7
    if show:
667 0
        def show_callback():
668 0
            server.show('/login' if config.oauth_provider else '/')
669 0
        server.io_loop.add_callback(show_callback)
670

671 7
    def sig_exit(*args, **kwargs):
672 0
        server.io_loop.add_callback_from_signal(do_stop)
673

674 7
    def do_stop(*args, **kwargs):
675 0
        server.io_loop.stop()
676

677 7
    try:
678 7
        signal.signal(signal.SIGINT, sig_exit)
679 7
    except ValueError:
680 7
        pass # Can't use signal on a thread
681

682 7
    if start:
683 7
        server.start()
684 7
        try:
685 7
            server.io_loop.start()
686 0
        except RuntimeError:
687 0
            pass
688 7
    return server
689

690

691 7
class StoppableThread(threading.Thread):
692
    """Thread class with a stop() method."""
693

694 7
    def __init__(self, io_loop=None, **kwargs):
695 7
        super().__init__(**kwargs)
696 7
        self.io_loop = io_loop
697

698 7
    def run(self):
699 7
        if hasattr(self, '_target'):
700 7
            target, args, kwargs = self._target, self._args, self._kwargs
701
        else:
702 0
            target, args, kwargs = self._Thread__target, self._Thread__args, self._Thread__kwargs
703 7
        if not target:
704 0
            return
705 7
        bokeh_server = None
706 7
        try:
707 7
            bokeh_server = target(*args, **kwargs)
708
        finally:
709 7
            if isinstance(bokeh_server, Server):
710 7
                bokeh_server.stop()
711 7
            if hasattr(self, '_target'):
712 7
                del self._target, self._args, self._kwargs
713
            else:
714 0
                del self._Thread__target, self._Thread__args, self._Thread__kwargs
715

716 7
    def stop(self):
717 7
        self.io_loop.add_callback(self.io_loop.stop)

Read our documentation on viewing source code .

Loading