#-----------------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------------
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)
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
#-----------------------------------------------------------------------------