Source code for bokeh.server.server

#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2022, Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
''' Provide basic Bokeh server objects that use a Tornado ``HTTPServer`` and
``BokeTornado`` Tornado Application to service Bokeh Server Applications.
There are two public classes in this module:

:class:`~bokeh.server.server.BaseServer`
    This is a lightweight class to explicitly coordinate the components needed
    to run a Bokeh server (A :class:`~bokeh.server.tornado.BokehTornado`
    instance, and Tornado ``HTTPServer`` and a Tornado ``IOLoop``)

:class:`~bokeh.server.server.Server`
    This higher-level convenience class only needs to be configured with Bokeh
    :class:`~bokeh.application.application.Application` instances, and will
    automatically create and coordinate the lower level Tornado components.

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
import atexit
import signal
import socket
import sys
from types import FrameType
from typing import (
    TYPE_CHECKING,
    Any,
    Dict,
    List,
    Mapping,
)

# External imports
from tornado import version as tornado_version
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop

# Bokeh imports
from .. import __version__
from ..core import properties as p
from ..core.properties import (
    Bool,
    Int,
    Nullable,
    String,
)
from ..resources import DEFAULT_SERVER_PORT
from ..util.options import Options
from .tornado import DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES, BokehTornado
from .util import bind_sockets, create_hosts_allowlist

if TYPE_CHECKING:
    from ..application.application import Application
    from ..application.handlers.function import ModifyDoc
    from ..core.types import ID
    from ..util.browser import BrowserTarget
    from .session import ServerSession

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'BaseServer',
    'Server',
)

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

[docs]class BaseServer: ''' Explicitly coordinate the level Tornado components required to run a Bokeh server: * A Tornado ``IOLoop`` to run the Bokeh server machinery. * a ``BokehTornado`` Tornado application that defines the Bokeh server machinery. * a Tornado ``HTTPServer`` to direct HTTP requests All three of these components must be passed to ``BaseServer``, which will initialize the ``BokehTornado`` instance on the ``io_loop``. The ``http_server`` must have been previously created and initialized with the ``BokehTornado`` instance. .. autoclasstoc:: '''
[docs] def __init__(self, io_loop: IOLoop, tornado_app: BokehTornado, http_server: HTTPServer) -> None: ''' Create a ``BaseServer`` instance. Args: io_loop (IOLoop) : A Tornado ``IOLoop`` to run the Bokeh Tornado application on. tornado_app (BokehTornado) : An instance of the Bokeh Tornado application that generates Bokeh Documents and Sessions. http_server (HTTPServer) : A Tornado ``HTTPServer`` to service HTTP requests for Bokeh applications. Should have already be configured with the ``tornado_app`` when created. ''' self._started = False self._stopped = False self._http = http_server self._loop = io_loop self._tornado = tornado_app self._tornado.initialize(io_loop)
@property def io_loop(self) -> IOLoop: ''' The Tornado ``IOLoop`` that this Bokeh Server is running on. ''' return self._loop
[docs] def start(self) -> None: ''' Install the Bokeh Server and its background tasks on a Tornado ``IOLoop``. This method does *not* block and does *not* affect the state of the Tornado ``IOLoop`` You must start and stop the loop yourself, i.e. this method is typically useful when you are already explicitly managing an ``IOLoop`` yourself. To start a Bokeh server and immediately "run forever" in a blocking manner, see :func:`~bokeh.server.server.BaseServer.run_until_shutdown`. ''' assert not self._started, "Already started" self._started = True self._tornado.start()
[docs] def stop(self, wait: bool = True) -> None: ''' Stop the Bokeh Server. This stops and removes all Bokeh Server ``IOLoop`` callbacks, as well as stops the ``HTTPServer`` that this instance was configured with. Args: fast (bool): Whether to wait for orderly cleanup (default: True) Returns: None ''' assert not self._stopped, "Already stopped" self._stopped = True self._tornado.stop(wait) self._http.stop()
[docs] def unlisten(self) -> None: ''' Stop listening on ports. The server will no longer be usable after calling this function. .. note:: This function is mostly useful for tests Returns: None ''' self._http.stop() self.io_loop.add_callback(self._http.close_all_connections)
[docs] def run_until_shutdown(self) -> None: ''' Run the Bokeh Server until shutdown is requested by the user, either via a Keyboard interrupt (Ctrl-C) or SIGTERM. Calling this method will start the Tornado ``IOLoop`` and block all execution in the calling process. Returns: None ''' if not self._started: self.start() # Install shutdown hooks atexit.register(self._atexit) signal.signal(signal.SIGTERM, self._sigterm) # type: ignore[arg-type] try: self._loop.start() except KeyboardInterrupt: print("\nInterrupted, shutting down") self.stop()
[docs] def get_session(self, app_path: str, session_id: ID) -> ServerSession: ''' Get an active a session by name application path and session ID. Args: app_path (str) : The configured application path for the application to return a session for. session_id (str) : The session ID of the session to retrieve. Returns: ServerSession ''' return self._tornado.get_session(app_path, session_id)
[docs] def get_sessions(self, app_path: str | None = None) -> List[ServerSession]: ''' Gets all currently active sessions for applications. Args: app_path (str, optional) : The configured application path for the application to return sessions for. If None, return active sessions for all applications. (default: None) Returns: list[ServerSession] ''' if app_path is not None: return self._tornado.get_sessions(app_path) all_sessions: List[ServerSession] = [] for path in self._tornado.app_paths: all_sessions += self._tornado.get_sessions(path) return all_sessions
[docs] def show(self, app_path: str, browser: str | None = None, new: BrowserTarget = "tab") -> None: ''' Opens an app in a browser window or tab. This method is useful for testing or running Bokeh server applications on a local machine but should not call when running Bokeh server for an actual deployment. Args: app_path (str) : the app path to open The part of the URL after the hostname:port, with leading slash. browser (str, optional) : browser to show with (default: None) For systems that support it, the **browser** argument allows specifying which browser to display in, e.g. "safari", "firefox", "opera", "windows-default" (see the :doc:`webbrowser <python:library/webbrowser>` module documentation in the standard lib for more details). new (str, optional) : window or tab (default: "tab") If ``new`` is 'tab', then opens a new tab. If ``new`` is 'window', then opens a new window. Returns: None ''' if not app_path.startswith("/"): raise ValueError("app_path must start with a /") address_string = 'localhost' if self.address is not None and self.address != '': address_string = self.address url = f"http://{address_string}:{self.port}{self.prefix}{app_path}" from bokeh.util.browser import view view(url, browser=browser, new=new)
_atexit_ran = False def _atexit(self) -> None: if self._atexit_ran: return self._atexit_ran = True log.debug("Shutdown: cleaning up") if not self._stopped: self.stop(wait=False) def _sigterm(self, signum: signal.Signals, frame: FrameType) -> None: print(f"Received signal {signum}, shutting down") # Tell self._loop.start() to return. self._loop.add_callback_from_signal(self._loop.stop) @property def port(self) -> int: ''' The configured port number that the server listens on for HTTP requests ''' sock = next( sock for sock in self._http._sockets.values() if sock.family in (socket.AF_INET, socket.AF_INET6) ) return sock.getsockname()[1] @property def address(self) -> str | None: ''' The configured address that the server listens on for HTTP requests ''' sock = next( sock for sock in self._http._sockets.values() if sock.family in (socket.AF_INET, socket.AF_INET6) ) return sock.getsockname()[0] @property def prefix(self) -> str: ''' The configured URL prefix to use for all Bokeh server paths. ''' return self._tornado.prefix @property def index(self) -> str | None: ''' A path to a Jinja2 template to use for index at "/" ''' return self._tornado.index
#----------------------------------------------------------------------------- # General API #-----------------------------------------------------------------------------
[docs]class Server(BaseServer): ''' A high level convenience class to run a Bokeh server. This class can automatically coordinate the three the base level components required to run a Bokeh server: * A Tornado ``IOLoop`` to run the Bokeh server machinery. * a ``BokehTornado`` Tornado application that defines the Bokeh server machinery. * a Tornado ``HTTPServer`` to direct HTTP requests This high level ``Server`` class has some limitations. In particular, it is not possible to set an explicit ``io_loop`` and ``num_procs`` other than 1 at the same time. To do that, it is necessary to use ``BaseServer`` and coordinate the three components above explicitly. .. autoclasstoc:: '''
[docs] def __init__(self, applications: Mapping[str, Application | ModifyDoc] | Application | ModifyDoc, io_loop: IOLoop | None = None, http_server_kwargs: Dict[str, Any] | None = None, **kwargs: Any) -> None: ''' Create a ``Server`` instance. Args: applications (dict[str, Application] or Application or callable) : A mapping from URL paths to Application instances, or a single Application to put at the root URL. The Application is a factory for Documents, with a new Document initialized for each Session. Each application is identified by a path that corresponds to a URL, like "/" or "/myapp" If a single Application is provided, it is mapped to the URL path "/" automatically. As a convenience, a callable may also be provided, in which an Application will be created for it using ``FunctionHandler``. io_loop (IOLoop, optional) : An explicit Tornado ``IOLoop`` to run Bokeh Server code on. If None, ``IOLoop.current()`` will be used (default: None) http_server_kwargs (dict, optional) : Extra arguments passed to ``tornado.httpserver.HTTPServer``. E.g. ``max_buffer_size`` to specify the maximum upload size. More details can be found at: http://www.tornadoweb.org/en/stable/httpserver.html#http-server If None, no extra arguments are passed (default: None) Additionally, the following options may be passed to configure the operation of ``Server``: .. bokeh-options:: _ServerOpts :module: bokeh.server.server Any remaining keyword arguments will be passed as-is to ``BokehTornado``. ''' log.info("Starting Bokeh server version %s (running on Tornado %s)" % (__version__, tornado_version)) opts = _ServerOpts(kwargs) if opts.num_procs > 1 and io_loop is not None: raise RuntimeError( "Setting both num_procs and io_loop in Server is incompatible. Use BaseServer to coordinate an explicit IOLoop and multi-process HTTPServer" ) if opts.num_procs > 1 and sys.platform == "win32": raise RuntimeError("num_procs > 1 not supported on Windows") if http_server_kwargs is None: http_server_kwargs = {} http_server_kwargs.setdefault('xheaders', opts.use_xheaders) if opts.ssl_certfile: log.info("Configuring for SSL termination") import ssl context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.load_cert_chain(certfile=opts.ssl_certfile, keyfile=opts.ssl_keyfile, password=opts.ssl_password) http_server_kwargs['ssl_options'] = context sockets, self._port = bind_sockets(opts.address, opts.port) self._address = opts.address extra_websocket_origins = create_hosts_allowlist(opts.allow_websocket_origin, self.port) try: tornado_app = BokehTornado(applications, extra_websocket_origins=extra_websocket_origins, prefix=opts.prefix, index=opts.index, websocket_max_message_size_bytes=opts.websocket_max_message_size, **kwargs) if opts.num_procs != 1: assert all(app_context.application.safe_to_fork for app_context in tornado_app.applications.values()), ( 'User application code has run before attempting to start ' 'multiple processes. This is considered an unsafe operation.') http_server = HTTPServer(tornado_app, **http_server_kwargs) http_server.start(opts.num_procs) http_server.add_sockets(sockets) except Exception: for s in sockets: s.close() raise # Can only refer to IOLoop after HTTPServer.start() is called, see #5524 if io_loop is None: io_loop = IOLoop.current() super().__init__(io_loop, tornado_app, http_server)
@property def port(self) -> int: ''' The configured port number that the server listens on for HTTP requests. ''' return self._port @property def address(self) -> str | None: ''' The configured address that the server listens on for HTTP requests. ''' return self._address
#----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- # This class itself is intentionally undocumented (it is used to generate # documentation elsewhere) class _ServerOpts(Options): num_procs: int = Int(default=1, help=""" The number of worker processes to start for the HTTP server. If an explicit ``io_loop`` is also configured, then ``num_procs=1`` is the only compatible value. Use ``BaseServer`` to coordinate an explicit ``IOLoop`` with a multi-process HTTP server. A value of 0 will auto detect number of cores. Note that due to limitations inherent in Tornado, Windows does not support ``num_procs`` values greater than one! In this case consider running multiple Bokeh server instances behind a load balancer. """) # type: ignore[assignment] address : str | None = Nullable(String, help=""" The address the server should listen on for HTTP requests. """) # type: ignore[assignment] port: int = Int(default=DEFAULT_SERVER_PORT, help=""" The port number the server should listen on for HTTP requests. """) # type: ignore[assignment] prefix: str = String(default="", help=""" A URL prefix to use for all Bokeh server paths. """) # type: ignore[assignment] index: str | None = Nullable(String, help=""" A path to a Jinja2 template to use for the index "/" """) # type: ignore[assignment] allow_websocket_origin: List[str] | None = Nullable(p.List(String), help=""" A list of hosts that can connect to the websocket. This is typically required when embedding a Bokeh server app in an external web site using :func:`~bokeh.embed.server_document` or similar. If None, "localhost" is used. """) # type: ignore[assignment] use_xheaders: bool = Bool(default=False, help=""" Whether to have the Bokeh server override the remote IP and URI scheme and protocol for all requests with ``X-Real-Ip``, ``X-Forwarded-For``, ``X-Scheme``, ``X-Forwarded-Proto`` headers (if they are provided). """) # type: ignore[assignment] ssl_certfile: str | None = Nullable(String, help=""" The path to a certificate file for SSL termination. """) # type: ignore[assignment] ssl_keyfile: str | None = Nullable(String, help=""" The path to a private key file for SSL termination. """) # type: ignore[assignment] ssl_password: str | None = Nullable(String, help=""" A password to decrypt the SSL keyfile, if necessary. """) # type: ignore[assignment] websocket_max_message_size: int = Int(default=DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES, help=""" Set the Tornado ``websocket_max_message_size`` value. """) # type: ignore[assignment] #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------