You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			3282 lines
		
	
	
		
			118 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			3282 lines
		
	
	
		
			118 KiB
		
	
	
	
		
			Python
		
	
"""A tornado based Jupyter server."""
 | 
						|
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import datetime
 | 
						|
import errno
 | 
						|
import gettext
 | 
						|
import hashlib
 | 
						|
import hmac
 | 
						|
import ipaddress
 | 
						|
import json
 | 
						|
import logging
 | 
						|
import mimetypes
 | 
						|
import os
 | 
						|
import pathlib
 | 
						|
import random
 | 
						|
import re
 | 
						|
import select
 | 
						|
import signal
 | 
						|
import socket
 | 
						|
import stat
 | 
						|
import sys
 | 
						|
import threading
 | 
						|
import time
 | 
						|
import typing as t
 | 
						|
import urllib
 | 
						|
import warnings
 | 
						|
from base64 import encodebytes
 | 
						|
from functools import partial
 | 
						|
from pathlib import Path
 | 
						|
 | 
						|
import jupyter_client
 | 
						|
from jupyter_client.kernelspec import KernelSpecManager
 | 
						|
from jupyter_client.manager import KernelManager
 | 
						|
from jupyter_client.session import Session
 | 
						|
from jupyter_core.application import JupyterApp, base_aliases, base_flags
 | 
						|
from jupyter_core.paths import jupyter_runtime_dir
 | 
						|
from jupyter_events.logger import EventLogger
 | 
						|
from nbformat.sign import NotebookNotary
 | 
						|
from tornado import httpserver, ioloop, web
 | 
						|
from tornado.httputil import url_concat
 | 
						|
from tornado.log import LogFormatter, access_log, app_log, gen_log
 | 
						|
from tornado.netutil import bind_sockets
 | 
						|
from tornado.routing import Matcher, Rule
 | 
						|
 | 
						|
if not sys.platform.startswith("win"):
 | 
						|
    from tornado.netutil import bind_unix_socket
 | 
						|
 | 
						|
if sys.platform.startswith("win"):
 | 
						|
    try:
 | 
						|
        import colorama
 | 
						|
 | 
						|
        colorama.init()
 | 
						|
    except ImportError:
 | 
						|
        pass
 | 
						|
 | 
						|
from traitlets import (
 | 
						|
    Any,
 | 
						|
    Bool,
 | 
						|
    Bytes,
 | 
						|
    Dict,
 | 
						|
    Float,
 | 
						|
    Instance,
 | 
						|
    Integer,
 | 
						|
    List,
 | 
						|
    TraitError,
 | 
						|
    Type,
 | 
						|
    Unicode,
 | 
						|
    Union,
 | 
						|
    default,
 | 
						|
    observe,
 | 
						|
    validate,
 | 
						|
)
 | 
						|
from traitlets.config import Config
 | 
						|
from traitlets.config.application import boolean_flag, catch_config_error
 | 
						|
 | 
						|
from jupyter_server import (
 | 
						|
    DEFAULT_EVENTS_SCHEMA_PATH,
 | 
						|
    DEFAULT_JUPYTER_SERVER_PORT,
 | 
						|
    DEFAULT_STATIC_FILES_PATH,
 | 
						|
    DEFAULT_TEMPLATE_PATH_LIST,
 | 
						|
    JUPYTER_SERVER_EVENTS_URI,
 | 
						|
    __version__,
 | 
						|
)
 | 
						|
from jupyter_server._sysinfo import get_sys_info
 | 
						|
from jupyter_server._tz import utcnow
 | 
						|
from jupyter_server.auth.authorizer import AllowAllAuthorizer, Authorizer
 | 
						|
from jupyter_server.auth.identity import (
 | 
						|
    IdentityProvider,
 | 
						|
    LegacyIdentityProvider,
 | 
						|
    PasswordIdentityProvider,
 | 
						|
)
 | 
						|
from jupyter_server.auth.login import LoginHandler
 | 
						|
from jupyter_server.auth.logout import LogoutHandler
 | 
						|
from jupyter_server.base.handlers import (
 | 
						|
    FileFindHandler,
 | 
						|
    MainHandler,
 | 
						|
    RedirectWithParams,
 | 
						|
    Template404,
 | 
						|
)
 | 
						|
from jupyter_server.extension.config import ExtensionConfigManager
 | 
						|
from jupyter_server.extension.manager import ExtensionManager
 | 
						|
from jupyter_server.extension.serverextension import ServerExtensionApp
 | 
						|
from jupyter_server.gateway.connections import GatewayWebSocketConnection
 | 
						|
from jupyter_server.gateway.gateway_client import GatewayClient
 | 
						|
from jupyter_server.gateway.managers import (
 | 
						|
    GatewayKernelSpecManager,
 | 
						|
    GatewayMappingKernelManager,
 | 
						|
    GatewaySessionManager,
 | 
						|
)
 | 
						|
from jupyter_server.log import log_request
 | 
						|
from jupyter_server.prometheus.metrics import (
 | 
						|
    ACTIVE_DURATION,
 | 
						|
    LAST_ACTIVITY,
 | 
						|
    SERVER_EXTENSION_INFO,
 | 
						|
    SERVER_INFO,
 | 
						|
    SERVER_STARTED,
 | 
						|
)
 | 
						|
from jupyter_server.services.config import ConfigManager
 | 
						|
from jupyter_server.services.contents.filemanager import (
 | 
						|
    AsyncFileContentsManager,
 | 
						|
    FileContentsManager,
 | 
						|
)
 | 
						|
from jupyter_server.services.contents.largefilemanager import AsyncLargeFileManager
 | 
						|
from jupyter_server.services.contents.manager import AsyncContentsManager, ContentsManager
 | 
						|
from jupyter_server.services.kernels.connection.base import BaseKernelWebsocketConnection
 | 
						|
from jupyter_server.services.kernels.connection.channels import ZMQChannelsWebsocketConnection
 | 
						|
from jupyter_server.services.kernels.kernelmanager import (
 | 
						|
    AsyncMappingKernelManager,
 | 
						|
    MappingKernelManager,
 | 
						|
)
 | 
						|
from jupyter_server.services.sessions.sessionmanager import SessionManager
 | 
						|
from jupyter_server.utils import (
 | 
						|
    JupyterServerAuthWarning,
 | 
						|
    check_pid,
 | 
						|
    fetch,
 | 
						|
    unix_socket_in_use,
 | 
						|
    url_escape,
 | 
						|
    url_path_join,
 | 
						|
    urlencode_unix_socket_path,
 | 
						|
)
 | 
						|
 | 
						|
try:
 | 
						|
    import resource
 | 
						|
except ImportError:
 | 
						|
    # Windows
 | 
						|
    resource = None  # type:ignore[assignment]
 | 
						|
 | 
						|
from jinja2 import Environment, FileSystemLoader
 | 
						|
from jupyter_core.paths import secure_write
 | 
						|
from jupyter_core.utils import ensure_async
 | 
						|
 | 
						|
from jupyter_server.transutils import _i18n, trans
 | 
						|
from jupyter_server.utils import pathname2url, urljoin
 | 
						|
 | 
						|
# the minimum viable tornado version: needs to be kept in sync with setup.py
 | 
						|
MIN_TORNADO = (6, 1, 0)
 | 
						|
 | 
						|
try:
 | 
						|
    import tornado
 | 
						|
 | 
						|
    assert tornado.version_info >= MIN_TORNADO
 | 
						|
except (ImportError, AttributeError, AssertionError) as e:  # pragma: no cover
 | 
						|
    raise ImportError(_i18n("The Jupyter Server requires tornado >=%s.%s.%s") % MIN_TORNADO) from e
 | 
						|
 | 
						|
try:
 | 
						|
    import resource
 | 
						|
except ImportError:
 | 
						|
    # Windows
 | 
						|
    resource = None  # type:ignore[assignment]
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# Module globals
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
_examples = """
 | 
						|
jupyter server                       # start the server
 | 
						|
jupyter server  --certfile=mycert.pem # use SSL/TLS certificate
 | 
						|
jupyter server password              # enter a password to protect the server
 | 
						|
"""
 | 
						|
 | 
						|
JUPYTER_SERVICE_HANDLERS = {
 | 
						|
    "auth": None,
 | 
						|
    "api": ["jupyter_server.services.api.handlers"],
 | 
						|
    "config": ["jupyter_server.services.config.handlers"],
 | 
						|
    "contents": ["jupyter_server.services.contents.handlers"],
 | 
						|
    "files": ["jupyter_server.files.handlers"],
 | 
						|
    "kernels": [
 | 
						|
        "jupyter_server.services.kernels.handlers",
 | 
						|
    ],
 | 
						|
    "kernelspecs": [
 | 
						|
        "jupyter_server.kernelspecs.handlers",
 | 
						|
        "jupyter_server.services.kernelspecs.handlers",
 | 
						|
    ],
 | 
						|
    "nbconvert": [
 | 
						|
        "jupyter_server.nbconvert.handlers",
 | 
						|
        "jupyter_server.services.nbconvert.handlers",
 | 
						|
    ],
 | 
						|
    "security": ["jupyter_server.services.security.handlers"],
 | 
						|
    "sessions": ["jupyter_server.services.sessions.handlers"],
 | 
						|
    "shutdown": ["jupyter_server.services.shutdown"],
 | 
						|
    "view": ["jupyter_server.view.handlers"],
 | 
						|
    "events": ["jupyter_server.services.events.handlers"],
 | 
						|
}
 | 
						|
 | 
						|
# Added for backwards compatibility from classic notebook server.
 | 
						|
DEFAULT_SERVER_PORT = DEFAULT_JUPYTER_SERVER_PORT
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# Helper functions
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
def random_ports(port: int, n: int) -> t.Generator[int, None, None]:
 | 
						|
    """Generate a list of n random ports near the given port.
 | 
						|
 | 
						|
    The first 5 ports will be sequential, and the remaining n-5 will be
 | 
						|
    randomly selected in the range [port-2*n, port+2*n].
 | 
						|
    """
 | 
						|
    for i in range(min(5, n)):
 | 
						|
        yield port + i
 | 
						|
    for _ in range(n - 5):
 | 
						|
        yield max(1, port + random.randint(-2 * n, 2 * n))  # noqa: S311
 | 
						|
 | 
						|
 | 
						|
def load_handlers(name: str) -> t.Any:
 | 
						|
    """Load the (URL pattern, handler) tuples for each component."""
 | 
						|
    mod = __import__(name, fromlist=["default_handlers"])
 | 
						|
    return mod.default_handlers
 | 
						|
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# The Tornado web application
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
class ServerWebApplication(web.Application):
 | 
						|
    """A server web application."""
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        jupyter_app,
 | 
						|
        default_services,
 | 
						|
        kernel_manager,
 | 
						|
        contents_manager,
 | 
						|
        session_manager,
 | 
						|
        kernel_spec_manager,
 | 
						|
        config_manager,
 | 
						|
        event_logger,
 | 
						|
        extra_services,
 | 
						|
        log,
 | 
						|
        base_url,
 | 
						|
        default_url,
 | 
						|
        settings_overrides,
 | 
						|
        jinja_env_options,
 | 
						|
        *,
 | 
						|
        authorizer=None,
 | 
						|
        identity_provider=None,
 | 
						|
        kernel_websocket_connection_class=None,
 | 
						|
        websocket_ping_interval=None,
 | 
						|
        websocket_ping_timeout=None,
 | 
						|
    ):
 | 
						|
        """Initialize a server web application."""
 | 
						|
        if identity_provider is None:
 | 
						|
            warnings.warn(
 | 
						|
                "identity_provider unspecified. Using default IdentityProvider."
 | 
						|
                " Specify an identity_provider to avoid this message.",
 | 
						|
                RuntimeWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
            identity_provider = IdentityProvider(parent=jupyter_app)
 | 
						|
 | 
						|
        if authorizer is None:
 | 
						|
            warnings.warn(
 | 
						|
                "authorizer unspecified. Using permissive AllowAllAuthorizer."
 | 
						|
                " Specify an authorizer to avoid this message.",
 | 
						|
                JupyterServerAuthWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
            authorizer = AllowAllAuthorizer(parent=jupyter_app, identity_provider=identity_provider)
 | 
						|
 | 
						|
        settings = self.init_settings(
 | 
						|
            jupyter_app,
 | 
						|
            kernel_manager,
 | 
						|
            contents_manager,
 | 
						|
            session_manager,
 | 
						|
            kernel_spec_manager,
 | 
						|
            config_manager,
 | 
						|
            event_logger,
 | 
						|
            extra_services,
 | 
						|
            log,
 | 
						|
            base_url,
 | 
						|
            default_url,
 | 
						|
            settings_overrides,
 | 
						|
            jinja_env_options,
 | 
						|
            authorizer=authorizer,
 | 
						|
            identity_provider=identity_provider,
 | 
						|
            kernel_websocket_connection_class=kernel_websocket_connection_class,
 | 
						|
            websocket_ping_interval=websocket_ping_interval,
 | 
						|
            websocket_ping_timeout=websocket_ping_timeout,
 | 
						|
        )
 | 
						|
        handlers = self.init_handlers(default_services, settings)
 | 
						|
 | 
						|
        undecorated_methods = []
 | 
						|
        for matcher, handler, *_ in handlers:
 | 
						|
            undecorated_methods.extend(self._check_handler_auth(matcher, handler))
 | 
						|
 | 
						|
        if undecorated_methods:
 | 
						|
            message = (
 | 
						|
                "Core endpoints without @allow_unauthenticated, @ws_authenticated, nor @web.authenticated:\n"
 | 
						|
                + "\n".join(undecorated_methods)
 | 
						|
            )
 | 
						|
            if jupyter_app.allow_unauthenticated_access:
 | 
						|
                warnings.warn(
 | 
						|
                    message,
 | 
						|
                    JupyterServerAuthWarning,
 | 
						|
                    stacklevel=2,
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                raise Exception(message)
 | 
						|
 | 
						|
        super().__init__(handlers, **settings)
 | 
						|
 | 
						|
    def add_handlers(self, host_pattern, host_handlers):
 | 
						|
        undecorated_methods = []
 | 
						|
        for rule in host_handlers:
 | 
						|
            if isinstance(rule, Rule):
 | 
						|
                matcher = rule.matcher
 | 
						|
                handler = rule.target
 | 
						|
            else:
 | 
						|
                matcher, handler, *_ = rule
 | 
						|
            undecorated_methods.extend(self._check_handler_auth(matcher, handler))
 | 
						|
 | 
						|
        if undecorated_methods and not self.settings["allow_unauthenticated_access"]:
 | 
						|
            message = (
 | 
						|
                "Extension endpoints without @allow_unauthenticated, @ws_authenticated, nor @web.authenticated:\n"
 | 
						|
                + "\n".join(undecorated_methods)
 | 
						|
            )
 | 
						|
            warnings.warn(
 | 
						|
                message,
 | 
						|
                JupyterServerAuthWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
 | 
						|
        return super().add_handlers(host_pattern, host_handlers)
 | 
						|
 | 
						|
    def init_settings(
 | 
						|
        self,
 | 
						|
        jupyter_app,
 | 
						|
        kernel_manager,
 | 
						|
        contents_manager,
 | 
						|
        session_manager,
 | 
						|
        kernel_spec_manager,
 | 
						|
        config_manager,
 | 
						|
        event_logger,
 | 
						|
        extra_services,
 | 
						|
        log,
 | 
						|
        base_url,
 | 
						|
        default_url,
 | 
						|
        settings_overrides,
 | 
						|
        jinja_env_options=None,
 | 
						|
        *,
 | 
						|
        authorizer=None,
 | 
						|
        identity_provider=None,
 | 
						|
        kernel_websocket_connection_class=None,
 | 
						|
        websocket_ping_interval=None,
 | 
						|
        websocket_ping_timeout=None,
 | 
						|
    ):
 | 
						|
        """Initialize settings for the web application."""
 | 
						|
        _template_path = settings_overrides.get(
 | 
						|
            "template_path",
 | 
						|
            jupyter_app.template_file_path,
 | 
						|
        )
 | 
						|
        if isinstance(_template_path, str):
 | 
						|
            _template_path = (_template_path,)
 | 
						|
        template_path = [os.path.expanduser(path) for path in _template_path]
 | 
						|
 | 
						|
        jenv_opt: dict[str, t.Any] = {"autoescape": True}
 | 
						|
        jenv_opt.update(jinja_env_options if jinja_env_options else {})
 | 
						|
 | 
						|
        env = Environment(  # noqa: S701
 | 
						|
            loader=FileSystemLoader(template_path), extensions=["jinja2.ext.i18n"], **jenv_opt
 | 
						|
        )
 | 
						|
        sys_info = get_sys_info()
 | 
						|
 | 
						|
        base_dir = os.path.realpath(os.path.join(__file__, "..", ".."))
 | 
						|
        nbui = gettext.translation(
 | 
						|
            "nbui",
 | 
						|
            localedir=os.path.join(base_dir, "jupyter_server/i18n"),
 | 
						|
            fallback=True,
 | 
						|
        )
 | 
						|
        env.install_gettext_translations(nbui, newstyle=False)
 | 
						|
 | 
						|
        if sys_info["commit_source"] == "repository":
 | 
						|
            # don't cache (rely on 304) when working from master
 | 
						|
            version_hash = ""
 | 
						|
        else:
 | 
						|
            # reset the cache on server restart
 | 
						|
            utc = datetime.timezone.utc
 | 
						|
            version_hash = datetime.datetime.now(tz=utc).strftime("%Y%m%d%H%M%S")
 | 
						|
 | 
						|
        now = utcnow()
 | 
						|
 | 
						|
        root_dir = contents_manager.root_dir
 | 
						|
        home = os.path.expanduser("~")
 | 
						|
        if root_dir.startswith(home + os.path.sep):
 | 
						|
            # collapse $HOME to ~
 | 
						|
            root_dir = "~" + root_dir[len(home) :]
 | 
						|
 | 
						|
        settings = {
 | 
						|
            # basics
 | 
						|
            "log_function": partial(
 | 
						|
                log_request, record_prometheus_metrics=jupyter_app.record_http_request_metrics
 | 
						|
            ),
 | 
						|
            "base_url": base_url,
 | 
						|
            "default_url": default_url,
 | 
						|
            "template_path": template_path,
 | 
						|
            "static_path": jupyter_app.static_file_path,
 | 
						|
            "static_custom_path": jupyter_app.static_custom_path,
 | 
						|
            "static_handler_class": FileFindHandler,
 | 
						|
            "static_url_prefix": url_path_join(base_url, "/static/"),
 | 
						|
            "static_handler_args": {
 | 
						|
                # don't cache custom.js
 | 
						|
                "no_cache_paths": [url_path_join(base_url, "static", "custom")],
 | 
						|
            },
 | 
						|
            "version_hash": version_hash,
 | 
						|
            # kernel message protocol over websocket
 | 
						|
            "kernel_ws_protocol": jupyter_app.kernel_ws_protocol,
 | 
						|
            # rate limits
 | 
						|
            "limit_rate": jupyter_app.limit_rate,
 | 
						|
            "iopub_msg_rate_limit": jupyter_app.iopub_msg_rate_limit,
 | 
						|
            "iopub_data_rate_limit": jupyter_app.iopub_data_rate_limit,
 | 
						|
            "rate_limit_window": jupyter_app.rate_limit_window,
 | 
						|
            # authentication
 | 
						|
            "cookie_secret": jupyter_app.cookie_secret,
 | 
						|
            "login_url": url_path_join(base_url, "/login"),
 | 
						|
            "xsrf_cookies": True,
 | 
						|
            "disable_check_xsrf": jupyter_app.disable_check_xsrf,
 | 
						|
            "allow_unauthenticated_access": jupyter_app.allow_unauthenticated_access,
 | 
						|
            "allow_remote_access": jupyter_app.allow_remote_access,
 | 
						|
            "local_hostnames": jupyter_app.local_hostnames,
 | 
						|
            "authenticate_prometheus": jupyter_app.authenticate_prometheus,
 | 
						|
            "extra_log_scrub_param_keys": jupyter_app.extra_log_scrub_param_keys,
 | 
						|
            # managers
 | 
						|
            "kernel_manager": kernel_manager,
 | 
						|
            "contents_manager": contents_manager,
 | 
						|
            "session_manager": session_manager,
 | 
						|
            "kernel_spec_manager": kernel_spec_manager,
 | 
						|
            "config_manager": config_manager,
 | 
						|
            "authorizer": authorizer,
 | 
						|
            "identity_provider": identity_provider,
 | 
						|
            "event_logger": event_logger,
 | 
						|
            "kernel_websocket_connection_class": kernel_websocket_connection_class,
 | 
						|
            "websocket_ping_interval": websocket_ping_interval,
 | 
						|
            "websocket_ping_timeout": websocket_ping_timeout,
 | 
						|
            # handlers
 | 
						|
            "extra_services": extra_services,
 | 
						|
            # Jupyter stuff
 | 
						|
            "started": now,
 | 
						|
            # place for extensions to register activity
 | 
						|
            # so that they can prevent idle-shutdown
 | 
						|
            "last_activity_times": {},
 | 
						|
            "jinja_template_vars": jupyter_app.jinja_template_vars,
 | 
						|
            "websocket_url": jupyter_app.websocket_url,
 | 
						|
            "shutdown_button": jupyter_app.quit_button,
 | 
						|
            "config": jupyter_app.config,
 | 
						|
            "config_dir": jupyter_app.config_dir,
 | 
						|
            "allow_password_change": jupyter_app.allow_password_change,
 | 
						|
            "server_root_dir": root_dir,
 | 
						|
            "jinja2_env": env,
 | 
						|
            "serverapp": jupyter_app,
 | 
						|
        }
 | 
						|
 | 
						|
        # allow custom overrides for the tornado web app.
 | 
						|
        settings.update(settings_overrides)
 | 
						|
 | 
						|
        if base_url and "xsrf_cookie_kwargs" not in settings:
 | 
						|
            # default: set xsrf cookie on base_url
 | 
						|
            settings["xsrf_cookie_kwargs"] = {"path": base_url}
 | 
						|
        return settings
 | 
						|
 | 
						|
    def init_handlers(self, default_services, settings):
 | 
						|
        """Load the (URL pattern, handler) tuples for each component."""
 | 
						|
        # Order matters. The first handler to match the URL will handle the request.
 | 
						|
        handlers = []
 | 
						|
        # load extra services specified by users before default handlers
 | 
						|
        for service in settings["extra_services"]:
 | 
						|
            handlers.extend(load_handlers(service))
 | 
						|
 | 
						|
        # Load default services. Raise exception if service not
 | 
						|
        # found in JUPYTER_SERVICE_HANLDERS.
 | 
						|
        for service in default_services:
 | 
						|
            if service in JUPYTER_SERVICE_HANDLERS:
 | 
						|
                locations = JUPYTER_SERVICE_HANDLERS[service]
 | 
						|
                if locations is not None:
 | 
						|
                    for loc in locations:
 | 
						|
                        handlers.extend(load_handlers(loc))
 | 
						|
            else:
 | 
						|
                msg = (
 | 
						|
                    f"{service} is not recognized as a jupyter_server "
 | 
						|
                    "service. If this is a custom service, "
 | 
						|
                    "try adding it to the "
 | 
						|
                    "`extra_services` list."
 | 
						|
                )
 | 
						|
                raise Exception(msg)
 | 
						|
 | 
						|
        # Add extra handlers from contents manager.
 | 
						|
        handlers.extend(settings["contents_manager"].get_extra_handlers())
 | 
						|
        # And from identity provider
 | 
						|
        handlers.extend(settings["identity_provider"].get_handlers())
 | 
						|
 | 
						|
        # register base handlers last
 | 
						|
        handlers.extend(load_handlers("jupyter_server.base.handlers"))
 | 
						|
 | 
						|
        if settings["default_url"] != settings["base_url"]:
 | 
						|
            # set the URL that will be redirected from `/`
 | 
						|
            handlers.append(
 | 
						|
                (
 | 
						|
                    r"/?",
 | 
						|
                    RedirectWithParams,
 | 
						|
                    {
 | 
						|
                        "url": settings["default_url"],
 | 
						|
                        "permanent": False,  # want 302, not 301
 | 
						|
                    },
 | 
						|
                )
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            handlers.append((r"/", MainHandler))
 | 
						|
 | 
						|
        # prepend base_url onto the patterns that we match
 | 
						|
        new_handlers = []
 | 
						|
        for handler in handlers:
 | 
						|
            pattern = url_path_join(settings["base_url"], handler[0])
 | 
						|
            new_handler = (pattern, *list(handler[1:]))
 | 
						|
            new_handlers.append(new_handler)
 | 
						|
        # add 404 on the end, which will catch everything that falls through
 | 
						|
        new_handlers.append((r"(.*)", Template404))
 | 
						|
        return new_handlers
 | 
						|
 | 
						|
    def last_activity(self):
 | 
						|
        """Get a UTC timestamp for when the server last did something.
 | 
						|
 | 
						|
        Includes: API activity, kernel activity, kernel shutdown, and terminal
 | 
						|
        activity.
 | 
						|
        """
 | 
						|
        sources = [
 | 
						|
            self.settings["started"],
 | 
						|
            self.settings["kernel_manager"].last_kernel_activity,
 | 
						|
        ]
 | 
						|
        # Any setting that ends with a key that ends with `_last_activity` is
 | 
						|
        # counted here. This provides a hook for extensions to add a last activity
 | 
						|
        # setting to the server.
 | 
						|
        sources.extend(
 | 
						|
            [val for key, val in self.settings.items() if key.endswith("_last_activity")]
 | 
						|
        )
 | 
						|
        sources.extend(self.settings["last_activity_times"].values())
 | 
						|
        return max(sources)
 | 
						|
 | 
						|
    def _check_handler_auth(
 | 
						|
        self, matcher: t.Union[str, Matcher], handler: type[web.RequestHandler]
 | 
						|
    ):
 | 
						|
        missing_authentication = []
 | 
						|
        for method_name in handler.SUPPORTED_METHODS:
 | 
						|
            method = getattr(handler, method_name.lower())
 | 
						|
            is_unimplemented = method == web.RequestHandler._unimplemented_method
 | 
						|
            is_allowlisted = hasattr(method, "__allow_unauthenticated")
 | 
						|
            is_blocklisted = _has_tornado_web_authenticated(method)
 | 
						|
            if not is_unimplemented and not is_allowlisted and not is_blocklisted:
 | 
						|
                missing_authentication.append(
 | 
						|
                    f"- {method_name} of {handler.__name__} registered for {matcher}"
 | 
						|
                )
 | 
						|
        return missing_authentication
 | 
						|
 | 
						|
 | 
						|
def _has_tornado_web_authenticated(method: t.Callable[..., t.Any]) -> bool:
 | 
						|
    """Check if given method was decorated with @web.authenticated.
 | 
						|
 | 
						|
    Note: it is ok if we reject on @authorized @web.authenticated
 | 
						|
    because the correct order is @web.authenticated @authorized.
 | 
						|
    """
 | 
						|
    if not hasattr(method, "__wrapped__"):
 | 
						|
        return False
 | 
						|
    if not hasattr(method, "__code__"):
 | 
						|
        return False
 | 
						|
    code = method.__code__
 | 
						|
    if hasattr(code, "co_qualname"):
 | 
						|
        # new in 3.11
 | 
						|
        return code.co_qualname.startswith("authenticated")  # type:ignore[no-any-return]
 | 
						|
    elif hasattr(code, "co_filename"):
 | 
						|
        return code.co_filename.replace("\\", "/").endswith("tornado/web.py")
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
class JupyterPasswordApp(JupyterApp):
 | 
						|
    """Set a password for the Jupyter server.
 | 
						|
 | 
						|
    Setting a password secures the Jupyter server
 | 
						|
    and removes the need for token-based authentication.
 | 
						|
    """
 | 
						|
 | 
						|
    description: str = __doc__
 | 
						|
 | 
						|
    def _config_file_default(self):
 | 
						|
        """the default config file."""
 | 
						|
        return os.path.join(self.config_dir, "jupyter_server_config.json")
 | 
						|
 | 
						|
    def start(self):
 | 
						|
        """Start the password app."""
 | 
						|
        from jupyter_server.auth.security import set_password
 | 
						|
 | 
						|
        set_password(config_file=self.config_file)
 | 
						|
        self.log.info("Wrote hashed password to %s" % self.config_file)
 | 
						|
 | 
						|
 | 
						|
def shutdown_server(server_info, timeout=5, log=None):
 | 
						|
    """Shutdown a Jupyter server in a separate process.
 | 
						|
 | 
						|
    *server_info* should be a dictionary as produced by list_running_servers().
 | 
						|
 | 
						|
    Will first try to request shutdown using /api/shutdown .
 | 
						|
    On Unix, if the server is still running after *timeout* seconds, it will
 | 
						|
    send SIGTERM. After another timeout, it escalates to SIGKILL.
 | 
						|
 | 
						|
    Returns True if the server was stopped by any means, False if stopping it
 | 
						|
    failed (on Windows).
 | 
						|
    """
 | 
						|
 | 
						|
    url = server_info["url"]
 | 
						|
    pid = server_info["pid"]
 | 
						|
    try:
 | 
						|
        shutdown_url = urljoin(url, "api/shutdown")
 | 
						|
        if log:
 | 
						|
            log.debug("POST request to %s", shutdown_url)
 | 
						|
        fetch(
 | 
						|
            shutdown_url,
 | 
						|
            method="POST",
 | 
						|
            body=b"",
 | 
						|
            headers={"Authorization": "token " + server_info["token"]},
 | 
						|
        )
 | 
						|
    except Exception as ex:
 | 
						|
        if not str(ex) == "Unknown URL scheme.":
 | 
						|
            raise ex
 | 
						|
        if log:
 | 
						|
            log.debug("Was not a HTTP scheme. Treating as socket instead.")
 | 
						|
            log.debug("POST request to %s", url)
 | 
						|
        fetch(
 | 
						|
            url,
 | 
						|
            method="POST",
 | 
						|
            body=b"",
 | 
						|
            headers={"Authorization": "token " + server_info["token"]},
 | 
						|
        )
 | 
						|
 | 
						|
    # Poll to see if it shut down.
 | 
						|
    for _ in range(timeout * 10):
 | 
						|
        if not check_pid(pid):
 | 
						|
            if log:
 | 
						|
                log.debug("Server PID %s is gone", pid)
 | 
						|
            return True
 | 
						|
        time.sleep(0.1)
 | 
						|
 | 
						|
    if sys.platform.startswith("win"):
 | 
						|
        return False
 | 
						|
 | 
						|
    if log:
 | 
						|
        log.debug("SIGTERM to PID %s", pid)
 | 
						|
    os.kill(pid, signal.SIGTERM)
 | 
						|
 | 
						|
    # Poll to see if it shut down.
 | 
						|
    for _ in range(timeout * 10):
 | 
						|
        if not check_pid(pid):
 | 
						|
            if log:
 | 
						|
                log.debug("Server PID %s is gone", pid)
 | 
						|
            return True
 | 
						|
        time.sleep(0.1)
 | 
						|
 | 
						|
    if log:
 | 
						|
        log.debug("SIGKILL to PID %s", pid)
 | 
						|
    os.kill(pid, signal.SIGKILL)
 | 
						|
    return True  # SIGKILL cannot be caught
 | 
						|
 | 
						|
 | 
						|
class JupyterServerStopApp(JupyterApp):
 | 
						|
    """An application to stop a Jupyter server."""
 | 
						|
 | 
						|
    version: str = __version__
 | 
						|
    description: str = "Stop currently running Jupyter server for a given port"
 | 
						|
 | 
						|
    port = Integer(
 | 
						|
        DEFAULT_JUPYTER_SERVER_PORT,
 | 
						|
        config=True,
 | 
						|
        help="Port of the server to be killed. Default %s" % DEFAULT_JUPYTER_SERVER_PORT,
 | 
						|
    )
 | 
						|
 | 
						|
    sock = Unicode("", config=True, help="UNIX socket of the server to be killed.")
 | 
						|
 | 
						|
    def parse_command_line(self, argv=None):
 | 
						|
        """Parse command line options."""
 | 
						|
        super().parse_command_line(argv)
 | 
						|
        if self.extra_args:
 | 
						|
            try:
 | 
						|
                self.port = int(self.extra_args[0])
 | 
						|
            except ValueError:
 | 
						|
                # self.extra_args[0] was not an int, so it must be a string (unix socket).
 | 
						|
                self.sock = self.extra_args[0]
 | 
						|
 | 
						|
    def shutdown_server(self, server):
 | 
						|
        """Shut down a server."""
 | 
						|
        return shutdown_server(server, log=self.log)
 | 
						|
 | 
						|
    def _shutdown_or_exit(self, target_endpoint, server):
 | 
						|
        """Handle a shutdown."""
 | 
						|
        self.log.info("Shutting down server on %s..." % target_endpoint)
 | 
						|
        if not self.shutdown_server(server):
 | 
						|
            sys.exit("Could not stop server on %s" % target_endpoint)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _maybe_remove_unix_socket(socket_path):
 | 
						|
        """Try to remove a socket path."""
 | 
						|
        try:
 | 
						|
            os.unlink(socket_path)
 | 
						|
        except OSError:
 | 
						|
            pass
 | 
						|
 | 
						|
    def start(self):
 | 
						|
        """Start the server stop app."""
 | 
						|
        info = self.log.info
 | 
						|
        servers = list(list_running_servers(self.runtime_dir, log=self.log))
 | 
						|
        if not servers:
 | 
						|
            self.exit("There are no running servers (per %s)" % self.runtime_dir)
 | 
						|
        for server in servers:
 | 
						|
            if self.sock:
 | 
						|
                sock = server.get("sock", None)
 | 
						|
                if sock and sock == self.sock:
 | 
						|
                    self._shutdown_or_exit(sock, server)
 | 
						|
                    # Attempt to remove the UNIX socket after stopping.
 | 
						|
                    self._maybe_remove_unix_socket(sock)
 | 
						|
                    return
 | 
						|
            elif self.port:
 | 
						|
                port = server.get("port", None)
 | 
						|
                if port == self.port:
 | 
						|
                    self._shutdown_or_exit(port, server)
 | 
						|
                    return
 | 
						|
        current_endpoint = self.sock or self.port
 | 
						|
        info(f"There is currently no server running on {current_endpoint}")
 | 
						|
        info("Ports/sockets currently in use:")
 | 
						|
        for server in servers:
 | 
						|
            info(" - {}".format(server.get("sock") or server["port"]))
 | 
						|
        self.exit(1)
 | 
						|
 | 
						|
 | 
						|
class JupyterServerListApp(JupyterApp):
 | 
						|
    """An application to list running Jupyter servers."""
 | 
						|
 | 
						|
    version: str = __version__
 | 
						|
    description: str = _i18n("List currently running Jupyter servers.")
 | 
						|
 | 
						|
    flags = {
 | 
						|
        "jsonlist": (
 | 
						|
            {"JupyterServerListApp": {"jsonlist": True}},
 | 
						|
            _i18n("Produce machine-readable JSON list output."),
 | 
						|
        ),
 | 
						|
        "json": (
 | 
						|
            {"JupyterServerListApp": {"json": True}},
 | 
						|
            _i18n("Produce machine-readable JSON object on each line of output."),
 | 
						|
        ),
 | 
						|
    }
 | 
						|
 | 
						|
    jsonlist = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "If True, the output will be a JSON list of objects, one per "
 | 
						|
            "active Jupyer server, each with the details from the "
 | 
						|
            "relevant server info file."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
    json = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "If True, each line of output will be a JSON object with the "
 | 
						|
            "details from the server info file. For a JSON list output, "
 | 
						|
            "see the JupyterServerListApp.jsonlist configuration value"
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    def start(self):
 | 
						|
        """Start the server list application."""
 | 
						|
        serverinfo_list = list(list_running_servers(self.runtime_dir, log=self.log))
 | 
						|
        if self.jsonlist:
 | 
						|
            print(json.dumps(serverinfo_list, indent=2))
 | 
						|
        elif self.json:
 | 
						|
            for serverinfo in serverinfo_list:
 | 
						|
                print(json.dumps(serverinfo))
 | 
						|
        else:
 | 
						|
            print("Currently running servers:")
 | 
						|
            for serverinfo in serverinfo_list:
 | 
						|
                url = serverinfo["url"]
 | 
						|
                if serverinfo.get("token"):
 | 
						|
                    url = url + "?token=%s" % serverinfo["token"]
 | 
						|
                print(url, "::", serverinfo["root_dir"])
 | 
						|
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# Aliases and Flags
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
flags = dict(base_flags)
 | 
						|
 | 
						|
flags["allow-root"] = (
 | 
						|
    {"ServerApp": {"allow_root": True}},
 | 
						|
    _i18n("Allow the server to be run from root user."),
 | 
						|
)
 | 
						|
flags["no-browser"] = (
 | 
						|
    {"ServerApp": {"open_browser": False}, "ExtensionApp": {"open_browser": False}},
 | 
						|
    _i18n("Prevent the opening of the default url in the browser."),
 | 
						|
)
 | 
						|
flags["debug"] = (
 | 
						|
    {"ServerApp": {"log_level": "DEBUG"}, "ExtensionApp": {"log_level": "DEBUG"}},
 | 
						|
    _i18n("Set debug level for the extension and underlying server applications."),
 | 
						|
)
 | 
						|
flags["autoreload"] = (
 | 
						|
    {"ServerApp": {"autoreload": True}},
 | 
						|
    """Autoreload the webapp
 | 
						|
    Enable reloading of the tornado webapp and all imported Python packages
 | 
						|
    when any changes are made to any Python src files in server or
 | 
						|
    extensions.
 | 
						|
    """,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
# Add notebook manager flags
 | 
						|
flags.update(
 | 
						|
    boolean_flag(
 | 
						|
        "script",
 | 
						|
        "FileContentsManager.save_script",
 | 
						|
        "DEPRECATED, IGNORED",
 | 
						|
        "DEPRECATED, IGNORED",
 | 
						|
    )
 | 
						|
)
 | 
						|
 | 
						|
aliases = dict(base_aliases)
 | 
						|
 | 
						|
aliases.update(
 | 
						|
    {
 | 
						|
        "ip": "ServerApp.ip",
 | 
						|
        "port": "ServerApp.port",
 | 
						|
        "port-retries": "ServerApp.port_retries",
 | 
						|
        "sock": "ServerApp.sock",
 | 
						|
        "sock-mode": "ServerApp.sock_mode",
 | 
						|
        "transport": "KernelManager.transport",
 | 
						|
        "keyfile": "ServerApp.keyfile",
 | 
						|
        "certfile": "ServerApp.certfile",
 | 
						|
        "client-ca": "ServerApp.client_ca",
 | 
						|
        "notebook-dir": "ServerApp.root_dir",
 | 
						|
        "preferred-dir": "ServerApp.preferred_dir",
 | 
						|
        "browser": "ServerApp.browser",
 | 
						|
        "pylab": "ServerApp.pylab",
 | 
						|
        "gateway-url": "GatewayClient.url",
 | 
						|
    }
 | 
						|
)
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# ServerApp
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
 | 
						|
class ServerApp(JupyterApp):
 | 
						|
    """The Jupyter Server application class."""
 | 
						|
 | 
						|
    name = "jupyter-server"
 | 
						|
    version: str = __version__
 | 
						|
    description: str = _i18n(
 | 
						|
        """The Jupyter Server.
 | 
						|
 | 
						|
    This launches a Tornado-based Jupyter Server."""
 | 
						|
    )
 | 
						|
    examples = _examples
 | 
						|
 | 
						|
    flags = Dict(flags)  # type:ignore[assignment]
 | 
						|
    aliases = Dict(aliases)  # type:ignore[assignment]
 | 
						|
 | 
						|
    classes = [
 | 
						|
        KernelManager,
 | 
						|
        Session,
 | 
						|
        MappingKernelManager,
 | 
						|
        KernelSpecManager,
 | 
						|
        AsyncMappingKernelManager,
 | 
						|
        ContentsManager,
 | 
						|
        FileContentsManager,
 | 
						|
        AsyncContentsManager,
 | 
						|
        AsyncFileContentsManager,
 | 
						|
        NotebookNotary,
 | 
						|
        GatewayMappingKernelManager,
 | 
						|
        GatewayKernelSpecManager,
 | 
						|
        GatewaySessionManager,
 | 
						|
        GatewayWebSocketConnection,
 | 
						|
        GatewayClient,
 | 
						|
        Authorizer,
 | 
						|
        EventLogger,
 | 
						|
        ZMQChannelsWebsocketConnection,
 | 
						|
    ]
 | 
						|
 | 
						|
    subcommands: dict[str, t.Any] = {
 | 
						|
        "list": (
 | 
						|
            JupyterServerListApp,
 | 
						|
            JupyterServerListApp.description.splitlines()[0],
 | 
						|
        ),
 | 
						|
        "stop": (
 | 
						|
            JupyterServerStopApp,
 | 
						|
            JupyterServerStopApp.description.splitlines()[0],
 | 
						|
        ),
 | 
						|
        "password": (
 | 
						|
            JupyterPasswordApp,
 | 
						|
            JupyterPasswordApp.description.splitlines()[0],
 | 
						|
        ),
 | 
						|
        "extension": (
 | 
						|
            ServerExtensionApp,
 | 
						|
            ServerExtensionApp.description.splitlines()[0],
 | 
						|
        ),
 | 
						|
    }
 | 
						|
 | 
						|
    # A list of services whose handlers will be exposed.
 | 
						|
    # Subclasses can override this list to
 | 
						|
    # expose a subset of these handlers.
 | 
						|
    default_services = (
 | 
						|
        "api",
 | 
						|
        "auth",
 | 
						|
        "config",
 | 
						|
        "contents",
 | 
						|
        "files",
 | 
						|
        "kernels",
 | 
						|
        "kernelspecs",
 | 
						|
        "nbconvert",
 | 
						|
        "security",
 | 
						|
        "sessions",
 | 
						|
        "shutdown",
 | 
						|
        "view",
 | 
						|
        "events",
 | 
						|
    )
 | 
						|
 | 
						|
    _log_formatter_cls = LogFormatter  # type:ignore[assignment]
 | 
						|
    _stopping = Bool(False, help="Signal that we've begun stopping.")
 | 
						|
 | 
						|
    @default("log_level")
 | 
						|
    def _default_log_level(self) -> int:
 | 
						|
        return logging.INFO
 | 
						|
 | 
						|
    @default("log_format")
 | 
						|
    def _default_log_format(self) -> str:
 | 
						|
        """override default log format to include date & time"""
 | 
						|
        return (
 | 
						|
            "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
 | 
						|
        )
 | 
						|
 | 
						|
    # file to be opened in the Jupyter server
 | 
						|
    file_to_run = Unicode("", help="Open the named file when the application is launched.").tag(
 | 
						|
        config=True
 | 
						|
    )
 | 
						|
 | 
						|
    file_url_prefix = Unicode(
 | 
						|
        "notebooks", help="The URL prefix where files are opened directly."
 | 
						|
    ).tag(config=True)
 | 
						|
 | 
						|
    # Network related information
 | 
						|
    allow_origin = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help="""Set the Access-Control-Allow-Origin header
 | 
						|
 | 
						|
        Use '*' to allow any origin to access your server.
 | 
						|
 | 
						|
        Takes precedence over allow_origin_pat.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    allow_origin_pat = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help="""Use a regular expression for the Access-Control-Allow-Origin header
 | 
						|
 | 
						|
        Requests from an origin matching the expression will get replies with:
 | 
						|
 | 
						|
            Access-Control-Allow-Origin: origin
 | 
						|
 | 
						|
        where `origin` is the origin of the request.
 | 
						|
 | 
						|
        Ignored if allow_origin is set.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    allow_credentials = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Set the Access-Control-Allow-Credentials: true header"),
 | 
						|
    )
 | 
						|
 | 
						|
    allow_root = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Whether to allow the user to run the server as root."),
 | 
						|
    )
 | 
						|
 | 
						|
    autoreload = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Reload the webapp when changes are made to any Python src files."),
 | 
						|
    )
 | 
						|
 | 
						|
    default_url = Unicode("/", config=True, help=_i18n("The default URL to redirect to from `/`"))
 | 
						|
 | 
						|
    ip = Unicode(
 | 
						|
        "localhost",
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The IP address the Jupyter server will listen on."),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("ip")
 | 
						|
    def _default_ip(self) -> str:
 | 
						|
        """Return localhost if available, 127.0.0.1 otherwise.
 | 
						|
 | 
						|
        On some (horribly broken) systems, localhost cannot be bound.
 | 
						|
        """
 | 
						|
        s = socket.socket()
 | 
						|
        try:
 | 
						|
            s.bind(("localhost", 0))
 | 
						|
        except OSError as e:
 | 
						|
            self.log.warning(
 | 
						|
                _i18n("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s"), e
 | 
						|
            )
 | 
						|
            return "127.0.0.1"
 | 
						|
        else:
 | 
						|
            s.close()
 | 
						|
            return "localhost"
 | 
						|
 | 
						|
    @validate("ip")
 | 
						|
    def _validate_ip(self, proposal: t.Any) -> str:
 | 
						|
        value = t.cast(str, proposal["value"])
 | 
						|
        if value == "*":
 | 
						|
            value = ""
 | 
						|
        return value
 | 
						|
 | 
						|
    custom_display_url = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """Override URL shown to users.
 | 
						|
 | 
						|
        Replace actual URL, including protocol, address, port and base URL,
 | 
						|
        with the given value when displaying URL to the users. Do not change
 | 
						|
        the actual connection URL. If authentication token is enabled, the
 | 
						|
        token is added to the custom URL automatically.
 | 
						|
 | 
						|
        This option is intended to be used when the URL to display to the user
 | 
						|
        cannot be determined reliably by the Jupyter server (proxified
 | 
						|
        or containerized setups for example)."""
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    port_env = "JUPYTER_PORT"
 | 
						|
    port_default_value = DEFAULT_JUPYTER_SERVER_PORT
 | 
						|
 | 
						|
    port = Integer(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The port the server will listen on (env: JUPYTER_PORT)."),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("port")
 | 
						|
    def _port_default(self) -> int:
 | 
						|
        return int(os.getenv(self.port_env, self.port_default_value))
 | 
						|
 | 
						|
    port_retries_env = "JUPYTER_PORT_RETRIES"
 | 
						|
    port_retries_default_value = 50
 | 
						|
    port_retries = Integer(
 | 
						|
        port_retries_default_value,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "The number of additional ports to try if the specified port is not "
 | 
						|
            "available (env: JUPYTER_PORT_RETRIES)."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("port_retries")
 | 
						|
    def _port_retries_default(self) -> int:
 | 
						|
        return int(os.getenv(self.port_retries_env, self.port_retries_default_value))
 | 
						|
 | 
						|
    sock = Unicode("", config=True, help="The UNIX socket the Jupyter server will listen on.")
 | 
						|
 | 
						|
    sock_mode = Unicode(
 | 
						|
        "0600",
 | 
						|
        config=True,
 | 
						|
        help="The permissions mode for UNIX socket creation (default: 0600).",
 | 
						|
    )
 | 
						|
 | 
						|
    @validate("sock_mode")
 | 
						|
    def _validate_sock_mode(self, proposal: t.Any) -> t.Any:
 | 
						|
        value = proposal["value"]
 | 
						|
        try:
 | 
						|
            converted_value = int(value.encode(), 8)
 | 
						|
            assert all(
 | 
						|
                (
 | 
						|
                    # Ensure the mode is at least user readable/writable.
 | 
						|
                    bool(converted_value & stat.S_IRUSR),
 | 
						|
                    bool(converted_value & stat.S_IWUSR),
 | 
						|
                    # And isn't out of bounds.
 | 
						|
                    converted_value <= 2**12,
 | 
						|
                )
 | 
						|
            )
 | 
						|
        except ValueError as e:
 | 
						|
            raise TraitError(
 | 
						|
                'invalid --sock-mode value: %s, please specify as e.g. "0600"' % value
 | 
						|
            ) from e
 | 
						|
        except AssertionError as e:
 | 
						|
            raise TraitError(
 | 
						|
                "invalid --sock-mode value: %s, must have u+rw (0600) at a minimum" % value
 | 
						|
            ) from e
 | 
						|
        return value
 | 
						|
 | 
						|
    certfile = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help=_i18n("""The full path to an SSL/TLS certificate file."""),
 | 
						|
    )
 | 
						|
 | 
						|
    keyfile = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help=_i18n("""The full path to a private key file for usage with SSL/TLS."""),
 | 
						|
    )
 | 
						|
 | 
						|
    client_ca = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """The full path to a certificate authority certificate for SSL/TLS client authentication."""
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    cookie_secret_file = Unicode(
 | 
						|
        config=True, help=_i18n("""The file where the cookie secret is stored.""")
 | 
						|
    )
 | 
						|
 | 
						|
    @default("cookie_secret_file")
 | 
						|
    def _default_cookie_secret_file(self) -> str:
 | 
						|
        return os.path.join(self.runtime_dir, "jupyter_cookie_secret")
 | 
						|
 | 
						|
    cookie_secret = Bytes(
 | 
						|
        b"",
 | 
						|
        config=True,
 | 
						|
        help="""The random bytes used to secure cookies.
 | 
						|
        By default this is generated on first start of the server and persisted across server
 | 
						|
        sessions by writing the cookie secret into the `cookie_secret_file` file.
 | 
						|
        When using an executable config file you can override this to be random at each server restart.
 | 
						|
 | 
						|
        Note: Cookie secrets should be kept private, do not share config files with
 | 
						|
        cookie_secret stored in plaintext (you can read the value from a file).
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    @default("cookie_secret")
 | 
						|
    def _default_cookie_secret(self) -> bytes:
 | 
						|
        if os.path.exists(self.cookie_secret_file):
 | 
						|
            with open(self.cookie_secret_file, "rb") as f:
 | 
						|
                key = f.read()
 | 
						|
        else:
 | 
						|
            key = encodebytes(os.urandom(32))
 | 
						|
            self._write_cookie_secret_file(key)
 | 
						|
        h = hmac.new(key, digestmod=hashlib.sha256)
 | 
						|
        h.update(self.password.encode())
 | 
						|
        return h.digest()
 | 
						|
 | 
						|
    def _write_cookie_secret_file(self, secret: bytes) -> None:
 | 
						|
        """write my secret to my secret_file"""
 | 
						|
        self.log.info(_i18n("Writing Jupyter server cookie secret to %s"), self.cookie_secret_file)
 | 
						|
        try:
 | 
						|
            with secure_write(self.cookie_secret_file, True) as f:
 | 
						|
                f.write(secret)
 | 
						|
        except OSError as e:
 | 
						|
            self.log.error(
 | 
						|
                _i18n("Failed to write cookie secret to %s: %s"),
 | 
						|
                self.cookie_secret_file,
 | 
						|
                e,
 | 
						|
            )
 | 
						|
 | 
						|
    _token_set = False
 | 
						|
 | 
						|
    token = Unicode("<DEPRECATED>", help=_i18n("""DEPRECATED. Use IdentityProvider.token""")).tag(
 | 
						|
        config=True
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("token")
 | 
						|
    def _deprecated_token(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "IdentityProvider")
 | 
						|
 | 
						|
    @default("token")
 | 
						|
    def _deprecated_token_access(self) -> str:
 | 
						|
        warnings.warn(
 | 
						|
            "ServerApp.token config is deprecated in jupyter-server 2.0. Use IdentityProvider.token",
 | 
						|
            DeprecationWarning,
 | 
						|
            stacklevel=3,
 | 
						|
        )
 | 
						|
        return self.identity_provider.token
 | 
						|
 | 
						|
    min_open_files_limit = Integer(
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
        Gets or sets a lower bound on the open file handles process resource
 | 
						|
        limit. This may need to be increased if you run into an
 | 
						|
        OSError: [Errno 24] Too many open files.
 | 
						|
        This is not applicable when running on Windows.
 | 
						|
        """,
 | 
						|
        allow_none=True,
 | 
						|
    )
 | 
						|
 | 
						|
    @default("min_open_files_limit")
 | 
						|
    def _default_min_open_files_limit(self) -> t.Optional[int]:
 | 
						|
        if resource is None:
 | 
						|
            # Ignoring min_open_files_limit because the limit cannot be adjusted (for example, on Windows)
 | 
						|
            return None  # type:ignore[unreachable]
 | 
						|
 | 
						|
        soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
 | 
						|
 | 
						|
        default_soft = 4096
 | 
						|
        if hard >= default_soft:
 | 
						|
            return default_soft
 | 
						|
 | 
						|
        self.log.debug(
 | 
						|
            "Default value for min_open_files_limit is ignored (hard=%r, soft=%r)",
 | 
						|
            hard,
 | 
						|
            soft,
 | 
						|
        )
 | 
						|
 | 
						|
        return soft
 | 
						|
 | 
						|
    max_body_size = Integer(
 | 
						|
        512 * 1024 * 1024,
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
        Sets the maximum allowed size of the client request body, specified in
 | 
						|
        the Content-Length request header field. If the size in a request
 | 
						|
        exceeds the configured value, a malformed HTTP message is returned to
 | 
						|
        the client.
 | 
						|
 | 
						|
        Note: max_body_size is applied even in streaming mode.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    max_buffer_size = Integer(
 | 
						|
        512 * 1024 * 1024,
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
        Gets or sets the maximum amount of memory, in bytes, that is allocated
 | 
						|
        for use by the buffer manager.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    password = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help="""DEPRECATED in 2.0. Use PasswordIdentityProvider.hashed_password""",
 | 
						|
    )
 | 
						|
 | 
						|
    password_required = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help="""DEPRECATED in 2.0. Use PasswordIdentityProvider.password_required""",
 | 
						|
    )
 | 
						|
 | 
						|
    allow_password_change = Bool(
 | 
						|
        True,
 | 
						|
        config=True,
 | 
						|
        help="""DEPRECATED in 2.0. Use PasswordIdentityProvider.allow_password_change""",
 | 
						|
    )
 | 
						|
 | 
						|
    def _warn_deprecated_config(
 | 
						|
        self, change: t.Any, clsname: str, new_name: t.Optional[str] = None
 | 
						|
    ) -> None:
 | 
						|
        """Warn on deprecated config."""
 | 
						|
        if new_name is None:
 | 
						|
            new_name = change.name
 | 
						|
        if clsname not in self.config or new_name not in self.config[clsname]:
 | 
						|
            # Deprecated config used, new config not used.
 | 
						|
            # Use deprecated config, warn about new name.
 | 
						|
            self.log.warning(
 | 
						|
                f"ServerApp.{change.name} config is deprecated in 2.0. Use {clsname}.{new_name}."
 | 
						|
            )
 | 
						|
            self.config[clsname][new_name] = change.new
 | 
						|
        # Deprecated config used, new config also used.
 | 
						|
        # Warn only if the values differ.
 | 
						|
        # If the values are the same, assume intentional backward-compatible config.
 | 
						|
        elif self.config[clsname][new_name] != change.new:
 | 
						|
            self.log.warning(
 | 
						|
                f"Ignoring deprecated ServerApp.{change.name} config. Using {clsname}.{new_name}."
 | 
						|
            )
 | 
						|
 | 
						|
    @observe("password")
 | 
						|
    def _deprecated_password(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "PasswordIdentityProvider", new_name="hashed_password")
 | 
						|
 | 
						|
    @observe("password_required", "allow_password_change")
 | 
						|
    def _deprecated_password_config(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "PasswordIdentityProvider")
 | 
						|
 | 
						|
    disable_check_xsrf = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help="""Disable cross-site-request-forgery protection
 | 
						|
 | 
						|
        Jupyter server includes protection from cross-site request forgeries,
 | 
						|
        requiring API requests to either:
 | 
						|
 | 
						|
        - originate from pages served by this server (validated with XSRF cookie and token), or
 | 
						|
        - authenticate with a token
 | 
						|
 | 
						|
        Some anonymous compute resources still desire the ability to run code,
 | 
						|
        completely without authentication.
 | 
						|
        These services can disable all authentication and security checks,
 | 
						|
        with the full knowledge of what that implies.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    _allow_unauthenticated_access_env = "JUPYTER_SERVER_ALLOW_UNAUTHENTICATED_ACCESS"
 | 
						|
 | 
						|
    allow_unauthenticated_access = Bool(
 | 
						|
        True,
 | 
						|
        config=True,
 | 
						|
        help=f"""Allow unauthenticated access to endpoints without authentication rule.
 | 
						|
 | 
						|
        When set to `True` (default in jupyter-server 2.0, subject to change
 | 
						|
        in the future), any request to an endpoint without an authentication rule
 | 
						|
        (either `@tornado.web.authenticated`, or `@allow_unauthenticated`)
 | 
						|
        will be permitted, regardless of whether user has logged in or not.
 | 
						|
 | 
						|
        When set to `False`, logging in will be required for access to each endpoint,
 | 
						|
        excluding the endpoints marked with `@allow_unauthenticated` decorator.
 | 
						|
 | 
						|
        This option can be configured using `{_allow_unauthenticated_access_env}`
 | 
						|
        environment variable: any non-empty value other than "true" and "yes" will
 | 
						|
        prevent unauthenticated access to endpoints without `@allow_unauthenticated`.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    @default("allow_unauthenticated_access")
 | 
						|
    def _allow_unauthenticated_access_default(self):
 | 
						|
        if os.getenv(self._allow_unauthenticated_access_env):
 | 
						|
            return os.environ[self._allow_unauthenticated_access_env].lower() in ["true", "yes"]
 | 
						|
        return True
 | 
						|
 | 
						|
    allow_remote_access = Bool(
 | 
						|
        config=True,
 | 
						|
        help="""Allow requests where the Host header doesn't point to a local server
 | 
						|
 | 
						|
       By default, requests get a 403 forbidden response if the 'Host' header
 | 
						|
       shows that the browser thinks it's on a non-local domain.
 | 
						|
       Setting this option to True disables this check.
 | 
						|
 | 
						|
       This protects against 'DNS rebinding' attacks, where a remote web server
 | 
						|
       serves you a page and then changes its DNS to send later requests to a
 | 
						|
       local IP, bypassing same-origin checks.
 | 
						|
 | 
						|
       Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local,
 | 
						|
       along with hostnames configured in local_hostnames.
 | 
						|
       """,
 | 
						|
    )
 | 
						|
 | 
						|
    @default("allow_remote_access")
 | 
						|
    def _default_allow_remote(self) -> bool:
 | 
						|
        """Disallow remote access if we're listening only on loopback addresses"""
 | 
						|
 | 
						|
        # if blank, self.ip was configured to "*" meaning bind to all interfaces,
 | 
						|
        # see _valdate_ip
 | 
						|
        if self.ip == "":
 | 
						|
            return True
 | 
						|
 | 
						|
        try:
 | 
						|
            addr = ipaddress.ip_address(self.ip)
 | 
						|
        except ValueError:
 | 
						|
            # Address is a hostname
 | 
						|
            for info in socket.getaddrinfo(self.ip, self.port, 0, socket.SOCK_STREAM):
 | 
						|
                addr = info[4][0]  # type:ignore[assignment]
 | 
						|
 | 
						|
                try:
 | 
						|
                    parsed = ipaddress.ip_address(addr.split("%")[0])  # type:ignore[union-attr]
 | 
						|
                except ValueError:
 | 
						|
                    self.log.warning("Unrecognised IP address: %r", addr)
 | 
						|
                    continue
 | 
						|
 | 
						|
                # Macs map localhost to 'fe80::1%lo0', a link local address
 | 
						|
                # scoped to the loopback interface. For now, we'll assume that
 | 
						|
                # any scoped link-local address is effectively local.
 | 
						|
                if not (
 | 
						|
                    parsed.is_loopback or (("%" in addr) and parsed.is_link_local)  # type:ignore[operator]
 | 
						|
                ):
 | 
						|
                    return True
 | 
						|
            return False
 | 
						|
        else:
 | 
						|
            return not addr.is_loopback
 | 
						|
 | 
						|
    use_redirect_file = Bool(
 | 
						|
        True,
 | 
						|
        config=True,
 | 
						|
        help="""Disable launching browser by redirect file
 | 
						|
     For versions of notebook > 5.7.2, a security feature measure was added that
 | 
						|
     prevented the authentication token used to launch the browser from being visible.
 | 
						|
     This feature makes it difficult for other users on a multi-user system from
 | 
						|
     running code in your Jupyter session as you.
 | 
						|
     However, some environments (like Windows Subsystem for Linux (WSL) and Chromebooks),
 | 
						|
     launching a browser using a redirect file can lead the browser failing to load.
 | 
						|
     This is because of the difference in file structures/paths between the runtime and
 | 
						|
     the browser.
 | 
						|
 | 
						|
     Disabling this setting to False will disable this behavior, allowing the browser
 | 
						|
     to launch by using a URL and visible token (as before).
 | 
						|
     """,
 | 
						|
    )
 | 
						|
 | 
						|
    local_hostnames = List(
 | 
						|
        Unicode(),
 | 
						|
        ["localhost"],
 | 
						|
        config=True,
 | 
						|
        help="""Hostnames to allow as local when allow_remote_access is False.
 | 
						|
 | 
						|
       Local IP addresses (such as 127.0.0.1 and ::1) are automatically accepted
 | 
						|
       as local as well.
 | 
						|
       """,
 | 
						|
    )
 | 
						|
 | 
						|
    open_browser = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help="""Whether to open in a browser after starting.
 | 
						|
                        The specific browser used is platform dependent and
 | 
						|
                        determined by the python standard library `webbrowser`
 | 
						|
                        module, unless it is overridden using the --browser
 | 
						|
                        (ServerApp.browser) configuration option.
 | 
						|
                        """,
 | 
						|
    )
 | 
						|
 | 
						|
    browser = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help="""Specify what command to use to invoke a web
 | 
						|
                      browser when starting the server. If not specified, the
 | 
						|
                      default browser will be determined by the `webbrowser`
 | 
						|
                      standard library module, which allows setting of the
 | 
						|
                      BROWSER environment variable to override it.
 | 
						|
                      """,
 | 
						|
    )
 | 
						|
 | 
						|
    webbrowser_open_new = Integer(
 | 
						|
        2,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """Specify where to open the server on startup. This is the
 | 
						|
        `new` argument passed to the standard library method `webbrowser.open`.
 | 
						|
        The behaviour is not guaranteed, but depends on browser support. Valid
 | 
						|
        values are:
 | 
						|
 | 
						|
         - 2 opens a new tab,
 | 
						|
         - 1 opens a new window,
 | 
						|
         - 0 opens in an existing window.
 | 
						|
 | 
						|
        See the `webbrowser.open` documentation for details.
 | 
						|
        """
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    tornado_settings = Dict(
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "Supply overrides for the tornado.web.Application that the Jupyter server uses."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    websocket_compression_options = Any(
 | 
						|
        None,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """
 | 
						|
        Set the tornado compression options for websocket connections.
 | 
						|
 | 
						|
        This value will be returned from :meth:`WebSocketHandler.get_compression_options`.
 | 
						|
        None (default) will disable compression.
 | 
						|
        A dict (even an empty one) will enable compression.
 | 
						|
 | 
						|
        See the tornado docs for WebSocketHandler.get_compression_options for details.
 | 
						|
        """
 | 
						|
        ),
 | 
						|
    )
 | 
						|
    terminado_settings = Dict(
 | 
						|
        Union([List(), Unicode()]),
 | 
						|
        config=True,
 | 
						|
        help=_i18n('Supply overrides for terminado. Currently only supports "shell_command".'),
 | 
						|
    )
 | 
						|
 | 
						|
    cookie_options = Dict(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use IdentityProvider.cookie_options"),
 | 
						|
    )
 | 
						|
    get_secure_cookie_kwargs = Dict(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use IdentityProvider.get_secure_cookie_kwargs"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("cookie_options", "get_secure_cookie_kwargs")
 | 
						|
    def _deprecated_cookie_config(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "IdentityProvider")
 | 
						|
 | 
						|
    ssl_options = Dict(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """Supply SSL options for the tornado HTTPServer.
 | 
						|
            See the tornado docs for details."""
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    jinja_environment_options = Dict(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Supply extra arguments that will be passed to Jinja environment."),
 | 
						|
    )
 | 
						|
 | 
						|
    jinja_template_vars = Dict(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Extra variables to supply to jinja templates when rendering."),
 | 
						|
    )
 | 
						|
 | 
						|
    base_url = Unicode(
 | 
						|
        "/",
 | 
						|
        config=True,
 | 
						|
        help="""The base URL for the Jupyter server.
 | 
						|
 | 
						|
                       Leading and trailing slashes can be omitted,
 | 
						|
                       and will automatically be added.
 | 
						|
                       """,
 | 
						|
    )
 | 
						|
 | 
						|
    @validate("base_url")
 | 
						|
    def _update_base_url(self, proposal: t.Any) -> str:
 | 
						|
        value = t.cast(str, proposal["value"])
 | 
						|
        if not value.startswith("/"):
 | 
						|
            value = "/" + value
 | 
						|
        if not value.endswith("/"):
 | 
						|
            value = value + "/"
 | 
						|
        return value
 | 
						|
 | 
						|
    extra_static_paths = List(
 | 
						|
        Unicode(),
 | 
						|
        config=True,
 | 
						|
        help="""Extra paths to search for serving static files.
 | 
						|
 | 
						|
        This allows adding javascript/css to be available from the Jupyter server machine,
 | 
						|
        or overriding individual files in the IPython""",
 | 
						|
    )
 | 
						|
 | 
						|
    @property
 | 
						|
    def static_file_path(self) -> list[str]:
 | 
						|
        """return extra paths + the default location"""
 | 
						|
        return [*self.extra_static_paths, DEFAULT_STATIC_FILES_PATH]
 | 
						|
 | 
						|
    static_custom_path = List(Unicode(), help=_i18n("""Path to search for custom.js, css"""))
 | 
						|
 | 
						|
    @default("static_custom_path")
 | 
						|
    def _default_static_custom_path(self) -> list[str]:
 | 
						|
        return [os.path.join(d, "custom") for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH)]
 | 
						|
 | 
						|
    extra_template_paths = List(
 | 
						|
        Unicode(),
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """Extra paths to search for serving jinja templates.
 | 
						|
 | 
						|
        Can be used to override templates from jupyter_server.templates."""
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    @property
 | 
						|
    def template_file_path(self) -> list[str]:
 | 
						|
        """return extra paths + the default locations"""
 | 
						|
        return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
 | 
						|
 | 
						|
    extra_services = List(
 | 
						|
        Unicode(),
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """handlers that should be loaded at higher priority than the default services"""
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    websocket_url = Unicode(
 | 
						|
        "",
 | 
						|
        config=True,
 | 
						|
        help="""The base URL for websockets,
 | 
						|
        if it differs from the HTTP server (hint: it almost certainly doesn't).
 | 
						|
 | 
						|
        Should be in the form of an HTTP origin: ws[s]://hostname[:port]
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    quit_button = Bool(
 | 
						|
        True,
 | 
						|
        config=True,
 | 
						|
        help="""If True, display controls to shut down the Jupyter server, such as menu items or buttons.""",
 | 
						|
    )
 | 
						|
 | 
						|
    contents_manager_class = Type(
 | 
						|
        default_value=AsyncLargeFileManager,
 | 
						|
        klass=ContentsManager,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The content manager class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    kernel_manager_class = Type(
 | 
						|
        klass=MappingKernelManager,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The kernel manager class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("kernel_manager_class")
 | 
						|
    def _default_kernel_manager_class(self) -> t.Union[str, type[AsyncMappingKernelManager]]:
 | 
						|
        if self.gateway_config.gateway_enabled:
 | 
						|
            return "jupyter_server.gateway.managers.GatewayMappingKernelManager"
 | 
						|
        return AsyncMappingKernelManager
 | 
						|
 | 
						|
    session_manager_class = Type(
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The session manager class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("session_manager_class")
 | 
						|
    def _default_session_manager_class(self) -> t.Union[str, type[SessionManager]]:
 | 
						|
        if self.gateway_config.gateway_enabled:
 | 
						|
            return "jupyter_server.gateway.managers.GatewaySessionManager"
 | 
						|
        return SessionManager
 | 
						|
 | 
						|
    kernel_websocket_connection_class = Type(
 | 
						|
        klass=BaseKernelWebsocketConnection,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The kernel websocket connection class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("kernel_websocket_connection_class")
 | 
						|
    def _default_kernel_websocket_connection_class(
 | 
						|
        self,
 | 
						|
    ) -> t.Union[str, type[ZMQChannelsWebsocketConnection]]:
 | 
						|
        if self.gateway_config.gateway_enabled:
 | 
						|
            return "jupyter_server.gateway.connections.GatewayWebSocketConnection"
 | 
						|
        return ZMQChannelsWebsocketConnection
 | 
						|
 | 
						|
    websocket_ping_interval = Integer(
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
            Configure the websocket ping interval in seconds.
 | 
						|
 | 
						|
            Websockets are long-lived connections that are used by some Jupyter
 | 
						|
            Server extensions.
 | 
						|
 | 
						|
            Periodic pings help to detect disconnected clients and keep the
 | 
						|
            connection active. If this is set to None, then no pings will be
 | 
						|
            performed.
 | 
						|
 | 
						|
            When a ping is sent, the client has ``websocket_ping_timeout``
 | 
						|
            seconds to respond. If no response is received within this period,
 | 
						|
            the connection will be closed from the server side.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
    websocket_ping_timeout = Integer(
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
            Configure the websocket ping timeout in seconds.
 | 
						|
 | 
						|
            See ``websocket_ping_interval`` for details.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    config_manager_class = Type(
 | 
						|
        default_value=ConfigManager,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The config manager class to use"),
 | 
						|
    )
 | 
						|
 | 
						|
    kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
 | 
						|
 | 
						|
    kernel_spec_manager_class = Type(
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
        The kernel spec manager class to use. Should be a subclass
 | 
						|
        of `jupyter_client.kernelspec.KernelSpecManager`.
 | 
						|
 | 
						|
        The Api of KernelSpecManager is provisional and might change
 | 
						|
        without warning between this version of Jupyter and the next stable one.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    @default("kernel_spec_manager_class")
 | 
						|
    def _default_kernel_spec_manager_class(self) -> t.Union[str, type[KernelSpecManager]]:
 | 
						|
        if self.gateway_config.gateway_enabled:
 | 
						|
            return "jupyter_server.gateway.managers.GatewayKernelSpecManager"
 | 
						|
        return KernelSpecManager
 | 
						|
 | 
						|
    login_handler_class = Type(
 | 
						|
        default_value=LoginHandler,
 | 
						|
        klass=web.RequestHandler,
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The login handler class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    logout_handler_class = Type(
 | 
						|
        default_value=LogoutHandler,
 | 
						|
        klass=web.RequestHandler,
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The logout handler class to use."),
 | 
						|
    )
 | 
						|
    # TODO: detect deprecated login handler config
 | 
						|
 | 
						|
    authorizer_class = Type(
 | 
						|
        default_value=AllowAllAuthorizer,
 | 
						|
        klass=Authorizer,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The authorizer class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    identity_provider_class = Type(
 | 
						|
        default_value=PasswordIdentityProvider,
 | 
						|
        klass=IdentityProvider,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("The identity provider class to use."),
 | 
						|
    )
 | 
						|
 | 
						|
    trust_xheaders = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=(
 | 
						|
            _i18n(
 | 
						|
                "Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
 | 
						|
                "sent by the upstream reverse proxy. Necessary if the proxy handles SSL"
 | 
						|
            )
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    event_logger = Instance(
 | 
						|
        EventLogger,
 | 
						|
        allow_none=True,
 | 
						|
        help="An EventLogger for emitting structured event data from Jupyter Server and extensions.",
 | 
						|
    )
 | 
						|
 | 
						|
    info_file = Unicode()
 | 
						|
 | 
						|
    @default("info_file")
 | 
						|
    def _default_info_file(self) -> str:
 | 
						|
        info_file = "jpserver-%s.json" % os.getpid()
 | 
						|
        return os.path.join(self.runtime_dir, info_file)
 | 
						|
 | 
						|
    no_browser_open_file = Bool(
 | 
						|
        False, help="If True, do not write redirect HTML file disk, or show in messages."
 | 
						|
    )
 | 
						|
 | 
						|
    browser_open_file = Unicode()
 | 
						|
 | 
						|
    @default("browser_open_file")
 | 
						|
    def _default_browser_open_file(self) -> str:
 | 
						|
        basename = "jpserver-%s-open.html" % os.getpid()
 | 
						|
        return os.path.join(self.runtime_dir, basename)
 | 
						|
 | 
						|
    browser_open_file_to_run = Unicode()
 | 
						|
 | 
						|
    @default("browser_open_file_to_run")
 | 
						|
    def _default_browser_open_file_to_run(self) -> str:
 | 
						|
        basename = "jpserver-file-to-run-%s-open.html" % os.getpid()
 | 
						|
        return os.path.join(self.runtime_dir, basename)
 | 
						|
 | 
						|
    pylab = Unicode(
 | 
						|
        "disabled",
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """
 | 
						|
        DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
 | 
						|
        """
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("pylab")
 | 
						|
    def _update_pylab(self, change: t.Any) -> None:
 | 
						|
        """when --pylab is specified, display a warning and exit"""
 | 
						|
        backend = " %s" % change["new"] if change["new"] != "warn" else ""
 | 
						|
        self.log.error(
 | 
						|
            _i18n("Support for specifying --pylab on the command line has been removed.")
 | 
						|
        )
 | 
						|
        self.log.error(
 | 
						|
            _i18n("Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.").format(
 | 
						|
                backend
 | 
						|
            )
 | 
						|
        )
 | 
						|
        self.exit(1)
 | 
						|
 | 
						|
    notebook_dir = Unicode(config=True, help=_i18n("DEPRECATED, use root_dir."))
 | 
						|
 | 
						|
    @observe("notebook_dir")
 | 
						|
    def _update_notebook_dir(self, change: t.Any) -> None:
 | 
						|
        if self._root_dir_set:
 | 
						|
            # only use deprecated config if new config is not set
 | 
						|
            return
 | 
						|
        self.log.warning(_i18n("notebook_dir is deprecated, use root_dir"))
 | 
						|
        self.root_dir = change["new"]
 | 
						|
 | 
						|
    external_connection_dir = Unicode(
 | 
						|
        None,
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "The directory to look at for external kernel connection files, if allow_external_kernels is True. "
 | 
						|
            "Defaults to Jupyter runtime_dir/external_kernels. "
 | 
						|
            "Make sure that this directory is not filled with left-over connection files, "
 | 
						|
            "that could result in unnecessary kernel manager creations."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    allow_external_kernels = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            "Whether or not to allow external kernels, whose connection files are placed in external_connection_dir."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    root_dir = Unicode(config=True, help=_i18n("The directory to use for notebooks and kernels."))
 | 
						|
    _root_dir_set = False
 | 
						|
 | 
						|
    @default("root_dir")
 | 
						|
    def _default_root_dir(self) -> str:
 | 
						|
        if self.file_to_run:
 | 
						|
            self._root_dir_set = True
 | 
						|
            return os.path.dirname(os.path.abspath(self.file_to_run))
 | 
						|
        else:
 | 
						|
            return os.getcwd()
 | 
						|
 | 
						|
    def _normalize_dir(self, value: str) -> str:
 | 
						|
        """Normalize a directory."""
 | 
						|
        # Strip any trailing slashes
 | 
						|
        # *except* if it's root
 | 
						|
        _, path = os.path.splitdrive(value)
 | 
						|
        if path == os.sep:
 | 
						|
            return value
 | 
						|
        value = value.rstrip(os.sep)
 | 
						|
        if not os.path.isabs(value):
 | 
						|
            # If we receive a non-absolute path, make it absolute.
 | 
						|
            value = os.path.abspath(value)
 | 
						|
        return value
 | 
						|
 | 
						|
    @validate("root_dir")
 | 
						|
    def _root_dir_validate(self, proposal: t.Any) -> str:
 | 
						|
        value = self._normalize_dir(proposal["value"])
 | 
						|
        if not os.path.isdir(value):
 | 
						|
            raise TraitError(trans.gettext("No such directory: '%r'") % value)
 | 
						|
        return value
 | 
						|
 | 
						|
    @observe("root_dir")
 | 
						|
    def _root_dir_changed(self, change: t.Any) -> None:
 | 
						|
        # record that root_dir is set,
 | 
						|
        # which affects loading of deprecated notebook_dir
 | 
						|
        self._root_dir_set = True
 | 
						|
 | 
						|
    preferred_dir = Unicode(
 | 
						|
        config=True,
 | 
						|
        help=trans.gettext(
 | 
						|
            "Preferred starting directory to use for notebooks and kernels. ServerApp.preferred_dir is deprecated in jupyter-server 2.0. Use FileContentsManager.preferred_dir instead"
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("preferred_dir")
 | 
						|
    def _default_prefered_dir(self) -> str:
 | 
						|
        return self.root_dir
 | 
						|
 | 
						|
    @validate("preferred_dir")
 | 
						|
    def _preferred_dir_validate(self, proposal: t.Any) -> str:
 | 
						|
        value = self._normalize_dir(proposal["value"])
 | 
						|
        if not os.path.isdir(value):
 | 
						|
            raise TraitError(trans.gettext("No such preferred dir: '%r'") % value)
 | 
						|
        return value
 | 
						|
 | 
						|
    @observe("server_extensions")
 | 
						|
    def _update_server_extensions(self, change: t.Any) -> None:
 | 
						|
        self.log.warning(_i18n("server_extensions is deprecated, use jpserver_extensions"))
 | 
						|
        self.server_extensions = change["new"]
 | 
						|
 | 
						|
    jpserver_extensions = Dict(
 | 
						|
        default_value={},
 | 
						|
        value_trait=Bool(),
 | 
						|
        config=True,
 | 
						|
        help=(
 | 
						|
            _i18n(
 | 
						|
                "Dict of Python modules to load as Jupyter server extensions."
 | 
						|
                "Entry values can be used to enable and disable the loading of"
 | 
						|
                "the extensions. The extensions will be loaded in alphabetical "
 | 
						|
                "order."
 | 
						|
            )
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    reraise_server_extension_failures = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("Reraise exceptions encountered loading server extensions?"),
 | 
						|
    )
 | 
						|
 | 
						|
    kernel_ws_protocol = Unicode(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use ZMQChannelsWebsocketConnection.kernel_ws_protocol"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("kernel_ws_protocol")
 | 
						|
    def _deprecated_kernel_ws_protocol(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "ZMQChannelsWebsocketConnection")
 | 
						|
 | 
						|
    limit_rate = Bool(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use ZMQChannelsWebsocketConnection.limit_rate"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("limit_rate")
 | 
						|
    def _deprecated_limit_rate(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "ZMQChannelsWebsocketConnection")
 | 
						|
 | 
						|
    iopub_msg_rate_limit = Float(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use ZMQChannelsWebsocketConnection.iopub_msg_rate_limit"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("iopub_msg_rate_limit")
 | 
						|
    def _deprecated_iopub_msg_rate_limit(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "ZMQChannelsWebsocketConnection")
 | 
						|
 | 
						|
    iopub_data_rate_limit = Float(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use ZMQChannelsWebsocketConnection.iopub_data_rate_limit"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("iopub_data_rate_limit")
 | 
						|
    def _deprecated_iopub_data_rate_limit(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "ZMQChannelsWebsocketConnection")
 | 
						|
 | 
						|
    rate_limit_window = Float(
 | 
						|
        allow_none=True,
 | 
						|
        config=True,
 | 
						|
        help=_i18n("DEPRECATED. Use ZMQChannelsWebsocketConnection.rate_limit_window"),
 | 
						|
    )
 | 
						|
 | 
						|
    @observe("rate_limit_window")
 | 
						|
    def _deprecated_rate_limit_window(self, change: t.Any) -> None:
 | 
						|
        self._warn_deprecated_config(change, "ZMQChannelsWebsocketConnection")
 | 
						|
 | 
						|
    shutdown_no_activity_timeout = Integer(
 | 
						|
        0,
 | 
						|
        config=True,
 | 
						|
        help=(
 | 
						|
            "Shut down the server after N seconds with no kernels"
 | 
						|
            "running and no activity. "
 | 
						|
            "This can be used together with culling idle kernels "
 | 
						|
            "(MappingKernelManager.cull_idle_timeout) to "
 | 
						|
            "shutdown the Jupyter server when it's not in use. This is not "
 | 
						|
            "precisely timed: it may shut down up to a minute later. "
 | 
						|
            "0 (the default) disables this automatic shutdown."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    terminals_enabled = Bool(
 | 
						|
        config=True,
 | 
						|
        help=_i18n(
 | 
						|
            """Set to False to disable terminals.
 | 
						|
 | 
						|
         This does *not* make the server more secure by itself.
 | 
						|
         Anything the user can in a terminal, they can also do in a notebook.
 | 
						|
 | 
						|
         Terminals may also be automatically disabled if the terminado package
 | 
						|
         is not available.
 | 
						|
         """
 | 
						|
        ),
 | 
						|
    )
 | 
						|
 | 
						|
    @default("terminals_enabled")
 | 
						|
    def _default_terminals_enabled(self) -> bool:
 | 
						|
        return True
 | 
						|
 | 
						|
    authenticate_prometheus = Bool(
 | 
						|
        True,
 | 
						|
        help=""""
 | 
						|
        Require authentication to access prometheus metrics.
 | 
						|
        """,
 | 
						|
        config=True,
 | 
						|
    )
 | 
						|
 | 
						|
    record_http_request_metrics = Bool(
 | 
						|
        True,
 | 
						|
        help="""
 | 
						|
        Record http_request_duration_seconds metric in the metrics endpoint.
 | 
						|
 | 
						|
        Since a histogram is exposed for each request handler, this can create a
 | 
						|
        *lot* of metrics, creating operational challenges for multitenant deployments.
 | 
						|
 | 
						|
        Set to False to disable recording the http_request_duration_seconds metric.
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    extra_log_scrub_param_keys = List(
 | 
						|
        Unicode(),
 | 
						|
        default_value=[],
 | 
						|
        config=True,
 | 
						|
        help="""
 | 
						|
        Additional URL parameter keys to scrub from logs.
 | 
						|
 | 
						|
        These will be added to the default list of scrubbed parameter keys.
 | 
						|
        Any URL parameter whose key contains one of these substrings will have
 | 
						|
        its value replaced with '[secret]' in the logs. This is to prevent
 | 
						|
        sensitive information like authentication tokens from being leaked
 | 
						|
        in log files.
 | 
						|
 | 
						|
        Default scrubbed keys: ["token", "auth", "key", "code", "state", "xsrf"]
 | 
						|
        """,
 | 
						|
    )
 | 
						|
 | 
						|
    static_immutable_cache = List(
 | 
						|
        Unicode(),
 | 
						|
        help="""
 | 
						|
        Paths to set up static files as immutable.
 | 
						|
 | 
						|
        This allow setting up the cache control of static files as immutable.
 | 
						|
        It should be used for static file named with a hash for instance.
 | 
						|
        """,
 | 
						|
        config=True,
 | 
						|
    )
 | 
						|
 | 
						|
    _starter_app = Instance(
 | 
						|
        default_value=None,
 | 
						|
        allow_none=True,
 | 
						|
        klass="jupyter_server.extension.application.ExtensionApp",
 | 
						|
    )
 | 
						|
 | 
						|
    @property
 | 
						|
    def starter_app(self) -> t.Any:
 | 
						|
        """Get the Extension that started this server."""
 | 
						|
        return self._starter_app
 | 
						|
 | 
						|
    def parse_command_line(self, argv: t.Optional[list[str]] = None) -> None:
 | 
						|
        """Parse the command line options."""
 | 
						|
        super().parse_command_line(argv)
 | 
						|
 | 
						|
        if self.extra_args:
 | 
						|
            arg0 = self.extra_args[0]
 | 
						|
            f = os.path.abspath(arg0)
 | 
						|
            self.argv.remove(arg0)
 | 
						|
            if not os.path.exists(f):
 | 
						|
                self.log.critical(_i18n("No such file or directory: %s"), f)
 | 
						|
                self.exit(1)
 | 
						|
 | 
						|
            # Use config here, to ensure that it takes higher priority than
 | 
						|
            # anything that comes from the config dirs.
 | 
						|
            c = Config()
 | 
						|
            if os.path.isdir(f):
 | 
						|
                c.ServerApp.root_dir = f
 | 
						|
            elif os.path.isfile(f):
 | 
						|
                c.ServerApp.file_to_run = f
 | 
						|
            self.update_config(c)
 | 
						|
 | 
						|
    def init_configurables(self) -> None:
 | 
						|
        """Initialize configurables."""
 | 
						|
        # If gateway server is configured, replace appropriate managers to perform redirection.  To make
 | 
						|
        # this determination, instantiate the GatewayClient config singleton.
 | 
						|
        self.gateway_config = GatewayClient.instance(parent=self)
 | 
						|
 | 
						|
        if not issubclass(
 | 
						|
            self.kernel_manager_class,
 | 
						|
            AsyncMappingKernelManager,
 | 
						|
        ):
 | 
						|
            warnings.warn(
 | 
						|
                "The synchronous MappingKernelManager class is deprecated and will not be supported in Jupyter Server 3.0",
 | 
						|
                DeprecationWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
 | 
						|
        if not issubclass(
 | 
						|
            self.contents_manager_class,
 | 
						|
            AsyncContentsManager,
 | 
						|
        ):
 | 
						|
            warnings.warn(
 | 
						|
                "The synchronous ContentsManager classes are deprecated and will not be supported in Jupyter Server 3.0",
 | 
						|
                DeprecationWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
 | 
						|
        self.kernel_spec_manager = self.kernel_spec_manager_class(
 | 
						|
            parent=self,
 | 
						|
        )
 | 
						|
 | 
						|
        kwargs = {
 | 
						|
            "parent": self,
 | 
						|
            "log": self.log,
 | 
						|
            "connection_dir": self.runtime_dir,
 | 
						|
            "kernel_spec_manager": self.kernel_spec_manager,
 | 
						|
        }
 | 
						|
        if jupyter_client.version_info > (8, 3, 0):  # type:ignore[attr-defined]
 | 
						|
            if self.allow_external_kernels:
 | 
						|
                external_connection_dir = self.external_connection_dir
 | 
						|
                if external_connection_dir is None:
 | 
						|
                    external_connection_dir = str(Path(self.runtime_dir) / "external_kernels")
 | 
						|
                kwargs["external_connection_dir"] = external_connection_dir
 | 
						|
        elif self.allow_external_kernels:
 | 
						|
            self.log.warning(
 | 
						|
                "Although allow_external_kernels=True, external kernels are not supported "
 | 
						|
                "because jupyter-client's version does not allow them (should be >8.3.0)."
 | 
						|
            )
 | 
						|
 | 
						|
        self.kernel_manager = self.kernel_manager_class(**kwargs)
 | 
						|
        self.contents_manager = self.contents_manager_class(
 | 
						|
            parent=self,
 | 
						|
            log=self.log,
 | 
						|
        )
 | 
						|
        # Trigger a default/validation here explicitly while we still support the
 | 
						|
        # deprecated trait on ServerApp (FIXME remove when deprecation finalized)
 | 
						|
        self.contents_manager.preferred_dir  # noqa: B018
 | 
						|
        self.session_manager = self.session_manager_class(
 | 
						|
            parent=self,
 | 
						|
            log=self.log,
 | 
						|
            kernel_manager=self.kernel_manager,
 | 
						|
            contents_manager=self.contents_manager,
 | 
						|
        )
 | 
						|
        self.config_manager = self.config_manager_class(
 | 
						|
            parent=self,
 | 
						|
            log=self.log,
 | 
						|
        )
 | 
						|
        identity_provider_kwargs = {"parent": self, "log": self.log}
 | 
						|
 | 
						|
        if (
 | 
						|
            self.login_handler_class is not LoginHandler
 | 
						|
            and self.identity_provider_class is PasswordIdentityProvider
 | 
						|
        ):
 | 
						|
            # default identity provider, non-default LoginHandler
 | 
						|
            # this indicates legacy custom LoginHandler config.
 | 
						|
            # enable LegacyIdentityProvider, which defers to the LoginHandler for pre-2.0 behavior.
 | 
						|
            self.identity_provider_class = LegacyIdentityProvider
 | 
						|
            self.log.warning(
 | 
						|
                f"Customizing authentication via ServerApp.login_handler_class={self.login_handler_class}"
 | 
						|
                " is deprecated in Jupyter Server 2.0."
 | 
						|
                " Use ServerApp.identity_provider_class."
 | 
						|
                " Falling back on legacy authentication.",
 | 
						|
            )
 | 
						|
            identity_provider_kwargs["login_handler_class"] = self.login_handler_class
 | 
						|
            if self.logout_handler_class:
 | 
						|
                identity_provider_kwargs["logout_handler_class"] = self.logout_handler_class
 | 
						|
        elif self.login_handler_class is not LoginHandler:
 | 
						|
            # non-default login handler ignored because also explicitly set identity provider
 | 
						|
            self.log.warning(
 | 
						|
                f"Ignoring deprecated config ServerApp.login_handler_class={self.login_handler_class}."
 | 
						|
                " Superseded by ServerApp.identity_provider_class={self.identity_provider_class}."
 | 
						|
            )
 | 
						|
        self.identity_provider = self.identity_provider_class(**identity_provider_kwargs)
 | 
						|
 | 
						|
        if self.identity_provider_class is LegacyIdentityProvider:
 | 
						|
            # legacy config stored the password in tornado_settings
 | 
						|
            self.tornado_settings["password"] = self.identity_provider.hashed_password  # type:ignore[attr-defined]
 | 
						|
            self.tornado_settings["token"] = self.identity_provider.token
 | 
						|
 | 
						|
        if self._token_set:
 | 
						|
            self.log.warning(
 | 
						|
                "ServerApp.token config is deprecated in jupyter-server 2.0. Use IdentityProvider.token"
 | 
						|
            )
 | 
						|
            if self.identity_provider.token_generated:
 | 
						|
                # default behavior: generated default token
 | 
						|
                # preserve deprecated ServerApp.token config
 | 
						|
                self.identity_provider.token_generated = False
 | 
						|
                self.identity_provider.token = self.token
 | 
						|
            else:
 | 
						|
                # identity_provider didn't generate a default token,
 | 
						|
                # that means it has some config that should take higher priority than deprecated ServerApp.token
 | 
						|
                self.log.warning("Ignoring deprecated ServerApp.token config")
 | 
						|
 | 
						|
        self.authorizer = self.authorizer_class(
 | 
						|
            parent=self, log=self.log, identity_provider=self.identity_provider
 | 
						|
        )
 | 
						|
 | 
						|
    def init_logging(self) -> None:
 | 
						|
        """Initialize logging."""
 | 
						|
        # This prevents double log messages because tornado use a root logger that
 | 
						|
        # self.log is a child of. The logging module dipatches log messages to a log
 | 
						|
        # and all of its ancenstors until propagate is set to False.
 | 
						|
        self.log.propagate = False
 | 
						|
 | 
						|
        for log in app_log, access_log, gen_log:
 | 
						|
            # consistent log output name (ServerApp instead of tornado.access, etc.)
 | 
						|
            log.name = self.log.name
 | 
						|
        # hook up tornado 3's loggers to our app handlers
 | 
						|
        logger = logging.getLogger("tornado")
 | 
						|
        logger.propagate = True
 | 
						|
        logger.parent = self.log
 | 
						|
        logger.setLevel(self.log.level)
 | 
						|
 | 
						|
    def init_event_logger(self) -> None:
 | 
						|
        """Initialize the Event Bus."""
 | 
						|
        self.event_logger = EventLogger(parent=self)
 | 
						|
        # Load the core Jupyter Server event schemas
 | 
						|
        # All event schemas must start with Jupyter Server's
 | 
						|
        # events URI, `JUPYTER_SERVER_EVENTS_URI`.
 | 
						|
        schema_ids = [
 | 
						|
            "https://events.jupyter.org/jupyter_server/contents_service/v1",
 | 
						|
            "https://events.jupyter.org/jupyter_server/gateway_client/v1",
 | 
						|
            "https://events.jupyter.org/jupyter_server/kernel_actions/v1",
 | 
						|
        ]
 | 
						|
        for schema_id in schema_ids:
 | 
						|
            # Get the schema path from the schema ID.
 | 
						|
            rel_schema_path = schema_id.replace(JUPYTER_SERVER_EVENTS_URI + "/", "") + ".yaml"
 | 
						|
            schema_path = DEFAULT_EVENTS_SCHEMA_PATH / rel_schema_path
 | 
						|
            # Use this pathlib object to register the schema
 | 
						|
            self.event_logger.register_event_schema(schema_path)
 | 
						|
 | 
						|
    def init_webapp(self) -> None:
 | 
						|
        """initialize tornado webapp"""
 | 
						|
        self.tornado_settings["allow_origin"] = self.allow_origin
 | 
						|
        self.tornado_settings["websocket_compression_options"] = self.websocket_compression_options
 | 
						|
        if self.allow_origin_pat:
 | 
						|
            self.tornado_settings["allow_origin_pat"] = re.compile(self.allow_origin_pat)
 | 
						|
        self.tornado_settings["allow_credentials"] = self.allow_credentials
 | 
						|
        self.tornado_settings["autoreload"] = self.autoreload
 | 
						|
 | 
						|
        # deprecate accessing these directly, in favor of identity_provider?
 | 
						|
        self.tornado_settings["cookie_options"] = self.identity_provider.cookie_options
 | 
						|
        self.tornado_settings["get_secure_cookie_kwargs"] = (
 | 
						|
            self.identity_provider.get_secure_cookie_kwargs
 | 
						|
        )
 | 
						|
        self.tornado_settings["token"] = self.identity_provider.token
 | 
						|
 | 
						|
        if self.static_immutable_cache:
 | 
						|
            self.tornado_settings["static_immutable_cache"] = self.static_immutable_cache
 | 
						|
 | 
						|
        # ensure default_url starts with base_url
 | 
						|
        if not self.default_url.startswith(self.base_url):
 | 
						|
            self.default_url = url_path_join(self.base_url, self.default_url)
 | 
						|
 | 
						|
        # Socket options validation.
 | 
						|
        if self.sock:
 | 
						|
            if self.port != DEFAULT_JUPYTER_SERVER_PORT:
 | 
						|
                self.log.critical(
 | 
						|
                    ("Options --port and --sock are mutually exclusive. Aborting."),
 | 
						|
                )
 | 
						|
                sys.exit(1)
 | 
						|
            else:
 | 
						|
                # Reset the default port if we're using a UNIX socket.
 | 
						|
                self.port = 0
 | 
						|
 | 
						|
            if self.open_browser:
 | 
						|
                # If we're bound to a UNIX socket, we can't reliably connect from a browser.
 | 
						|
                self.log.info(
 | 
						|
                    ("Ignoring --ServerApp.open_browser due to --sock being used."),
 | 
						|
                )
 | 
						|
 | 
						|
            if self.file_to_run:
 | 
						|
                self.log.critical(
 | 
						|
                    ("Options --ServerApp.file_to_run and --sock are mutually exclusive."),
 | 
						|
                )
 | 
						|
                sys.exit(1)
 | 
						|
 | 
						|
            if sys.platform.startswith("win"):
 | 
						|
                self.log.critical(
 | 
						|
                    (
 | 
						|
                        "Option --sock is not supported on Windows, but got value of %s. Aborting."
 | 
						|
                        % self.sock
 | 
						|
                    ),
 | 
						|
                )
 | 
						|
                sys.exit(1)
 | 
						|
 | 
						|
        self.web_app = ServerWebApplication(
 | 
						|
            self,
 | 
						|
            self.default_services,
 | 
						|
            self.kernel_manager,
 | 
						|
            self.contents_manager,
 | 
						|
            self.session_manager,
 | 
						|
            self.kernel_spec_manager,
 | 
						|
            self.config_manager,
 | 
						|
            self.event_logger,
 | 
						|
            self.extra_services,
 | 
						|
            self.log,
 | 
						|
            self.base_url,
 | 
						|
            self.default_url,
 | 
						|
            self.tornado_settings,
 | 
						|
            self.jinja_environment_options,
 | 
						|
            authorizer=self.authorizer,
 | 
						|
            identity_provider=self.identity_provider,
 | 
						|
            kernel_websocket_connection_class=self.kernel_websocket_connection_class,
 | 
						|
            websocket_ping_interval=self.websocket_ping_interval,
 | 
						|
            websocket_ping_timeout=self.websocket_ping_timeout,
 | 
						|
        )
 | 
						|
        if self.certfile:
 | 
						|
            self.ssl_options["certfile"] = self.certfile
 | 
						|
        if self.keyfile:
 | 
						|
            self.ssl_options["keyfile"] = self.keyfile
 | 
						|
        if self.client_ca:
 | 
						|
            self.ssl_options["ca_certs"] = self.client_ca
 | 
						|
        if not self.ssl_options:
 | 
						|
            # could be an empty dict or None
 | 
						|
            # None indicates no SSL config
 | 
						|
            self.ssl_options = None  # type:ignore[assignment]
 | 
						|
        else:
 | 
						|
            # SSL may be missing, so only import it if it's to be used
 | 
						|
            import ssl
 | 
						|
 | 
						|
            # PROTOCOL_TLS selects the highest ssl/tls protocol version that both the client and
 | 
						|
            # server support. When PROTOCOL_TLS is not available use PROTOCOL_SSLv23.
 | 
						|
            self.ssl_options.setdefault(
 | 
						|
                "ssl_version", getattr(ssl, "PROTOCOL_TLS", ssl.PROTOCOL_SSLv23)
 | 
						|
            )
 | 
						|
            if self.ssl_options.get("ca_certs", False):
 | 
						|
                self.ssl_options.setdefault("cert_reqs", ssl.CERT_REQUIRED)
 | 
						|
 | 
						|
        self.identity_provider.validate_security(self, ssl_options=self.ssl_options)
 | 
						|
 | 
						|
        if isinstance(self.identity_provider, LegacyIdentityProvider):
 | 
						|
            # LegacyIdentityProvider needs access to the tornado settings dict
 | 
						|
            self.identity_provider.settings = self.web_app.settings
 | 
						|
 | 
						|
    def init_resources(self) -> None:
 | 
						|
        """initialize system resources"""
 | 
						|
        if resource is None:
 | 
						|
            self.log.debug(  # type:ignore[unreachable]
 | 
						|
                "Ignoring min_open_files_limit because the limit cannot be adjusted (for example, on Windows)"
 | 
						|
            )
 | 
						|
            return
 | 
						|
 | 
						|
        old_soft, old_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
 | 
						|
        soft = self.min_open_files_limit
 | 
						|
        hard = old_hard
 | 
						|
        if soft is not None and old_soft < soft:
 | 
						|
            if hard < soft:
 | 
						|
                hard = soft
 | 
						|
            self.log.debug(
 | 
						|
                f"Raising open file limit: soft {old_soft}->{soft}; hard {old_hard}->{hard}"
 | 
						|
            )
 | 
						|
            resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
 | 
						|
 | 
						|
    def _get_urlparts(
 | 
						|
        self, path: t.Optional[str] = None, include_token: bool = False
 | 
						|
    ) -> urllib.parse.ParseResult:
 | 
						|
        """Constructs a urllib named tuple, ParseResult,
 | 
						|
        with default values set by server config.
 | 
						|
        The returned tuple can be manipulated using the `_replace` method.
 | 
						|
        """
 | 
						|
        if self.sock:
 | 
						|
            scheme = "http+unix"
 | 
						|
            netloc = urlencode_unix_socket_path(self.sock)
 | 
						|
        else:
 | 
						|
            if not self.ip:
 | 
						|
                ip = "localhost"
 | 
						|
            # Handle nonexplicit hostname.
 | 
						|
            elif self.ip in ("0.0.0.0", "::"):  # noqa: S104
 | 
						|
                ip = "%s" % socket.gethostname()
 | 
						|
            else:
 | 
						|
                ip = f"[{self.ip}]" if ":" in self.ip else self.ip
 | 
						|
            netloc = f"{ip}:{self.port}"
 | 
						|
            scheme = "https" if self.certfile else "http"
 | 
						|
        if not path:
 | 
						|
            path = self.default_url
 | 
						|
        query = None
 | 
						|
        # Don't log full token if it came from config
 | 
						|
        if include_token and self.identity_provider.token:
 | 
						|
            token = (
 | 
						|
                self.identity_provider.token if self.identity_provider.token_generated else "..."
 | 
						|
            )
 | 
						|
            query = urllib.parse.urlencode({"token": token})
 | 
						|
        # Build the URL Parts to dump.
 | 
						|
        urlparts = urllib.parse.ParseResult(
 | 
						|
            scheme=scheme, netloc=netloc, path=path, query=query or "", params="", fragment=""
 | 
						|
        )
 | 
						|
        return urlparts
 | 
						|
 | 
						|
    @property
 | 
						|
    def public_url(self) -> str:
 | 
						|
        parts = self._get_urlparts(include_token=True)
 | 
						|
        # Update with custom pieces.
 | 
						|
        if self.custom_display_url:
 | 
						|
            # Parse custom display_url
 | 
						|
            custom = urllib.parse.urlparse(self.custom_display_url)._asdict()
 | 
						|
            # Get pieces that are matter (non None)
 | 
						|
            custom_updates = {key: item for key, item in custom.items() if item}
 | 
						|
            # Update public URL parts with custom pieces.
 | 
						|
            parts = parts._replace(**custom_updates)
 | 
						|
        return parts.geturl()
 | 
						|
 | 
						|
    @property
 | 
						|
    def local_url(self) -> str:
 | 
						|
        parts = self._get_urlparts(include_token=True)
 | 
						|
        # Update with custom pieces.
 | 
						|
        if not self.sock:
 | 
						|
            localhost = "[::1]" if ":" in self.ip else "127.0.0.1"
 | 
						|
            parts = parts._replace(netloc=f"{localhost}:{self.port}")
 | 
						|
        return parts.geturl()
 | 
						|
 | 
						|
    @property
 | 
						|
    def display_url(self) -> str:
 | 
						|
        """Human readable string with URLs for interacting
 | 
						|
        with the running Jupyter Server
 | 
						|
        """
 | 
						|
        url = self.public_url
 | 
						|
        if self.public_url != self.local_url:
 | 
						|
            url = f"{url}\n    {self.local_url}"
 | 
						|
        return url
 | 
						|
 | 
						|
    @property
 | 
						|
    def connection_url(self) -> str:
 | 
						|
        urlparts = self._get_urlparts(path=self.base_url)
 | 
						|
        return urlparts.geturl()
 | 
						|
 | 
						|
    def init_signal(self) -> None:
 | 
						|
        """Initialize signal handlers."""
 | 
						|
        if (
 | 
						|
            not sys.platform.startswith("win")
 | 
						|
            and sys.stdin  # type:ignore[truthy-bool]
 | 
						|
            and sys.stdin.isatty()
 | 
						|
        ):
 | 
						|
            signal.signal(signal.SIGINT, self._handle_sigint)
 | 
						|
        signal.signal(signal.SIGTERM, self._signal_stop)
 | 
						|
        if hasattr(signal, "SIGUSR1"):
 | 
						|
            # Windows doesn't support SIGUSR1
 | 
						|
            signal.signal(signal.SIGUSR1, self._signal_info)
 | 
						|
        if hasattr(signal, "SIGINFO"):
 | 
						|
            # only on BSD-based systems
 | 
						|
            signal.signal(signal.SIGINFO, self._signal_info)
 | 
						|
 | 
						|
    def _handle_sigint(self, sig: t.Any, frame: t.Any) -> None:
 | 
						|
        """SIGINT handler spawns confirmation dialog
 | 
						|
 | 
						|
        Note:
 | 
						|
            JupyterHub replaces this method with _signal_stop
 | 
						|
            in order to bypass the interactive prompt.
 | 
						|
            https://github.com/jupyterhub/jupyterhub/pull/4864
 | 
						|
 | 
						|
        """
 | 
						|
        # register more forceful signal handler for ^C^C case
 | 
						|
        signal.signal(signal.SIGINT, self._signal_stop)
 | 
						|
        # request confirmation dialog in bg thread, to avoid
 | 
						|
        # blocking the App
 | 
						|
        thread = threading.Thread(target=self._confirm_exit)
 | 
						|
        thread.daemon = True
 | 
						|
        thread.start()
 | 
						|
 | 
						|
    def _restore_sigint_handler(self) -> None:
 | 
						|
        """callback for restoring original SIGINT handler"""
 | 
						|
        signal.signal(signal.SIGINT, self._handle_sigint)
 | 
						|
 | 
						|
    def _confirm_exit(self) -> None:
 | 
						|
        """confirm shutdown on ^C
 | 
						|
 | 
						|
        A second ^C, or answering 'y' within 5s will cause shutdown,
 | 
						|
        otherwise original SIGINT handler will be restored.
 | 
						|
 | 
						|
        This doesn't work on Windows.
 | 
						|
        """
 | 
						|
        info = self.log.info
 | 
						|
        info(_i18n("interrupted"))
 | 
						|
        # Check if answer_yes is set
 | 
						|
        if self.answer_yes:
 | 
						|
            self.log.critical(_i18n("Shutting down..."))
 | 
						|
            # schedule stop on the main thread,
 | 
						|
            # since this might be called from a signal handler
 | 
						|
            self.stop(from_signal=True)
 | 
						|
            return
 | 
						|
        info(self.running_server_info())
 | 
						|
        yes = _i18n("y")
 | 
						|
        no = _i18n("n")
 | 
						|
        sys.stdout.write(_i18n("Shut down this Jupyter server (%s/[%s])? ") % (yes, no))
 | 
						|
        sys.stdout.flush()
 | 
						|
        r, w, x = select.select([sys.stdin], [], [], 5)
 | 
						|
        if r:
 | 
						|
            line = sys.stdin.readline()
 | 
						|
            if line.lower().startswith(yes) and no not in line.lower():
 | 
						|
                self.log.critical(_i18n("Shutdown confirmed"))
 | 
						|
                # schedule stop on the main thread,
 | 
						|
                # since this might be called from a signal handler
 | 
						|
                self.stop(from_signal=True)
 | 
						|
                return
 | 
						|
        else:
 | 
						|
            if self._stopping:
 | 
						|
                # don't show 'no answer' if we're actually stopping,
 | 
						|
                # e.g. ctrl-C ctrl-C
 | 
						|
                return
 | 
						|
            info(_i18n("No answer for 5s:"))
 | 
						|
        info(_i18n("resuming operation..."))
 | 
						|
        # no answer, or answer is no:
 | 
						|
        # set it back to original SIGINT handler
 | 
						|
        # use IOLoop.add_callback because signal.signal must be called
 | 
						|
        # from main thread
 | 
						|
        self.io_loop.add_callback_from_signal(self._restore_sigint_handler)
 | 
						|
 | 
						|
    def _signal_stop(self, sig: t.Any, frame: t.Any) -> None:
 | 
						|
        """Handle a stop signal.
 | 
						|
 | 
						|
        Note:
 | 
						|
            JupyterHub configures this method to be called for SIGINT.
 | 
						|
            https://github.com/jupyterhub/jupyterhub/pull/4864
 | 
						|
 | 
						|
        """
 | 
						|
        self.log.critical(_i18n("received signal %s, stopping"), sig)
 | 
						|
        self.stop(from_signal=True)
 | 
						|
 | 
						|
    def _signal_info(self, sig: t.Any, frame: t.Any) -> None:
 | 
						|
        """Handle an info signal."""
 | 
						|
        self.log.info(self.running_server_info())
 | 
						|
 | 
						|
    def init_components(self) -> None:
 | 
						|
        """Check the components submodule, and warn if it's unclean"""
 | 
						|
        # TODO: this should still check, but now we use bower, not git submodule
 | 
						|
 | 
						|
    def find_server_extensions(self) -> None:
 | 
						|
        """
 | 
						|
        Searches Jupyter paths for jpserver_extensions.
 | 
						|
        """
 | 
						|
 | 
						|
        # Walk through all config files looking for jpserver_extensions.
 | 
						|
        #
 | 
						|
        # Each extension will likely have a JSON config file enabling itself in
 | 
						|
        # the "jupyter_server_config.d" directory. Find each of these and
 | 
						|
        # merge there results in order of precedence.
 | 
						|
        #
 | 
						|
        # Load server extensions with ConfigManager.
 | 
						|
        # This enables merging on keys, which we want for extension enabling.
 | 
						|
        # Regular config loading only merges at the class level,
 | 
						|
        # so each level clobbers the previous.
 | 
						|
        manager = ExtensionConfigManager(read_config_path=self.config_file_paths)
 | 
						|
        extensions = manager.get_jpserver_extensions()
 | 
						|
 | 
						|
        for modulename, enabled in sorted(extensions.items()):
 | 
						|
            if modulename not in self.jpserver_extensions:
 | 
						|
                self.config.ServerApp.jpserver_extensions.update({modulename: enabled})
 | 
						|
                self.jpserver_extensions.update({modulename: enabled})
 | 
						|
 | 
						|
    def init_server_extensions(self) -> None:
 | 
						|
        """
 | 
						|
        If an extension's metadata includes an 'app' key,
 | 
						|
        the value must be a subclass of ExtensionApp. An instance
 | 
						|
        of the class will be created at this step. The config for
 | 
						|
        this instance will inherit the ServerApp's config object
 | 
						|
        and load its own config.
 | 
						|
        """
 | 
						|
        # Create an instance of the ExtensionManager.
 | 
						|
        self.extension_manager = ExtensionManager(log=self.log, serverapp=self)
 | 
						|
        self.extension_manager.from_jpserver_extensions(self.jpserver_extensions)
 | 
						|
        self.extension_manager.link_all_extensions()
 | 
						|
 | 
						|
    def load_server_extensions(self) -> None:
 | 
						|
        """Load any extensions specified by config.
 | 
						|
 | 
						|
        Import the module, then call the load_jupyter_server_extension function,
 | 
						|
        if one exists.
 | 
						|
 | 
						|
        The extension API is experimental, and may change in future releases.
 | 
						|
        """
 | 
						|
        self.extension_manager.load_all_extensions()
 | 
						|
 | 
						|
    def init_mime_overrides(self) -> None:
 | 
						|
        # On some Windows machines, an application has registered incorrect
 | 
						|
        # mimetypes in the registry.
 | 
						|
        # Tornado uses this when serving .css and .js files, causing browsers to
 | 
						|
        # reject these files. We know the mimetype always needs to be text/css for css
 | 
						|
        # and application/javascript for JS, so we override it here
 | 
						|
        # and explicitly tell the mimetypes to not trust the Windows registry
 | 
						|
        if os.name == "nt":
 | 
						|
            # do not trust windows registry, which regularly has bad info
 | 
						|
            mimetypes.init(files=[])
 | 
						|
        # ensure css, js are correct, which are required for pages to function
 | 
						|
        mimetypes.add_type("text/css", ".css")
 | 
						|
        mimetypes.add_type("application/javascript", ".js")
 | 
						|
 | 
						|
    def shutdown_no_activity(self) -> None:
 | 
						|
        """Shutdown server on timeout when there are no kernels or terminals."""
 | 
						|
        km = self.kernel_manager
 | 
						|
        if len(km) != 0:
 | 
						|
            return  # Kernels still running
 | 
						|
 | 
						|
        if self.extension_manager.any_activity():
 | 
						|
            return
 | 
						|
 | 
						|
        seconds_since_active = (utcnow() - self.web_app.last_activity()).total_seconds()
 | 
						|
        self.log.debug("No activity for %d seconds.", seconds_since_active)
 | 
						|
        if seconds_since_active > self.shutdown_no_activity_timeout:
 | 
						|
            self.log.info(
 | 
						|
                "No kernels for %d seconds; shutting down.",
 | 
						|
                seconds_since_active,
 | 
						|
            )
 | 
						|
            self.stop()
 | 
						|
 | 
						|
    def init_shutdown_no_activity(self) -> None:
 | 
						|
        """Initialize a shutdown on no activity."""
 | 
						|
        if self.shutdown_no_activity_timeout > 0:
 | 
						|
            self.log.info(
 | 
						|
                "Will shut down after %d seconds with no kernels.",
 | 
						|
                self.shutdown_no_activity_timeout,
 | 
						|
            )
 | 
						|
            pc = ioloop.PeriodicCallback(self.shutdown_no_activity, 60000)
 | 
						|
            pc.start()
 | 
						|
 | 
						|
    @property
 | 
						|
    def http_server(self) -> httpserver.HTTPServer:
 | 
						|
        """An instance of Tornado's HTTPServer class for the Server Web Application."""
 | 
						|
        try:
 | 
						|
            return self._http_server
 | 
						|
        except AttributeError:
 | 
						|
            msg = (
 | 
						|
                "An HTTPServer instance has not been created for the "
 | 
						|
                "Server Web Application. To create an HTTPServer for this "
 | 
						|
                "application, call `.init_httpserver()`."
 | 
						|
            )
 | 
						|
            raise AttributeError(msg) from None
 | 
						|
 | 
						|
    def init_httpserver(self) -> None:
 | 
						|
        """Creates an instance of a Tornado HTTPServer for the Server Web Application
 | 
						|
        and sets the http_server attribute.
 | 
						|
        """
 | 
						|
        # Check that a web_app has been initialized before starting a server.
 | 
						|
        if not hasattr(self, "web_app"):
 | 
						|
            msg = (
 | 
						|
                "A tornado web application has not be initialized. "
 | 
						|
                "Try calling `.init_webapp()` first."
 | 
						|
            )
 | 
						|
            raise AttributeError(msg)
 | 
						|
 | 
						|
        # Create an instance of the server.
 | 
						|
        self._http_server = httpserver.HTTPServer(
 | 
						|
            self.web_app,
 | 
						|
            ssl_options=self.ssl_options,
 | 
						|
            xheaders=self.trust_xheaders,
 | 
						|
            max_body_size=self.max_body_size,
 | 
						|
            max_buffer_size=self.max_buffer_size,
 | 
						|
        )
 | 
						|
 | 
						|
        # binding sockets must be called from inside an event loop
 | 
						|
        if not self.sock:
 | 
						|
            self._find_http_port()
 | 
						|
        self.io_loop.add_callback(self._bind_http_server)
 | 
						|
 | 
						|
    def _bind_http_server(self) -> None:
 | 
						|
        """Bind our http server."""
 | 
						|
        success = self._bind_http_server_unix() if self.sock else self._bind_http_server_tcp()
 | 
						|
        if not success:
 | 
						|
            self.log.critical(
 | 
						|
                _i18n(
 | 
						|
                    "ERROR: the Jupyter server could not be started because "
 | 
						|
                    "no available port could be found."
 | 
						|
                )
 | 
						|
            )
 | 
						|
            self.exit(1)
 | 
						|
 | 
						|
    def _bind_http_server_unix(self) -> bool:
 | 
						|
        """Bind an http server on unix."""
 | 
						|
        if unix_socket_in_use(self.sock):
 | 
						|
            self.log.warning(_i18n("The socket %s is already in use.") % self.sock)
 | 
						|
            return False
 | 
						|
 | 
						|
        try:
 | 
						|
            sock = bind_unix_socket(self.sock, mode=int(self.sock_mode.encode(), 8))
 | 
						|
            self.http_server.add_socket(sock)
 | 
						|
        except OSError as e:
 | 
						|
            if e.errno == errno.EADDRINUSE:
 | 
						|
                self.log.warning(_i18n("The socket %s is already in use.") % self.sock)
 | 
						|
                return False
 | 
						|
            elif e.errno in (errno.EACCES, getattr(errno, "WSAEACCES", errno.EACCES)):
 | 
						|
                self.log.warning(_i18n("Permission to listen on sock %s denied") % self.sock)
 | 
						|
                return False
 | 
						|
            else:
 | 
						|
                raise
 | 
						|
        else:
 | 
						|
            return True
 | 
						|
 | 
						|
    def _bind_http_server_tcp(self) -> bool:
 | 
						|
        """Bind a tcp server."""
 | 
						|
        self.http_server.listen(self.port, self.ip)
 | 
						|
        return True
 | 
						|
 | 
						|
    def _find_http_port(self) -> None:
 | 
						|
        """Find an available http port."""
 | 
						|
        success = False
 | 
						|
        port = self.port
 | 
						|
        for port in random_ports(self.port, self.port_retries + 1):
 | 
						|
            try:
 | 
						|
                sockets = bind_sockets(port, self.ip)
 | 
						|
                sockets[0].close()
 | 
						|
            except OSError as e:
 | 
						|
                if e.errno == errno.EADDRINUSE:
 | 
						|
                    if self.port_retries:
 | 
						|
                        self.log.info(
 | 
						|
                            _i18n("The port %i is already in use, trying another port.") % port
 | 
						|
                        )
 | 
						|
                    else:
 | 
						|
                        self.log.info(_i18n("The port %i is already in use.") % port)
 | 
						|
                    continue
 | 
						|
                if e.errno in (
 | 
						|
                    errno.EACCES,
 | 
						|
                    getattr(errno, "WSAEACCES", errno.EACCES),
 | 
						|
                ):
 | 
						|
                    self.log.warning(_i18n("Permission to listen on port %i denied.") % port)
 | 
						|
                    continue
 | 
						|
                raise
 | 
						|
            else:
 | 
						|
                success = True
 | 
						|
                self.port = port
 | 
						|
                break
 | 
						|
        if not success:
 | 
						|
            if self.port_retries:
 | 
						|
                self.log.critical(
 | 
						|
                    _i18n(
 | 
						|
                        "ERROR: the Jupyter server could not be started because "
 | 
						|
                        "no available port could be found."
 | 
						|
                    )
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                self.log.critical(
 | 
						|
                    _i18n(
 | 
						|
                        "ERROR: the Jupyter server could not be started because "
 | 
						|
                        "port %i is not available."
 | 
						|
                    )
 | 
						|
                    % port
 | 
						|
                )
 | 
						|
            self.exit(1)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _init_asyncio_patch() -> None:
 | 
						|
        """set default asyncio policy to be compatible with tornado
 | 
						|
 | 
						|
        Tornado 6.0 is not compatible with default asyncio
 | 
						|
        ProactorEventLoop, which lacks basic *_reader methods.
 | 
						|
        Tornado 6.1 adds a workaround to add these methods in a thread,
 | 
						|
        but SelectorEventLoop should still be preferred
 | 
						|
        to avoid the extra thread for ~all of our events,
 | 
						|
        at least until asyncio adds *_reader methods
 | 
						|
        to proactor.
 | 
						|
        """
 | 
						|
        if sys.platform.startswith("win"):
 | 
						|
            import asyncio
 | 
						|
 | 
						|
            try:
 | 
						|
                from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy
 | 
						|
            except ImportError:
 | 
						|
                pass
 | 
						|
                # not affected
 | 
						|
            else:
 | 
						|
                if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
 | 
						|
                    # prefer Selector to Proactor for tornado + pyzmq
 | 
						|
                    asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
 | 
						|
 | 
						|
    def init_metrics(self) -> None:
 | 
						|
        """
 | 
						|
        Initialize any prometheus metrics that need to be set up on server startup
 | 
						|
        """
 | 
						|
        SERVER_INFO.info({"version": __version__})
 | 
						|
 | 
						|
        for ext in self.extension_manager.extensions.values():
 | 
						|
            SERVER_EXTENSION_INFO.labels(
 | 
						|
                name=ext.name, version=ext.version, enabled=str(ext.enabled).lower()
 | 
						|
            )
 | 
						|
 | 
						|
        started = self.web_app.settings["started"]
 | 
						|
        SERVER_STARTED.set(started.timestamp())
 | 
						|
 | 
						|
        LAST_ACTIVITY.set_function(lambda: self.web_app.last_activity().timestamp())
 | 
						|
        ACTIVE_DURATION.set_function(
 | 
						|
            lambda: (
 | 
						|
                self.web_app.last_activity() - self.web_app.settings["started"]
 | 
						|
            ).total_seconds()
 | 
						|
        )
 | 
						|
 | 
						|
    @catch_config_error
 | 
						|
    def initialize(
 | 
						|
        self,
 | 
						|
        argv: t.Optional[list[str]] = None,
 | 
						|
        find_extensions: bool = True,
 | 
						|
        new_httpserver: bool = True,
 | 
						|
        starter_extension: t.Any = None,
 | 
						|
    ) -> None:
 | 
						|
        """Initialize the Server application class, configurables, web application, and http server.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        argv : list or None
 | 
						|
            CLI arguments to parse.
 | 
						|
        find_extensions : bool
 | 
						|
            If True, find and load extensions listed in Jupyter config paths. If False,
 | 
						|
            only load extensions that are passed to ServerApp directly through
 | 
						|
            the `argv`, `config`, or `jpserver_extensions` arguments.
 | 
						|
        new_httpserver : bool
 | 
						|
            If True, a tornado HTTPServer instance will be created and configured for the Server Web
 | 
						|
            Application. This will set the http_server attribute of this class.
 | 
						|
        starter_extension : str
 | 
						|
            If given, it references the name of an extension point that started the Server.
 | 
						|
            We will try to load configuration from extension point
 | 
						|
        """
 | 
						|
        self._init_asyncio_patch()
 | 
						|
        # Parse command line, load ServerApp config files,
 | 
						|
        # and update ServerApp config.
 | 
						|
        # preserve jpserver_extensions, which may have been set by starter_extension
 | 
						|
        # don't let config clobber this value
 | 
						|
        jpserver_extensions = self.jpserver_extensions.copy()
 | 
						|
        super().initialize(argv=argv)
 | 
						|
        self.jpserver_extensions.update(jpserver_extensions)
 | 
						|
        if self._dispatching:
 | 
						|
            return
 | 
						|
        # initialize io loop as early as possible,
 | 
						|
        # so configurables, extensions may reference the event loop
 | 
						|
        self.init_ioloop()
 | 
						|
 | 
						|
        # Then, use extensions' config loading mechanism to
 | 
						|
        # update config. ServerApp config takes precedence.
 | 
						|
        if find_extensions:
 | 
						|
            self.find_server_extensions()
 | 
						|
        self.init_logging()
 | 
						|
        self.init_event_logger()
 | 
						|
        self.init_server_extensions()
 | 
						|
 | 
						|
        # Special case the starter extension and load
 | 
						|
        # any server configuration is provides.
 | 
						|
        if starter_extension:
 | 
						|
            # Configure ServerApp based on named extension.
 | 
						|
            point = self.extension_manager.extension_points[starter_extension]
 | 
						|
            # Set starter_app property.
 | 
						|
            if point.app:
 | 
						|
                self._starter_app = point.app
 | 
						|
            # Load any configuration that comes from the Extension point.
 | 
						|
            self.update_config(Config(point.config))
 | 
						|
 | 
						|
        # Initialize other pieces of the server.
 | 
						|
        self.init_resources()
 | 
						|
        self.init_configurables()
 | 
						|
        self.init_components()
 | 
						|
        self.init_webapp()
 | 
						|
        self.init_signal()
 | 
						|
        self.load_server_extensions()
 | 
						|
        self.init_mime_overrides()
 | 
						|
        self.init_shutdown_no_activity()
 | 
						|
        self.init_metrics()
 | 
						|
        if new_httpserver:
 | 
						|
            self.init_httpserver()
 | 
						|
 | 
						|
    async def cleanup_kernels(self) -> None:
 | 
						|
        """Shutdown all kernels.
 | 
						|
 | 
						|
        The kernels will shutdown themselves when this process no longer exists,
 | 
						|
        but explicit shutdown allows the KernelManagers to cleanup the connection files.
 | 
						|
        """
 | 
						|
        if not getattr(self, "kernel_manager", None):
 | 
						|
            return
 | 
						|
        n_kernels = len(self.kernel_manager.list_kernel_ids())
 | 
						|
        kernel_msg = trans.ngettext(
 | 
						|
            "Shutting down %d kernel", "Shutting down %d kernels", n_kernels
 | 
						|
        )
 | 
						|
        self.log.info(kernel_msg % n_kernels)
 | 
						|
        await ensure_async(self.kernel_manager.shutdown_all())
 | 
						|
 | 
						|
    async def cleanup_extensions(self) -> None:
 | 
						|
        """Call shutdown hooks in all extensions."""
 | 
						|
        if not getattr(self, "extension_manager", None):
 | 
						|
            return
 | 
						|
        n_extensions = len(self.extension_manager.extension_apps)
 | 
						|
        extension_msg = trans.ngettext(
 | 
						|
            "Shutting down %d extension", "Shutting down %d extensions", n_extensions
 | 
						|
        )
 | 
						|
        self.log.info(extension_msg % n_extensions)
 | 
						|
        await ensure_async(self.extension_manager.stop_all_extensions())
 | 
						|
 | 
						|
    def running_server_info(self, kernel_count: bool = True) -> str:
 | 
						|
        """Return the current working directory and the server url information"""
 | 
						|
        info = t.cast(str, self.contents_manager.info_string()) + "\n"
 | 
						|
        if kernel_count:
 | 
						|
            n_kernels = len(self.kernel_manager.list_kernel_ids())
 | 
						|
            kernel_msg = trans.ngettext("%d active kernel", "%d active kernels", n_kernels)
 | 
						|
            info += kernel_msg % n_kernels
 | 
						|
            info += "\n"
 | 
						|
        # Format the info so that the URL fits on a single line in 80 char display
 | 
						|
        info += _i18n("Jupyter Server {version} is running at:\n{url}").format(
 | 
						|
            version=ServerApp.version, url=self.display_url
 | 
						|
        )
 | 
						|
        if self.gateway_config.gateway_enabled:
 | 
						|
            info += (
 | 
						|
                _i18n("\nKernels will be managed by the Gateway server running at:\n%s")
 | 
						|
                % self.gateway_config.url
 | 
						|
            )
 | 
						|
        return info
 | 
						|
 | 
						|
    def server_info(self) -> dict[str, t.Any]:
 | 
						|
        """Return a JSONable dict of information about this server."""
 | 
						|
        return {
 | 
						|
            "url": self.connection_url,
 | 
						|
            "hostname": self.ip if self.ip else "localhost",
 | 
						|
            "port": self.port,
 | 
						|
            "sock": self.sock,
 | 
						|
            "secure": bool(self.certfile),
 | 
						|
            "base_url": self.base_url,
 | 
						|
            "token": self.identity_provider.token,
 | 
						|
            "root_dir": os.path.abspath(self.root_dir),
 | 
						|
            "password": bool(self.password),
 | 
						|
            "pid": os.getpid(),
 | 
						|
            "version": ServerApp.version,
 | 
						|
        }
 | 
						|
 | 
						|
    def write_server_info_file(self) -> None:
 | 
						|
        """Write the result of server_info() to the JSON file info_file."""
 | 
						|
        try:
 | 
						|
            with secure_write(self.info_file) as f:
 | 
						|
                json.dump(self.server_info(), f, indent=2, sort_keys=True)
 | 
						|
        except OSError as e:
 | 
						|
            self.log.error(_i18n("Failed to write server-info to %s: %r"), self.info_file, e)
 | 
						|
 | 
						|
    def remove_server_info_file(self) -> None:
 | 
						|
        """Remove the jpserver-<pid>.json file created for this server.
 | 
						|
 | 
						|
        Ignores the error raised when the file has already been removed.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            os.unlink(self.info_file)
 | 
						|
        except OSError as e:
 | 
						|
            if e.errno != errno.ENOENT:
 | 
						|
                raise
 | 
						|
 | 
						|
    def _resolve_file_to_run_and_root_dir(self) -> str:
 | 
						|
        """Returns a relative path from file_to_run
 | 
						|
        to root_dir. If root_dir and file_to_run
 | 
						|
        are incompatible, i.e. on different subtrees,
 | 
						|
        crash the app and log a critical message. Note
 | 
						|
        that if root_dir is not configured and file_to_run
 | 
						|
        is configured, root_dir will be set to the parent
 | 
						|
        directory of file_to_run.
 | 
						|
        """
 | 
						|
        rootdir_abspath = pathlib.Path(self.root_dir).absolute()
 | 
						|
        file_rawpath = pathlib.Path(self.file_to_run)
 | 
						|
        combined_path = (rootdir_abspath / file_rawpath).absolute()
 | 
						|
        is_child = str(combined_path).startswith(str(rootdir_abspath))
 | 
						|
 | 
						|
        if is_child:
 | 
						|
            if combined_path.parent != rootdir_abspath:
 | 
						|
                self.log.debug(
 | 
						|
                    "The `root_dir` trait is set to a directory that's not "
 | 
						|
                    "the immediate parent directory of `file_to_run`. Note that "
 | 
						|
                    "the server will start at `root_dir` and open the "
 | 
						|
                    "the file from the relative path to the `root_dir`."
 | 
						|
                )
 | 
						|
            return str(combined_path.relative_to(rootdir_abspath))
 | 
						|
 | 
						|
        self.log.critical(
 | 
						|
            "`root_dir` and `file_to_run` are incompatible. They "
 | 
						|
            "don't share the same subtrees. Make sure `file_to_run` "
 | 
						|
            "is on the same path as `root_dir`."
 | 
						|
        )
 | 
						|
        self.exit(1)
 | 
						|
        return ""
 | 
						|
 | 
						|
    def _write_browser_open_file(self, url: str, fh: t.Any) -> None:
 | 
						|
        """Write the browser open file."""
 | 
						|
        if self.identity_provider.token:
 | 
						|
            url = url_concat(url, {"token": self.identity_provider.token})
 | 
						|
        url = url_path_join(self.connection_url, url)
 | 
						|
 | 
						|
        jinja2_env = self.web_app.settings["jinja2_env"]
 | 
						|
        template = jinja2_env.get_template("browser-open.html")
 | 
						|
        fh.write(template.render(open_url=url, base_url=self.base_url))
 | 
						|
 | 
						|
    def write_browser_open_files(self) -> None:
 | 
						|
        """Write an `browser_open_file` and `browser_open_file_to_run` files
 | 
						|
 | 
						|
        This can be used to open a file directly in a browser.
 | 
						|
        """
 | 
						|
        # default_url contains base_url, but so does connection_url
 | 
						|
        self.write_browser_open_file()
 | 
						|
 | 
						|
        # Create a second browser open file if
 | 
						|
        # file_to_run is set.
 | 
						|
        if self.file_to_run:
 | 
						|
            # Make sure file_to_run and root_dir are compatible.
 | 
						|
            file_to_run_relpath = self._resolve_file_to_run_and_root_dir()
 | 
						|
 | 
						|
            file_open_url = url_escape(
 | 
						|
                url_path_join(self.file_url_prefix, *file_to_run_relpath.split(os.sep))
 | 
						|
            )
 | 
						|
 | 
						|
            with open(self.browser_open_file_to_run, "w", encoding="utf-8") as f:
 | 
						|
                self._write_browser_open_file(file_open_url, f)
 | 
						|
 | 
						|
    def write_browser_open_file(self) -> None:
 | 
						|
        """Write an jpserver-<pid>-open.html file
 | 
						|
 | 
						|
        This can be used to open the notebook in a browser
 | 
						|
        """
 | 
						|
        # default_url contains base_url, but so does connection_url
 | 
						|
        open_url = self.default_url[len(self.base_url) :]
 | 
						|
 | 
						|
        with open(self.browser_open_file, "w", encoding="utf-8") as f:
 | 
						|
            self._write_browser_open_file(open_url, f)
 | 
						|
 | 
						|
    def remove_browser_open_files(self) -> None:
 | 
						|
        """Remove the `browser_open_file` and `browser_open_file_to_run` files
 | 
						|
        created for this server.
 | 
						|
 | 
						|
        Ignores the error raised when the file has already been removed.
 | 
						|
        """
 | 
						|
        self.remove_browser_open_file()
 | 
						|
        try:
 | 
						|
            os.unlink(self.browser_open_file_to_run)
 | 
						|
        except OSError as e:
 | 
						|
            if e.errno != errno.ENOENT:
 | 
						|
                raise
 | 
						|
 | 
						|
    def remove_browser_open_file(self) -> None:
 | 
						|
        """Remove the jpserver-<pid>-open.html file created for this server.
 | 
						|
 | 
						|
        Ignores the error raised when the file has already been removed.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            os.unlink(self.browser_open_file)
 | 
						|
        except OSError as e:
 | 
						|
            if e.errno != errno.ENOENT:
 | 
						|
                raise
 | 
						|
 | 
						|
    def _prepare_browser_open(self) -> tuple[str, t.Optional[str]]:
 | 
						|
        """Prepare to open the browser."""
 | 
						|
        if not self.use_redirect_file:
 | 
						|
            uri = self.default_url[len(self.base_url) :]
 | 
						|
 | 
						|
            if self.identity_provider.token:
 | 
						|
                uri = url_concat(uri, {"token": self.identity_provider.token})
 | 
						|
 | 
						|
        if self.file_to_run:  # noqa: SIM108
 | 
						|
            # Create a separate, temporary open-browser-file
 | 
						|
            # pointing at a specific file.
 | 
						|
            open_file = self.browser_open_file_to_run
 | 
						|
        else:
 | 
						|
            # otherwise, just return the usual open browser file.
 | 
						|
            open_file = self.browser_open_file
 | 
						|
 | 
						|
        if self.use_redirect_file:
 | 
						|
            assembled_url = urljoin("file:", pathname2url(open_file))
 | 
						|
        else:
 | 
						|
            assembled_url = url_path_join(self.connection_url, uri)
 | 
						|
 | 
						|
        return assembled_url, open_file
 | 
						|
 | 
						|
    def launch_browser(self) -> None:
 | 
						|
        """Launch the browser."""
 | 
						|
        # Deferred import for environments that do not have
 | 
						|
        # the webbrowser module.
 | 
						|
        import webbrowser
 | 
						|
 | 
						|
        try:
 | 
						|
            browser = webbrowser.get(self.browser or None)
 | 
						|
        except webbrowser.Error as e:
 | 
						|
            self.log.warning(_i18n("No web browser found: %r.") % e)
 | 
						|
            browser = None
 | 
						|
 | 
						|
        if not browser:
 | 
						|
            return
 | 
						|
 | 
						|
        assembled_url, _ = self._prepare_browser_open()
 | 
						|
 | 
						|
        def target():
 | 
						|
            assert browser is not None
 | 
						|
            browser.open(assembled_url, new=self.webbrowser_open_new)
 | 
						|
 | 
						|
        threading.Thread(target=target).start()
 | 
						|
 | 
						|
    def start_app(self) -> None:
 | 
						|
        """Start the Jupyter Server application."""
 | 
						|
        super().start()
 | 
						|
 | 
						|
        if not self.allow_root:
 | 
						|
            # check if we are running as root, and abort if it's not allowed
 | 
						|
            try:
 | 
						|
                uid = os.geteuid()
 | 
						|
            except AttributeError:
 | 
						|
                uid = -1  # anything nonzero here, since we can't check UID assume non-root
 | 
						|
            if uid == 0:
 | 
						|
                self.log.critical(
 | 
						|
                    _i18n("Running as root is not recommended. Use --allow-root to bypass.")
 | 
						|
                )
 | 
						|
                self.exit(1)
 | 
						|
 | 
						|
        info = self.log.info
 | 
						|
        for line in self.running_server_info(kernel_count=False).split("\n"):
 | 
						|
            info(line)
 | 
						|
        info(
 | 
						|
            _i18n(
 | 
						|
                "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
 | 
						|
            )
 | 
						|
        )
 | 
						|
        if "dev" in __version__:
 | 
						|
            info(
 | 
						|
                _i18n(
 | 
						|
                    "Welcome to Project Jupyter! Explore the various tools available"
 | 
						|
                    " and their corresponding documentation. If you are interested"
 | 
						|
                    " in contributing to the platform, please visit the community"
 | 
						|
                    " resources section at https://jupyter.org/community.html."
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
        self.write_server_info_file()
 | 
						|
 | 
						|
        if not self.no_browser_open_file:
 | 
						|
            self.write_browser_open_files()
 | 
						|
 | 
						|
        # Handle the browser opening.
 | 
						|
        if self.open_browser and not self.sock:
 | 
						|
            self.launch_browser()
 | 
						|
 | 
						|
        if self.identity_provider.token and self.identity_provider.token_generated:
 | 
						|
            # log full URL with generated token, so there's a copy/pasteable link
 | 
						|
            # with auth info.
 | 
						|
            if self.sock:
 | 
						|
                self.log.critical(
 | 
						|
                    "\n".join(
 | 
						|
                        [
 | 
						|
                            "\n",
 | 
						|
                            "Jupyter Server is listening on %s" % self.display_url,
 | 
						|
                            "",
 | 
						|
                            (
 | 
						|
                                "UNIX sockets are not browser-connectable, but you can tunnel to "
 | 
						|
                                f"the instance via e.g.`ssh -L 8888:{self.sock} -N user@this_host` and then "
 | 
						|
                                f"open e.g. {self.connection_url} in a browser."
 | 
						|
                            ),
 | 
						|
                        ]
 | 
						|
                    )
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                if self.no_browser_open_file:
 | 
						|
                    message = [
 | 
						|
                        "\n",
 | 
						|
                        _i18n("To access the server, copy and paste one of these URLs:"),
 | 
						|
                        "    %s" % self.display_url,
 | 
						|
                    ]
 | 
						|
                else:
 | 
						|
                    message = [
 | 
						|
                        "\n",
 | 
						|
                        _i18n(
 | 
						|
                            "To access the server, open this file in a browser:",
 | 
						|
                        ),
 | 
						|
                        "    %s" % urljoin("file:", pathname2url(self.browser_open_file)),
 | 
						|
                        _i18n(
 | 
						|
                            "Or copy and paste one of these URLs:",
 | 
						|
                        ),
 | 
						|
                        "    %s" % self.display_url,
 | 
						|
                    ]
 | 
						|
 | 
						|
                self.log.critical("\n".join(message))
 | 
						|
 | 
						|
    async def _cleanup(self) -> None:
 | 
						|
        """General cleanup of files, extensions and kernels created
 | 
						|
        by this instance ServerApp.
 | 
						|
        """
 | 
						|
        self.remove_server_info_file()
 | 
						|
        self.remove_browser_open_files()
 | 
						|
        await self.cleanup_extensions()
 | 
						|
        await self.cleanup_kernels()
 | 
						|
        try:
 | 
						|
            await self.kernel_websocket_connection_class.close_all()  # type:ignore[attr-defined]
 | 
						|
        except AttributeError:
 | 
						|
            # This can happen in two different scenarios:
 | 
						|
            #
 | 
						|
            # 1. During tests, where the _cleanup method is invoked without
 | 
						|
            #    the corresponding initialize method having been invoked.
 | 
						|
            # 2. If the provided `kernel_websocket_connection_class` does not
 | 
						|
            #    implement the `close_all` class method.
 | 
						|
            #
 | 
						|
            # In either case, we don't need to do anything and just want to treat
 | 
						|
            # the raised error as a no-op.
 | 
						|
            pass
 | 
						|
        if getattr(self, "kernel_manager", None):
 | 
						|
            self.kernel_manager.__del__()
 | 
						|
        if getattr(self, "session_manager", None):
 | 
						|
            self.session_manager.close()
 | 
						|
        if hasattr(self, "http_server"):
 | 
						|
            # Stop a server if its set.
 | 
						|
            self.http_server.stop()
 | 
						|
 | 
						|
    def start_ioloop(self) -> None:
 | 
						|
        """Start the IO Loop."""
 | 
						|
        if sys.platform.startswith("win"):
 | 
						|
            # add no-op to wake every 5s
 | 
						|
            # to handle signals that may be ignored by the inner loop
 | 
						|
            pc = ioloop.PeriodicCallback(lambda: None, 5000)
 | 
						|
            pc.start()
 | 
						|
        try:
 | 
						|
            self.io_loop.add_callback(self._post_start)
 | 
						|
            self.io_loop.start()
 | 
						|
        except KeyboardInterrupt:
 | 
						|
            self.log.info(_i18n("Interrupted..."))
 | 
						|
 | 
						|
    def init_ioloop(self) -> None:
 | 
						|
        """init self.io_loop so that an extension can use it by io_loop.call_later() to create background tasks"""
 | 
						|
        self.io_loop = ioloop.IOLoop.current()
 | 
						|
 | 
						|
    async def _post_start(self):
 | 
						|
        """Add an async hook to start tasks after the event loop is running.
 | 
						|
 | 
						|
        This will also attempt to start all tasks found in
 | 
						|
        the `start_extension` method in Extension Apps.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            await self.extension_manager.start_all_extensions()
 | 
						|
        except Exception as err:
 | 
						|
            self.log.error(err)
 | 
						|
 | 
						|
    def start(self) -> None:
 | 
						|
        """Start the Jupyter server app, after initialization
 | 
						|
 | 
						|
        This method takes no arguments so all configuration and initialization
 | 
						|
        must be done prior to calling this method."""
 | 
						|
        self.start_app()
 | 
						|
        self.start_ioloop()
 | 
						|
 | 
						|
    async def _stop(self) -> None:
 | 
						|
        """Cleanup resources and stop the IO Loop."""
 | 
						|
        await self._cleanup()
 | 
						|
        if getattr(self, "io_loop", None):
 | 
						|
            self.io_loop.stop()
 | 
						|
 | 
						|
    def stop(self, from_signal: bool = False) -> None:
 | 
						|
        """Cleanup resources and stop the server."""
 | 
						|
        # signal that stopping has begun
 | 
						|
        self._stopping = True
 | 
						|
        if hasattr(self, "http_server"):
 | 
						|
            # Stop a server if its set.
 | 
						|
            self.http_server.stop()
 | 
						|
        if getattr(self, "io_loop", None):
 | 
						|
            # use IOLoop.add_callback because signal.signal must be called
 | 
						|
            # from main thread
 | 
						|
            if from_signal:
 | 
						|
                self.io_loop.add_callback_from_signal(self._stop)
 | 
						|
            else:
 | 
						|
                self.io_loop.add_callback(self._stop)
 | 
						|
 | 
						|
 | 
						|
def list_running_servers(
 | 
						|
    runtime_dir: t.Optional[str] = None, log: t.Optional[logging.Logger] = None
 | 
						|
) -> t.Generator[t.Any, None, None]:
 | 
						|
    """Iterate over the server info files of running Jupyter servers.
 | 
						|
 | 
						|
    Given a runtime directory, find jpserver-* files in the security directory,
 | 
						|
    and yield dicts of their information, each one pertaining to
 | 
						|
    a currently running Jupyter server instance.
 | 
						|
    """
 | 
						|
    if runtime_dir is None:
 | 
						|
        runtime_dir = jupyter_runtime_dir()
 | 
						|
 | 
						|
    # The runtime dir might not exist
 | 
						|
    if not os.path.isdir(runtime_dir):
 | 
						|
        return
 | 
						|
 | 
						|
    for file_name in os.listdir(runtime_dir):
 | 
						|
        if re.match("jpserver-(.+).json", file_name):
 | 
						|
            with open(os.path.join(runtime_dir, file_name), encoding="utf-8") as f:
 | 
						|
                # Handle race condition where file is being written.
 | 
						|
                try:
 | 
						|
                    info = json.load(f)
 | 
						|
                except json.JSONDecodeError:
 | 
						|
                    continue
 | 
						|
 | 
						|
            # Simple check whether that process is really still running
 | 
						|
            # Also remove leftover files from IPython 2.x without a pid field
 | 
						|
            if ("pid" in info) and check_pid(info["pid"]):
 | 
						|
                yield info
 | 
						|
            else:
 | 
						|
                # If the process has died, try to delete its info file
 | 
						|
                try:
 | 
						|
                    os.unlink(os.path.join(runtime_dir, file_name))
 | 
						|
                except OSError as e:
 | 
						|
                    if log:
 | 
						|
                        log.warning(_i18n("Deleting server info file failed: %s.") % e)
 | 
						|
 | 
						|
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
# Main entry point
 | 
						|
# -----------------------------------------------------------------------------
 | 
						|
 | 
						|
main = launch_new_instance = ServerApp.launch_instance
 |