#----------------------------------------------------------------------------- # Copyright (c) 2012 - 2021, 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 #----------------------------------------------------------------------------- import logging # isort:skip log = logging.getLogger(__name__) #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- # Standard library imports import atexit import signal import socket import sys # 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.properties import Bool, Int, List, 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 #----------------------------------------------------------------------------- # 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. ''' [docs] def __init__(self, io_loop, tornado_app, http_server): ''' 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): ''' The Tornado ``IOLoop`` that this Bokeh Server is running on. ''' return self._loop [docs] def start(self): ''' 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=True): ''' 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): ''' Stop listening on ports. The server will no longer be usable after calling this function. Returns: None ''' yield self._http.close_all_connections() self._http.stop() [docs] def run_until_shutdown(self): ''' 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) try: self._loop.start() except KeyboardInterrupt: print("\nInterrupted, shutting down") self.stop() [docs] def get_session(self, app_path, session_id): ''' 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=None): ''' 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 = [] for path in self._tornado.app_paths: all_sessions += self._tornado.get_sessions(path) return all_sessions [docs] def show(self, app_path, browser=None, new='tab'): ''' 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 ``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 = "http://%s:%d%s%s" % (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): 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, frame): print("Received signal %d, shutting down" % (signum,)) # Tell self._loop.start() to return. self._loop.add_callback_from_signal(self._loop.stop) @property def port(self): ''' 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): ''' 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): ''' The configured URL prefix to use for all Bokeh server paths. ''' return self._tornado.prefix @property def index(self): ''' 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. ''' [docs] def __init__(self, applications, io_loop=None, http_server_kwargs=None, **kwargs): ''' 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): ''' The configured port number that the server listens on for HTTP requests. ''' return self._port @property def address(self): ''' 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(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. """) address = Nullable(String, help=""" The address the server should listen on for HTTP requests. """) port = Int(default=DEFAULT_SERVER_PORT, help=""" The port number the server should listen on for HTTP requests. """) prefix = String(default="", help=""" A URL prefix to use for all Bokeh server paths. """) index = Nullable(String, help=""" A path to a Jinja2 template to use for the index "/" """) allow_websocket_origin = Nullable(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. """) use_xheaders = 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). """) ssl_certfile = Nullable(String, help=""" The path to a certificate file for SSL termination. """) ssl_keyfile = Nullable(String, help=""" The path to a private key file for SSL termination. """) ssl_password = Nullable(String, help=""" A password to decrypt the SSL keyfile, if necessary. """) websocket_max_message_size = Int(default=DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES, help=""" Set the Tornado ``websocket_max_message_size`` value. """) #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------