1# coding: utf-8
2"""A tornado based Jupyter server."""
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5import binascii
6import datetime
7import errno
8import gettext
9import hashlib
10import hmac
11import inspect
12import io
13import ipaddress
14import json
15import logging
16import mimetypes
17import os
18import pathlib
19import random
20import re
21import select
22import signal
23import socket
24import stat
25import sys
26import threading
27import time
28import urllib
29import webbrowser
30from base64 import encodebytes
31
32try:
33    import resource
34except ImportError:
35    # Windows
36    resource = None
37
38from jinja2 import Environment, FileSystemLoader
39
40from jupyter_core.paths import secure_write
41from jupyter_server.transutils import trans, _i18n
42from jupyter_server.utils import run_sync_in_loop, urljoin, pathname2url
43
44# the minimum viable tornado version: needs to be kept in sync with setup.py
45MIN_TORNADO = (6, 1, 0)
46
47try:
48    import tornado
49
50    assert tornado.version_info >= MIN_TORNADO
51except (ImportError, AttributeError, AssertionError) as e:  # pragma: no cover
52    raise ImportError(_i18n("The Jupyter Server requires tornado >=%s.%s.%s") % MIN_TORNADO) from e
53
54from tornado import httpserver
55from tornado import ioloop
56from tornado import web
57from tornado.httputil import url_concat
58from tornado.log import LogFormatter, app_log, access_log, gen_log
59
60if not sys.platform.startswith("win"):
61    from tornado.netutil import bind_unix_socket
62
63from jupyter_server import (
64    DEFAULT_JUPYTER_SERVER_PORT,
65    DEFAULT_STATIC_FILES_PATH,
66    DEFAULT_TEMPLATE_PATH_LIST,
67    __version__,
68)
69
70from jupyter_server.base.handlers import MainHandler, RedirectWithParams, Template404
71from jupyter_server.log import log_request
72from jupyter_server.services.kernels.kernelmanager import (
73    MappingKernelManager,
74    AsyncMappingKernelManager,
75)
76from jupyter_server.services.config import ConfigManager
77from jupyter_server.services.contents.manager import AsyncContentsManager, ContentsManager
78from jupyter_server.services.contents.filemanager import (
79    AsyncFileContentsManager,
80    FileContentsManager,
81)
82from jupyter_server.services.contents.largefilemanager import LargeFileManager
83from jupyter_server.services.sessions.sessionmanager import SessionManager
84from jupyter_server.gateway.managers import (
85    GatewayMappingKernelManager,
86    GatewayKernelSpecManager,
87    GatewaySessionManager,
88    GatewayClient,
89)
90
91from jupyter_server.auth.login import LoginHandler
92from jupyter_server.auth.logout import LogoutHandler
93from jupyter_server.base.handlers import FileFindHandler
94
95from traitlets.config import Config
96from traitlets.config.application import catch_config_error, boolean_flag
97from jupyter_core.application import (
98    JupyterApp,
99    base_flags,
100    base_aliases,
101)
102from jupyter_core.paths import jupyter_config_path
103from jupyter_client import KernelManager
104from jupyter_client.kernelspec import KernelSpecManager
105from jupyter_client.session import Session
106from nbformat.sign import NotebookNotary
107from traitlets import (
108    Any,
109    Dict,
110    Unicode,
111    Integer,
112    List,
113    Bool,
114    Bytes,
115    Instance,
116    TraitError,
117    Type,
118    Float,
119    observe,
120    default,
121    validate,
122)
123from jupyter_core.paths import jupyter_runtime_dir
124from jupyter_server._sysinfo import get_sys_info
125
126from jupyter_server._tz import utcnow
127from jupyter_server.utils import (
128    url_path_join,
129    check_pid,
130    url_escape,
131    pathname2url,
132    unix_socket_in_use,
133    urlencode_unix_socket_path,
134    fetch,
135)
136
137from jupyter_server.extension.serverextension import ServerExtensionApp
138from jupyter_server.extension.manager import ExtensionManager
139from jupyter_server.extension.config import ExtensionConfigManager
140from jupyter_server.traittypes import TypeFromClasses
141
142# Tolerate missing terminado package.
143try:
144    from jupyter_server.terminal import TerminalManager
145
146    terminado_available = True
147except ImportError:
148    terminado_available = False
149
150# -----------------------------------------------------------------------------
151# Module globals
152# -----------------------------------------------------------------------------
153
154_examples = """
155jupyter server                       # start the server
156jupyter server  --certfile=mycert.pem # use SSL/TLS certificate
157jupyter server password              # enter a password to protect the server
158"""
159
160JUPYTER_SERVICE_HANDLERS = dict(
161    auth=None,
162    api=["jupyter_server.services.api.handlers"],
163    config=["jupyter_server.services.config.handlers"],
164    contents=["jupyter_server.services.contents.handlers"],
165    files=["jupyter_server.files.handlers"],
166    kernels=["jupyter_server.services.kernels.handlers"],
167    kernelspecs=[
168        "jupyter_server.kernelspecs.handlers",
169        "jupyter_server.services.kernelspecs.handlers",
170    ],
171    nbconvert=["jupyter_server.nbconvert.handlers", "jupyter_server.services.nbconvert.handlers"],
172    security=["jupyter_server.services.security.handlers"],
173    sessions=["jupyter_server.services.sessions.handlers"],
174    shutdown=["jupyter_server.services.shutdown"],
175    view=["jupyter_server.view.handlers"],
176)
177
178# Added for backwards compatibility from classic notebook server.
179DEFAULT_SERVER_PORT = DEFAULT_JUPYTER_SERVER_PORT
180
181# -----------------------------------------------------------------------------
182# Helper functions
183# -----------------------------------------------------------------------------
184
185
186def random_ports(port, n):
187    """Generate a list of n random ports near the given port.
188
189    The first 5 ports will be sequential, and the remaining n-5 will be
190    randomly selected in the range [port-2*n, port+2*n].
191    """
192    for i in range(min(5, n)):
193        yield port + i
194    for i in range(n - 5):
195        yield max(1, port + random.randint(-2 * n, 2 * n))
196
197
198def load_handlers(name):
199    """Load the (URL pattern, handler) tuples for each component."""
200    mod = __import__(name, fromlist=["default_handlers"])
201    return mod.default_handlers
202
203
204# -----------------------------------------------------------------------------
205# The Tornado web application
206# -----------------------------------------------------------------------------
207
208
209class ServerWebApplication(web.Application):
210    def __init__(
211        self,
212        jupyter_app,
213        default_services,
214        kernel_manager,
215        contents_manager,
216        session_manager,
217        kernel_spec_manager,
218        config_manager,
219        extra_services,
220        log,
221        base_url,
222        default_url,
223        settings_overrides,
224        jinja_env_options,
225    ):
226
227        settings = self.init_settings(
228            jupyter_app,
229            kernel_manager,
230            contents_manager,
231            session_manager,
232            kernel_spec_manager,
233            config_manager,
234            extra_services,
235            log,
236            base_url,
237            default_url,
238            settings_overrides,
239            jinja_env_options,
240        )
241        handlers = self.init_handlers(default_services, settings)
242
243        super(ServerWebApplication, self).__init__(handlers, **settings)
244
245    def init_settings(
246        self,
247        jupyter_app,
248        kernel_manager,
249        contents_manager,
250        session_manager,
251        kernel_spec_manager,
252        config_manager,
253        extra_services,
254        log,
255        base_url,
256        default_url,
257        settings_overrides,
258        jinja_env_options=None,
259    ):
260
261        _template_path = settings_overrides.get(
262            "template_path",
263            jupyter_app.template_file_path,
264        )
265        if isinstance(_template_path, str):
266            _template_path = (_template_path,)
267        template_path = [os.path.expanduser(path) for path in _template_path]
268
269        jenv_opt = {"autoescape": True}
270        jenv_opt.update(jinja_env_options if jinja_env_options else {})
271
272        env = Environment(
273            loader=FileSystemLoader(template_path), extensions=["jinja2.ext.i18n"], **jenv_opt
274        )
275        sys_info = get_sys_info()
276
277        # If the user is running the server in a git directory, make the assumption
278        # that this is a dev install and suggest to the developer `npm run build:watch`.
279        base_dir = os.path.realpath(os.path.join(__file__, "..", ".."))
280        dev_mode = os.path.exists(os.path.join(base_dir, ".git"))
281
282        nbui = gettext.translation(
283            "nbui", localedir=os.path.join(base_dir, "jupyter_server/i18n"), fallback=True
284        )
285        env.install_gettext_translations(nbui, newstyle=False)
286
287        if sys_info["commit_source"] == "repository":
288            # don't cache (rely on 304) when working from master
289            version_hash = ""
290        else:
291            # reset the cache on server restart
292            version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
293
294        now = utcnow()
295
296        root_dir = contents_manager.root_dir
297        home = os.path.expanduser("~")
298        if root_dir.startswith(home + os.path.sep):
299            # collapse $HOME to ~
300            root_dir = "~" + root_dir[len(home) :]
301
302        settings = dict(
303            # basics
304            log_function=log_request,
305            base_url=base_url,
306            default_url=default_url,
307            template_path=template_path,
308            static_path=jupyter_app.static_file_path,
309            static_custom_path=jupyter_app.static_custom_path,
310            static_handler_class=FileFindHandler,
311            static_url_prefix=url_path_join(base_url, "/static/"),
312            static_handler_args={
313                # don't cache custom.js
314                "no_cache_paths": [url_path_join(base_url, "static", "custom")],
315            },
316            version_hash=version_hash,
317            # rate limits
318            iopub_msg_rate_limit=jupyter_app.iopub_msg_rate_limit,
319            iopub_data_rate_limit=jupyter_app.iopub_data_rate_limit,
320            rate_limit_window=jupyter_app.rate_limit_window,
321            # authentication
322            cookie_secret=jupyter_app.cookie_secret,
323            login_url=url_path_join(base_url, "/login"),
324            login_handler_class=jupyter_app.login_handler_class,
325            logout_handler_class=jupyter_app.logout_handler_class,
326            password=jupyter_app.password,
327            xsrf_cookies=True,
328            disable_check_xsrf=jupyter_app.disable_check_xsrf,
329            allow_remote_access=jupyter_app.allow_remote_access,
330            local_hostnames=jupyter_app.local_hostnames,
331            authenticate_prometheus=jupyter_app.authenticate_prometheus,
332            # managers
333            kernel_manager=kernel_manager,
334            contents_manager=contents_manager,
335            session_manager=session_manager,
336            kernel_spec_manager=kernel_spec_manager,
337            config_manager=config_manager,
338            # handlers
339            extra_services=extra_services,
340            # Jupyter stuff
341            started=now,
342            # place for extensions to register activity
343            # so that they can prevent idle-shutdown
344            last_activity_times={},
345            jinja_template_vars=jupyter_app.jinja_template_vars,
346            websocket_url=jupyter_app.websocket_url,
347            shutdown_button=jupyter_app.quit_button,
348            config=jupyter_app.config,
349            config_dir=jupyter_app.config_dir,
350            allow_password_change=jupyter_app.allow_password_change,
351            server_root_dir=root_dir,
352            jinja2_env=env,
353            terminals_available=terminado_available and jupyter_app.terminals_enabled,
354            serverapp=jupyter_app,
355        )
356
357        # allow custom overrides for the tornado web app.
358        settings.update(settings_overrides)
359
360        if base_url and "xsrf_cookie_kwargs" not in settings:
361            # default: set xsrf cookie on base_url
362            settings["xsrf_cookie_kwargs"] = {"path": base_url}
363        return settings
364
365    def init_handlers(self, default_services, settings):
366        """Load the (URL pattern, handler) tuples for each component."""
367        # Order matters. The first handler to match the URL will handle the request.
368        handlers = []
369        # load extra services specified by users before default handlers
370        for service in settings["extra_services"]:
371            handlers.extend(load_handlers(service))
372
373        # Add auth services.
374        if "auth" in default_services:
375            handlers.extend([(r"/login", settings["login_handler_class"])])
376            handlers.extend([(r"/logout", settings["logout_handler_class"])])
377
378        # Load default services. Raise exception if service not
379        # found in JUPYTER_SERVICE_HANLDERS.
380        for service in default_services:
381            if service in JUPYTER_SERVICE_HANDLERS:
382                locations = JUPYTER_SERVICE_HANDLERS[service]
383                if locations is not None:
384                    for loc in locations:
385                        handlers.extend(load_handlers(loc))
386            else:
387                raise Exception(
388                    "{} is not recognized as a jupyter_server "
389                    "service. If this is a custom service, "
390                    "try adding it to the "
391                    "`extra_services` list.".format(service)
392                )
393
394        # Add extra handlers from contents manager.
395        handlers.extend(settings["contents_manager"].get_extra_handlers())
396
397        # If gateway mode is enabled, replace appropriate handlers to perform redirection
398        if GatewayClient.instance().gateway_enabled:
399            # for each handler required for gateway, locate its pattern
400            # in the current list and replace that entry...
401            gateway_handlers = load_handlers("jupyter_server.gateway.handlers")
402            for i, gwh in enumerate(gateway_handlers):
403                for j, h in enumerate(handlers):
404                    if gwh[0] == h[0]:
405                        handlers[j] = (gwh[0], gwh[1])
406                        break
407
408        # register base handlers last
409        handlers.extend(load_handlers("jupyter_server.base.handlers"))
410
411        if settings["default_url"] != settings["base_url"]:
412            # set the URL that will be redirected from `/`
413            handlers.append(
414                (
415                    r"/?",
416                    RedirectWithParams,
417                    {
418                        "url": settings["default_url"],
419                        "permanent": False,  # want 302, not 301
420                    },
421                )
422            )
423        else:
424            handlers.append((r"/", MainHandler))
425
426        # prepend base_url onto the patterns that we match
427        new_handlers = []
428        for handler in handlers:
429            pattern = url_path_join(settings["base_url"], handler[0])
430            new_handler = tuple([pattern] + list(handler[1:]))
431            new_handlers.append(new_handler)
432        # add 404 on the end, which will catch everything that falls through
433        new_handlers.append((r"(.*)", Template404))
434        return new_handlers
435
436    def last_activity(self):
437        """Get a UTC timestamp for when the server last did something.
438
439        Includes: API activity, kernel activity, kernel shutdown, and terminal
440        activity.
441        """
442        sources = [
443            self.settings["started"],
444            self.settings["kernel_manager"].last_kernel_activity,
445        ]
446        try:
447            sources.append(self.settings["api_last_activity"])
448        except KeyError:
449            pass
450        try:
451            sources.append(self.settings["terminal_last_activity"])
452        except KeyError:
453            pass
454        sources.extend(self.settings["last_activity_times"].values())
455        return max(sources)
456
457
458class JupyterPasswordApp(JupyterApp):
459    """Set a password for the Jupyter server.
460
461    Setting a password secures the Jupyter server
462    and removes the need for token-based authentication.
463    """
464
465    description = __doc__
466
467    def _config_file_default(self):
468        return os.path.join(self.config_dir, "jupyter_server_config.json")
469
470    def start(self):
471        from jupyter_server.auth.security import set_password
472
473        set_password(config_file=self.config_file)
474        self.log.info("Wrote hashed password to %s" % self.config_file)
475
476
477def shutdown_server(server_info, timeout=5, log=None):
478    """Shutdown a Jupyter server in a separate process.
479
480    *server_info* should be a dictionary as produced by list_running_servers().
481
482    Will first try to request shutdown using /api/shutdown .
483    On Unix, if the server is still running after *timeout* seconds, it will
484    send SIGTERM. After another timeout, it escalates to SIGKILL.
485
486    Returns True if the server was stopped by any means, False if stopping it
487    failed (on Windows).
488    """
489    from tornado.httpclient import HTTPClient, HTTPRequest
490
491    url = server_info["url"]
492    pid = server_info["pid"]
493
494    if log:
495        log.debug("POST request to %sapi/shutdown", url)
496
497    r = fetch(url, method="POST", headers={"Authorization": "token " + server_info["token"]})
498    # Poll to see if it shut down.
499    for _ in range(timeout * 10):
500        if not check_pid(pid):
501            if log:
502                log.debug("Server PID %s is gone", pid)
503            return True
504        time.sleep(0.1)
505
506    if sys.platform.startswith("win"):
507        return False
508
509    if log:
510        log.debug("SIGTERM to PID %s", pid)
511    os.kill(pid, signal.SIGTERM)
512
513    # Poll to see if it shut down.
514    for _ in range(timeout * 10):
515        if not check_pid(pid):
516            if log:
517                log.debug("Server PID %s is gone", pid)
518            return True
519        time.sleep(0.1)
520
521    if log:
522        log.debug("SIGKILL to PID %s", pid)
523    os.kill(pid, signal.SIGKILL)
524    return True  # SIGKILL cannot be caught
525
526
527class JupyterServerStopApp(JupyterApp):
528
529    version = __version__
530    description = "Stop currently running Jupyter server for a given port"
531
532    port = Integer(
533        DEFAULT_JUPYTER_SERVER_PORT,
534        config=True,
535        help="Port of the server to be killed. Default %s" % DEFAULT_JUPYTER_SERVER_PORT,
536    )
537
538    sock = Unicode(u"", config=True, help="UNIX socket of the server to be killed.")
539
540    def parse_command_line(self, argv=None):
541        super(JupyterServerStopApp, self).parse_command_line(argv)
542        if self.extra_args:
543            try:
544                self.port = int(self.extra_args[0])
545            except ValueError:
546                # self.extra_args[0] was not an int, so it must be a string (unix socket).
547                self.sock = self.extra_args[0]
548
549    def shutdown_server(self, server):
550        return shutdown_server(server, log=self.log)
551
552    def _shutdown_or_exit(self, target_endpoint, server):
553        print("Shutting down server on %s..." % target_endpoint)
554        if not self.shutdown_server(server):
555            sys.exit("Could not stop server on %s" % target_endpoint)
556
557    @staticmethod
558    def _maybe_remove_unix_socket(socket_path):
559        try:
560            os.unlink(socket_path)
561        except (OSError, IOError):
562            pass
563
564    def start(self):
565        servers = list(list_running_servers(self.runtime_dir, log=self.log))
566        if not servers:
567            self.exit("There are no running servers (per %s)" % self.runtime_dir)
568        for server in servers:
569            if self.sock:
570                sock = server.get("sock", None)
571                if sock and sock == self.sock:
572                    self._shutdown_or_exit(sock, server)
573                    # Attempt to remove the UNIX socket after stopping.
574                    self._maybe_remove_unix_socket(sock)
575                    return
576            elif self.port:
577                port = server.get("port", None)
578                if port == self.port:
579                    self._shutdown_or_exit(port, server)
580                    return
581        current_endpoint = self.sock or self.port
582        print(
583            "There is currently no server running on {}".format(current_endpoint), file=sys.stderr
584        )
585        print("Ports/sockets currently in use:", file=sys.stderr)
586        for server in servers:
587            print(" - {}".format(server.get("sock") or server["port"]), file=sys.stderr)
588        self.exit(1)
589
590
591class JupyterServerListApp(JupyterApp):
592    version = __version__
593    description = _i18n("List currently running Jupyter servers.")
594
595    flags = dict(
596        jsonlist=(
597            {"JupyterServerListApp": {"jsonlist": True}},
598            _i18n("Produce machine-readable JSON list output."),
599        ),
600        json=(
601            {"JupyterServerListApp": {"json": True}},
602            _i18n("Produce machine-readable JSON object on each line of output."),
603        ),
604    )
605
606    jsonlist = Bool(
607        False,
608        config=True,
609        help=_i18n(
610            "If True, the output will be a JSON list of objects, one per "
611            "active Jupyer server, each with the details from the "
612            "relevant server info file."
613        ),
614    )
615    json = Bool(
616        False,
617        config=True,
618        help=_i18n(
619            "If True, each line of output will be a JSON object with the "
620            "details from the server info file. For a JSON list output, "
621            "see the JupyterServerListApp.jsonlist configuration value"
622        ),
623    )
624
625    def start(self):
626        serverinfo_list = list(list_running_servers(self.runtime_dir, log=self.log))
627        if self.jsonlist:
628            print(json.dumps(serverinfo_list, indent=2))
629        elif self.json:
630            for serverinfo in serverinfo_list:
631                print(json.dumps(serverinfo))
632        else:
633            print("Currently running servers:")
634            for serverinfo in serverinfo_list:
635                url = serverinfo["url"]
636                if serverinfo.get("token"):
637                    url = url + "?token=%s" % serverinfo["token"]
638                print(url, "::", serverinfo["root_dir"])
639
640
641# -----------------------------------------------------------------------------
642# Aliases and Flags
643# -----------------------------------------------------------------------------
644
645flags = dict(base_flags)
646
647flags["allow-root"] = (
648    {"ServerApp": {"allow_root": True}},
649    _i18n("Allow the server to be run from root user."),
650)
651flags["no-browser"] = (
652    {"ServerApp": {"open_browser": False}, "ExtensionApp": {"open_browser": False}},
653    _i18n("Prevent the opening of the default url in the browser."),
654)
655flags["debug"] = (
656    {"ServerApp": {"log_level": "DEBUG"}, "ExtensionApp": {"log_level": "DEBUG"}},
657    _i18n("Set debug level for the extension and underlying server applications."),
658)
659flags["autoreload"] = (
660    {"ServerApp": {"autoreload": True}},
661    """Autoreload the webapp
662    Enable reloading of the tornado webapp and all imported Python packages
663    when any changes are made to any Python src files in server or
664    extensions.
665    """,
666)
667
668
669# Add notebook manager flags
670flags.update(
671    boolean_flag(
672        "script", "FileContentsManager.save_script", "DEPRECATED, IGNORED", "DEPRECATED, IGNORED"
673    )
674)
675
676aliases = dict(base_aliases)
677
678aliases.update(
679    {
680        "ip": "ServerApp.ip",
681        "port": "ServerApp.port",
682        "port-retries": "ServerApp.port_retries",
683        "sock": "ServerApp.sock",
684        "sock-mode": "ServerApp.sock_mode",
685        "transport": "KernelManager.transport",
686        "keyfile": "ServerApp.keyfile",
687        "certfile": "ServerApp.certfile",
688        "client-ca": "ServerApp.client_ca",
689        "notebook-dir": "ServerApp.root_dir",
690        "preferred-dir": "ServerApp.preferred_dir",
691        "browser": "ServerApp.browser",
692        "pylab": "ServerApp.pylab",
693        "gateway-url": "GatewayClient.url",
694    }
695)
696
697# -----------------------------------------------------------------------------
698# ServerApp
699# -----------------------------------------------------------------------------
700
701
702class ServerApp(JupyterApp):
703
704    name = "jupyter-server"
705    version = __version__
706    description = _i18n(
707        """The Jupyter Server.
708
709    This launches a Tornado-based Jupyter Server."""
710    )
711    examples = _examples
712
713    flags = Dict(flags)
714    aliases = Dict(aliases)
715
716    classes = [
717        KernelManager,
718        Session,
719        MappingKernelManager,
720        KernelSpecManager,
721        AsyncMappingKernelManager,
722        ContentsManager,
723        FileContentsManager,
724        AsyncContentsManager,
725        AsyncFileContentsManager,
726        NotebookNotary,
727        GatewayMappingKernelManager,
728        GatewayKernelSpecManager,
729        GatewaySessionManager,
730        GatewayClient,
731    ]
732    if terminado_available:  # Only necessary when terminado is available
733        classes.append(TerminalManager)
734
735    subcommands = dict(
736        list=(JupyterServerListApp, JupyterServerListApp.description.splitlines()[0]),
737        stop=(JupyterServerStopApp, JupyterServerStopApp.description.splitlines()[0]),
738        password=(JupyterPasswordApp, JupyterPasswordApp.description.splitlines()[0]),
739        extension=(ServerExtensionApp, ServerExtensionApp.description.splitlines()[0]),
740    )
741
742    # A list of services whose handlers will be exposed.
743    # Subclasses can override this list to
744    # expose a subset of these handlers.
745    default_services = (
746        "api",
747        "auth",
748        "config",
749        "contents",
750        "files",
751        "kernels",
752        "kernelspecs",
753        "nbconvert",
754        "security",
755        "sessions",
756        "shutdown",
757        "view",
758    )
759
760    _log_formatter_cls = LogFormatter
761
762    @default("log_level")
763    def _default_log_level(self):
764        return logging.INFO
765
766    @default("log_format")
767    def _default_log_format(self):
768        """override default log format to include date & time"""
769        return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
770
771    # file to be opened in the Jupyter server
772    file_to_run = Unicode("", help="Open the named file when the application is launched.").tag(
773        config=True
774    )
775
776    file_url_prefix = Unicode(
777        "notebooks", help="The URL prefix where files are opened directly."
778    ).tag(config=True)
779
780    # Network related information
781    allow_origin = Unicode(
782        "",
783        config=True,
784        help="""Set the Access-Control-Allow-Origin header
785
786        Use '*' to allow any origin to access your server.
787
788        Takes precedence over allow_origin_pat.
789        """,
790    )
791
792    allow_origin_pat = Unicode(
793        "",
794        config=True,
795        help="""Use a regular expression for the Access-Control-Allow-Origin header
796
797        Requests from an origin matching the expression will get replies with:
798
799            Access-Control-Allow-Origin: origin
800
801        where `origin` is the origin of the request.
802
803        Ignored if allow_origin is set.
804        """,
805    )
806
807    allow_credentials = Bool(
808        False, config=True, help=_i18n("Set the Access-Control-Allow-Credentials: true header")
809    )
810
811    allow_root = Bool(
812        False, config=True, help=_i18n("Whether to allow the user to run the server as root.")
813    )
814
815    autoreload = Bool(
816        False,
817        config=True,
818        help=_i18n("Reload the webapp when changes are made to any Python src files."),
819    )
820
821    default_url = Unicode("/", config=True, help=_i18n("The default URL to redirect to from `/`"))
822
823    ip = Unicode(
824        "localhost", config=True, help=_i18n("The IP address the Jupyter server will listen on.")
825    )
826
827    @default("ip")
828    def _default_ip(self):
829        """Return localhost if available, 127.0.0.1 otherwise.
830
831        On some (horribly broken) systems, localhost cannot be bound.
832        """
833        s = socket.socket()
834        try:
835            s.bind(("localhost", 0))
836        except socket.error as e:
837            self.log.warning(
838                _i18n("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s"), e
839            )
840            return "127.0.0.1"
841        else:
842            s.close()
843            return "localhost"
844
845    @validate("ip")
846    def _validate_ip(self, proposal):
847        value = proposal["value"]
848        if value == u"*":
849            value = u""
850        return value
851
852    custom_display_url = Unicode(
853        u"",
854        config=True,
855        help=_i18n(
856            """Override URL shown to users.
857
858        Replace actual URL, including protocol, address, port and base URL,
859        with the given value when displaying URL to the users. Do not change
860        the actual connection URL. If authentication token is enabled, the
861        token is added to the custom URL automatically.
862
863        This option is intended to be used when the URL to display to the user
864        cannot be determined reliably by the Jupyter server (proxified
865        or containerized setups for example)."""
866        ),
867    )
868
869    port_env = "JUPYTER_PORT"
870    port_default_value = DEFAULT_JUPYTER_SERVER_PORT
871
872    port = Integer(
873        config=True, help=_i18n("The port the server will listen on (env: JUPYTER_PORT).")
874    )
875
876    @default("port")
877    def port_default(self):
878        return int(os.getenv(self.port_env, self.port_default_value))
879
880    port_retries_env = "JUPYTER_PORT_RETRIES"
881    port_retries_default_value = 50
882    port_retries = Integer(
883        port_retries_default_value,
884        config=True,
885        help=_i18n(
886            "The number of additional ports to try if the specified port is not "
887            "available (env: JUPYTER_PORT_RETRIES)."
888        ),
889    )
890
891    @default("port_retries")
892    def port_retries_default(self):
893        return int(os.getenv(self.port_retries_env, self.port_retries_default_value))
894
895    sock = Unicode(u"", config=True, help="The UNIX socket the Jupyter server will listen on.")
896
897    sock_mode = Unicode(
898        "0600", config=True, help="The permissions mode for UNIX socket creation (default: 0600)."
899    )
900
901    @validate("sock_mode")
902    def _validate_sock_mode(self, proposal):
903        value = proposal["value"]
904        try:
905            converted_value = int(value.encode(), 8)
906            assert all(
907                (
908                    # Ensure the mode is at least user readable/writable.
909                    bool(converted_value & stat.S_IRUSR),
910                    bool(converted_value & stat.S_IWUSR),
911                    # And isn't out of bounds.
912                    converted_value <= 2 ** 12,
913                )
914            )
915        except ValueError:
916            raise TraitError('invalid --sock-mode value: %s, please specify as e.g. "0600"' % value)
917        except AssertionError:
918            raise TraitError(
919                "invalid --sock-mode value: %s, must have u+rw (0600) at a minimum" % value
920            )
921        return value
922
923    certfile = Unicode(
924        u"", config=True, help=_i18n("""The full path to an SSL/TLS certificate file.""")
925    )
926
927    keyfile = Unicode(
928        u"",
929        config=True,
930        help=_i18n("""The full path to a private key file for usage with SSL/TLS."""),
931    )
932
933    client_ca = Unicode(
934        u"",
935        config=True,
936        help=_i18n(
937            """The full path to a certificate authority certificate for SSL/TLS client authentication."""
938        ),
939    )
940
941    cookie_secret_file = Unicode(
942        config=True, help=_i18n("""The file where the cookie secret is stored.""")
943    )
944
945    @default("cookie_secret_file")
946    def _default_cookie_secret_file(self):
947        return os.path.join(self.runtime_dir, "jupyter_cookie_secret")
948
949    cookie_secret = Bytes(
950        b"",
951        config=True,
952        help="""The random bytes used to secure cookies.
953        By default this is a new random number every time you start the server.
954        Set it to a value in a config file to enable logins to persist across server sessions.
955
956        Note: Cookie secrets should be kept private, do not share config files with
957        cookie_secret stored in plaintext (you can read the value from a file).
958        """,
959    )
960
961    @default("cookie_secret")
962    def _default_cookie_secret(self):
963        if os.path.exists(self.cookie_secret_file):
964            with io.open(self.cookie_secret_file, "rb") as f:
965                key = f.read()
966        else:
967            key = encodebytes(os.urandom(32))
968            self._write_cookie_secret_file(key)
969        h = hmac.new(key, digestmod=hashlib.sha256)
970        h.update(self.password.encode())
971        return h.digest()
972
973    def _write_cookie_secret_file(self, secret):
974        """write my secret to my secret_file"""
975        self.log.info(_i18n("Writing Jupyter server cookie secret to %s"), self.cookie_secret_file)
976        try:
977            with secure_write(self.cookie_secret_file, True) as f:
978                f.write(secret)
979        except OSError as e:
980            self.log.error(
981                _i18n("Failed to write cookie secret to %s: %s"), self.cookie_secret_file, e
982            )
983
984    token = Unicode(
985        "<generated>",
986        help=_i18n(
987            """Token used for authenticating first-time connections to the server.
988
989        The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly
990        with the JUPYTER_TOKEN environment variable.
991
992        When no password is enabled,
993        the default is to generate a new, random token.
994
995        Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.
996        """
997        ),
998    ).tag(config=True)
999
1000    _token_generated = True
1001
1002    @default("token")
1003    def _token_default(self):
1004        if os.getenv("JUPYTER_TOKEN"):
1005            self._token_generated = False
1006            return os.getenv("JUPYTER_TOKEN")
1007        if os.getenv("JUPYTER_TOKEN_FILE"):
1008            self._token_generated = False
1009            with io.open(os.getenv("JUPYTER_TOKEN_FILE"), "r") as token_file:
1010                return token_file.read()
1011        if self.password:
1012            # no token if password is enabled
1013            self._token_generated = False
1014            return u""
1015        else:
1016            self._token_generated = True
1017            return binascii.hexlify(os.urandom(24)).decode("ascii")
1018
1019    min_open_files_limit = Integer(
1020        config=True,
1021        help="""
1022        Gets or sets a lower bound on the open file handles process resource
1023        limit. This may need to be increased if you run into an
1024        OSError: [Errno 24] Too many open files.
1025        This is not applicable when running on Windows.
1026        """,
1027        allow_none=True,
1028    )
1029
1030    @default("min_open_files_limit")
1031    def _default_min_open_files_limit(self):
1032        if resource is None:
1033            # Ignoring min_open_files_limit because the limit cannot be adjusted (for example, on Windows)
1034            return None
1035
1036        soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
1037
1038        DEFAULT_SOFT = 4096
1039        if hard >= DEFAULT_SOFT:
1040            return DEFAULT_SOFT
1041
1042        self.log.debug(
1043            "Default value for min_open_files_limit is ignored (hard=%r, soft=%r)", hard, soft
1044        )
1045
1046        return soft
1047
1048    max_body_size = Integer(
1049        512 * 1024 * 1024,
1050        config=True,
1051        help="""
1052        Sets the maximum allowed size of the client request body, specified in
1053        the Content-Length request header field. If the size in a request
1054        exceeds the configured value, a malformed HTTP message is returned to
1055        the client.
1056
1057        Note: max_body_size is applied even in streaming mode.
1058        """,
1059    )
1060
1061    max_buffer_size = Integer(
1062        512 * 1024 * 1024,
1063        config=True,
1064        help="""
1065        Gets or sets the maximum amount of memory, in bytes, that is allocated
1066        for use by the buffer manager.
1067        """,
1068    )
1069
1070    @observe("token")
1071    def _token_changed(self, change):
1072        self._token_generated = False
1073
1074    password = Unicode(
1075        u"",
1076        config=True,
1077        help="""Hashed password to use for web authentication.
1078
1079                      To generate, type in a python/IPython shell:
1080
1081                        from jupyter_server.auth import passwd; passwd()
1082
1083                      The string should be of the form type:salt:hashed-password.
1084                      """,
1085    )
1086
1087    password_required = Bool(
1088        False,
1089        config=True,
1090        help="""Forces users to use a password for the Jupyter server.
1091                      This is useful in a multi user environment, for instance when
1092                      everybody in the LAN can access each other's machine through ssh.
1093
1094                      In such a case, serving on localhost is not secure since
1095                      any user can connect to the Jupyter server via ssh.
1096
1097                      """,
1098    )
1099
1100    allow_password_change = Bool(
1101        True,
1102        config=True,
1103        help="""Allow password to be changed at login for the Jupyter server.
1104
1105                    While logging in with a token, the Jupyter server UI will give the opportunity to
1106                    the user to enter a new password at the same time that will replace
1107                    the token login mechanism.
1108
1109                    This can be set to false to prevent changing password from the UI/API.
1110                    """,
1111    )
1112
1113    disable_check_xsrf = Bool(
1114        False,
1115        config=True,
1116        help="""Disable cross-site-request-forgery protection
1117
1118        Jupyter notebook 4.3.1 introduces protection from cross-site request forgeries,
1119        requiring API requests to either:
1120
1121        - originate from pages served by this server (validated with XSRF cookie and token), or
1122        - authenticate with a token
1123
1124        Some anonymous compute resources still desire the ability to run code,
1125        completely without authentication.
1126        These services can disable all authentication and security checks,
1127        with the full knowledge of what that implies.
1128        """,
1129    )
1130
1131    allow_remote_access = Bool(
1132        config=True,
1133        help="""Allow requests where the Host header doesn't point to a local server
1134
1135       By default, requests get a 403 forbidden response if the 'Host' header
1136       shows that the browser thinks it's on a non-local domain.
1137       Setting this option to True disables this check.
1138
1139       This protects against 'DNS rebinding' attacks, where a remote web server
1140       serves you a page and then changes its DNS to send later requests to a
1141       local IP, bypassing same-origin checks.
1142
1143       Local IP addresses (such as 127.0.0.1 and ::1) are allowed as local,
1144       along with hostnames configured in local_hostnames.
1145       """,
1146    )
1147
1148    @default("allow_remote_access")
1149    def _default_allow_remote(self):
1150        """Disallow remote access if we're listening only on loopback addresses"""
1151
1152        # if blank, self.ip was configured to "*" meaning bind to all interfaces,
1153        # see _valdate_ip
1154        if self.ip == "":
1155            return True
1156
1157        try:
1158            addr = ipaddress.ip_address(self.ip)
1159        except ValueError:
1160            # Address is a hostname
1161            for info in socket.getaddrinfo(self.ip, self.port, 0, socket.SOCK_STREAM):
1162                addr = info[4][0]
1163
1164                try:
1165                    parsed = ipaddress.ip_address(addr.split("%")[0])
1166                except ValueError:
1167                    self.log.warning("Unrecognised IP address: %r", addr)
1168                    continue
1169
1170                # Macs map localhost to 'fe80::1%lo0', a link local address
1171                # scoped to the loopback interface. For now, we'll assume that
1172                # any scoped link-local address is effectively local.
1173                if not (parsed.is_loopback or (("%" in addr) and parsed.is_link_local)):
1174                    return True
1175            return False
1176        else:
1177            return not addr.is_loopback
1178
1179    use_redirect_file = Bool(
1180        True,
1181        config=True,
1182        help="""Disable launching browser by redirect file
1183     For versions of notebook > 5.7.2, a security feature measure was added that
1184     prevented the authentication token used to launch the browser from being visible.
1185     This feature makes it difficult for other users on a multi-user system from
1186     running code in your Jupyter session as you.
1187     However, some environments (like Windows Subsystem for Linux (WSL) and Chromebooks),
1188     launching a browser using a redirect file can lead the browser failing to load.
1189     This is because of the difference in file structures/paths between the runtime and
1190     the browser.
1191
1192     Disabling this setting to False will disable this behavior, allowing the browser
1193     to launch by using a URL and visible token (as before).
1194     """,
1195    )
1196
1197    local_hostnames = List(
1198        Unicode(),
1199        ["localhost"],
1200        config=True,
1201        help="""Hostnames to allow as local when allow_remote_access is False.
1202
1203       Local IP addresses (such as 127.0.0.1 and ::1) are automatically accepted
1204       as local as well.
1205       """,
1206    )
1207
1208    open_browser = Bool(
1209        False,
1210        config=True,
1211        help="""Whether to open in a browser after starting.
1212                        The specific browser used is platform dependent and
1213                        determined by the python standard library `webbrowser`
1214                        module, unless it is overridden using the --browser
1215                        (ServerApp.browser) configuration option.
1216                        """,
1217    )
1218
1219    browser = Unicode(
1220        u"",
1221        config=True,
1222        help="""Specify what command to use to invoke a web
1223                      browser when starting the server. If not specified, the
1224                      default browser will be determined by the `webbrowser`
1225                      standard library module, which allows setting of the
1226                      BROWSER environment variable to override it.
1227                      """,
1228    )
1229
1230    webbrowser_open_new = Integer(
1231        2,
1232        config=True,
1233        help=_i18n(
1234            """Specify where to open the server on startup. This is the
1235        `new` argument passed to the standard library method `webbrowser.open`.
1236        The behaviour is not guaranteed, but depends on browser support. Valid
1237        values are:
1238
1239         - 2 opens a new tab,
1240         - 1 opens a new window,
1241         - 0 opens in an existing window.
1242
1243        See the `webbrowser.open` documentation for details.
1244        """
1245        ),
1246    )
1247
1248    tornado_settings = Dict(
1249        config=True,
1250        help=_i18n(
1251            "Supply overrides for the tornado.web.Application that the " "Jupyter server uses."
1252        ),
1253    )
1254
1255    websocket_compression_options = Any(
1256        None,
1257        config=True,
1258        help=_i18n(
1259            """
1260        Set the tornado compression options for websocket connections.
1261
1262        This value will be returned from :meth:`WebSocketHandler.get_compression_options`.
1263        None (default) will disable compression.
1264        A dict (even an empty one) will enable compression.
1265
1266        See the tornado docs for WebSocketHandler.get_compression_options for details.
1267        """
1268        ),
1269    )
1270    terminado_settings = Dict(
1271        config=True,
1272        help=_i18n('Supply overrides for terminado. Currently only supports "shell_command".'),
1273    )
1274
1275    cookie_options = Dict(
1276        config=True,
1277        help=_i18n(
1278            "Extra keyword arguments to pass to `set_secure_cookie`."
1279            " See tornado's set_secure_cookie docs for details."
1280        ),
1281    )
1282    get_secure_cookie_kwargs = Dict(
1283        config=True,
1284        help=_i18n(
1285            "Extra keyword arguments to pass to `get_secure_cookie`."
1286            " See tornado's get_secure_cookie docs for details."
1287        ),
1288    )
1289    ssl_options = Dict(
1290        allow_none=True,
1291        config=True,
1292        help=_i18n(
1293            """Supply SSL options for the tornado HTTPServer.
1294            See the tornado docs for details."""
1295        ),
1296    )
1297
1298    jinja_environment_options = Dict(
1299        config=True, help=_i18n("Supply extra arguments that will be passed to Jinja environment.")
1300    )
1301
1302    jinja_template_vars = Dict(
1303        config=True,
1304        help=_i18n("Extra variables to supply to jinja templates when rendering."),
1305    )
1306
1307    base_url = Unicode(
1308        "/",
1309        config=True,
1310        help="""The base URL for the Jupyter server.
1311
1312                       Leading and trailing slashes can be omitted,
1313                       and will automatically be added.
1314                       """,
1315    )
1316
1317    @validate("base_url")
1318    def _update_base_url(self, proposal):
1319        value = proposal["value"]
1320        if not value.startswith("/"):
1321            value = "/" + value
1322        if not value.endswith("/"):
1323            value = value + "/"
1324        return value
1325
1326    extra_static_paths = List(
1327        Unicode(),
1328        config=True,
1329        help="""Extra paths to search for serving static files.
1330
1331        This allows adding javascript/css to be available from the Jupyter server machine,
1332        or overriding individual files in the IPython""",
1333    )
1334
1335    @property
1336    def static_file_path(self):
1337        """return extra paths + the default location"""
1338        return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
1339
1340    static_custom_path = List(Unicode(), help=_i18n("""Path to search for custom.js, css"""))
1341
1342    @default("static_custom_path")
1343    def _default_static_custom_path(self):
1344        return [os.path.join(d, "custom") for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH)]
1345
1346    extra_template_paths = List(
1347        Unicode(),
1348        config=True,
1349        help=_i18n(
1350            """Extra paths to search for serving jinja templates.
1351
1352        Can be used to override templates from jupyter_server.templates."""
1353        ),
1354    )
1355
1356    @property
1357    def template_file_path(self):
1358        """return extra paths + the default locations"""
1359        return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
1360
1361    extra_services = List(
1362        Unicode(),
1363        config=True,
1364        help=_i18n(
1365            """handlers that should be loaded at higher priority than the default services"""
1366        ),
1367    )
1368
1369    websocket_url = Unicode(
1370        "",
1371        config=True,
1372        help="""The base URL for websockets,
1373        if it differs from the HTTP server (hint: it almost certainly doesn't).
1374
1375        Should be in the form of an HTTP origin: ws[s]://hostname[:port]
1376        """,
1377    )
1378
1379    quit_button = Bool(
1380        True,
1381        config=True,
1382        help="""If True, display controls to shut down the Jupyter server, such as menu items or buttons.""",
1383    )
1384
1385    # REMOVE in VERSION 2.0
1386    # Temporarily allow content managers to inherit from the 'notebook'
1387    # package. We will deprecate this in the next major release.
1388    contents_manager_class = TypeFromClasses(
1389        default_value=LargeFileManager,
1390        klasses=[
1391            "jupyter_server.services.contents.manager.ContentsManager",
1392            "notebook.services.contents.manager.ContentsManager",
1393        ],
1394        config=True,
1395        help=_i18n("The content manager class to use."),
1396    )
1397
1398    # Throws a deprecation warning to notebook based contents managers.
1399    @observe("contents_manager_class")
1400    def _observe_contents_manager_class(self, change):
1401        new = change["new"]
1402        # If 'new' is a class, get a string representing the import
1403        # module path.
1404        if inspect.isclass(new):
1405            new = new.__module__
1406
1407        if new.startswith("notebook"):
1408            self.log.warning(
1409                "The specified 'contents_manager_class' class inherits a manager from the "
1410                "'notebook' package. This is not guaranteed to work in future "
1411                "releases of Jupyter Server. Instead, consider switching the "
1412                "manager to inherit from the 'jupyter_server' managers. "
1413                "Jupyter Server will temporarily allow 'notebook' managers "
1414                "until its next major release (2.x)."
1415            )
1416
1417    kernel_manager_class = Type(
1418        default_value=AsyncMappingKernelManager,
1419        klass=MappingKernelManager,
1420        config=True,
1421        help=_i18n("The kernel manager class to use."),
1422    )
1423
1424    session_manager_class = Type(
1425        default_value=SessionManager, config=True, help=_i18n("The session manager class to use.")
1426    )
1427
1428    config_manager_class = Type(
1429        default_value=ConfigManager, config=True, help=_i18n("The config manager class to use")
1430    )
1431
1432    kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
1433
1434    kernel_spec_manager_class = Type(
1435        default_value=KernelSpecManager,
1436        config=True,
1437        help="""
1438        The kernel spec manager class to use. Should be a subclass
1439        of `jupyter_client.kernelspec.KernelSpecManager`.
1440
1441        The Api of KernelSpecManager is provisional and might change
1442        without warning between this version of Jupyter and the next stable one.
1443        """,
1444    )
1445
1446    login_handler_class = Type(
1447        default_value=LoginHandler,
1448        klass=web.RequestHandler,
1449        config=True,
1450        help=_i18n("The login handler class to use."),
1451    )
1452
1453    logout_handler_class = Type(
1454        default_value=LogoutHandler,
1455        klass=web.RequestHandler,
1456        config=True,
1457        help=_i18n("The logout handler class to use."),
1458    )
1459
1460    trust_xheaders = Bool(
1461        False,
1462        config=True,
1463        help=(
1464            _i18n(
1465                "Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
1466                "sent by the upstream reverse proxy. Necessary if the proxy handles SSL"
1467            )
1468        ),
1469    )
1470
1471    info_file = Unicode()
1472
1473    @default("info_file")
1474    def _default_info_file(self):
1475        info_file = "jpserver-%s.json" % os.getpid()
1476        return os.path.join(self.runtime_dir, info_file)
1477
1478    browser_open_file = Unicode()
1479
1480    @default("browser_open_file")
1481    def _default_browser_open_file(self):
1482        basename = "jpserver-%s-open.html" % os.getpid()
1483        return os.path.join(self.runtime_dir, basename)
1484
1485    browser_open_file_to_run = Unicode()
1486
1487    @default("browser_open_file_to_run")
1488    def _default_browser_open_file_to_run(self):
1489        basename = "jpserver-file-to-run-%s-open.html" % os.getpid()
1490        return os.path.join(self.runtime_dir, basename)
1491
1492    pylab = Unicode(
1493        "disabled",
1494        config=True,
1495        help=_i18n(
1496            """
1497        DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
1498        """
1499        ),
1500    )
1501
1502    @observe("pylab")
1503    def _update_pylab(self, change):
1504        """when --pylab is specified, display a warning and exit"""
1505        if change["new"] != "warn":
1506            backend = " %s" % change["new"]
1507        else:
1508            backend = ""
1509        self.log.error(
1510            _i18n("Support for specifying --pylab on the command line has been removed.")
1511        )
1512        self.log.error(
1513            _i18n("Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.").format(
1514                backend
1515            )
1516        )
1517        self.exit(1)
1518
1519    notebook_dir = Unicode(config=True, help=_i18n("DEPRECATED, use root_dir."))
1520
1521    @observe("notebook_dir")
1522    def _update_notebook_dir(self, change):
1523        if self._root_dir_set:
1524            # only use deprecated config if new config is not set
1525            return
1526        self.log.warning(_i18n("notebook_dir is deprecated, use root_dir"))
1527        self.root_dir = change["new"]
1528
1529    root_dir = Unicode(config=True, help=_i18n("The directory to use for notebooks and kernels."))
1530    _root_dir_set = False
1531
1532    @default("root_dir")
1533    def _default_root_dir(self):
1534        if self.file_to_run:
1535            self._root_dir_set = True
1536            return os.path.dirname(os.path.abspath(self.file_to_run))
1537        else:
1538            return os.getcwd()
1539
1540    def _normalize_dir(self, value):
1541        # Strip any trailing slashes
1542        # *except* if it's root
1543        _, path = os.path.splitdrive(value)
1544        if path == os.sep:
1545            return value
1546        value = value.rstrip(os.sep)
1547        if not os.path.isabs(value):
1548            # If we receive a non-absolute path, make it absolute.
1549            value = os.path.abspath(value)
1550        return value
1551
1552    @validate("root_dir")
1553    def _root_dir_validate(self, proposal):
1554        value = self._normalize_dir(proposal["value"])
1555        if not os.path.isdir(value):
1556            raise TraitError(trans.gettext("No such directory: '%r'") % value)
1557        return value
1558
1559    preferred_dir = Unicode(
1560        config=True,
1561        help=trans.gettext("Preferred starting directory to use for notebooks and kernels."),
1562    )
1563
1564    @default("preferred_dir")
1565    def _default_prefered_dir(self):
1566        return self.root_dir
1567
1568    @validate("preferred_dir")
1569    def _preferred_dir_validate(self, proposal):
1570        value = self._normalize_dir(proposal["value"])
1571        if not os.path.isdir(value):
1572            raise TraitError(trans.gettext("No such preferred dir: '%r'") % value)
1573
1574        # preferred_dir must be equal or a subdir of root_dir
1575        if not value.startswith(self.root_dir):
1576            raise TraitError(
1577                trans.gettext("preferred_dir must be equal or a subdir of root_dir: '%r'") % value
1578            )
1579
1580        return value
1581
1582    @observe("root_dir")
1583    def _root_dir_changed(self, change):
1584        self._root_dir_set = True
1585        if not self.preferred_dir.startswith(change["new"]):
1586            self.log.warning(
1587                trans.gettext("Value of preferred_dir updated to use value of root_dir")
1588            )
1589            self.preferred_dir = change["new"]
1590
1591    @observe("server_extensions")
1592    def _update_server_extensions(self, change):
1593        self.log.warning(_i18n("server_extensions is deprecated, use jpserver_extensions"))
1594        self.server_extensions = change["new"]
1595
1596    jpserver_extensions = Dict(
1597        default_value={},
1598        value_trait=Bool(),
1599        config=True,
1600        help=(
1601            _i18n(
1602                "Dict of Python modules to load as Jupyter server extensions."
1603                "Entry values can be used to enable and disable the loading of"
1604                "the extensions. The extensions will be loaded in alphabetical "
1605                "order."
1606            )
1607        ),
1608    )
1609
1610    reraise_server_extension_failures = Bool(
1611        False,
1612        config=True,
1613        help=_i18n("Reraise exceptions encountered loading server extensions?"),
1614    )
1615
1616    iopub_msg_rate_limit = Float(
1617        1000,
1618        config=True,
1619        help=_i18n(
1620            """(msgs/sec)
1621        Maximum rate at which messages can be sent on iopub before they are
1622        limited."""
1623        ),
1624    )
1625
1626    iopub_data_rate_limit = Float(
1627        1000000,
1628        config=True,
1629        help=_i18n(
1630            """(bytes/sec)
1631        Maximum rate at which stream output can be sent on iopub before they are
1632        limited."""
1633        ),
1634    )
1635
1636    rate_limit_window = Float(
1637        3,
1638        config=True,
1639        help=_i18n(
1640            """(sec) Time window used to
1641        check the message and data rate limits."""
1642        ),
1643    )
1644
1645    shutdown_no_activity_timeout = Integer(
1646        0,
1647        config=True,
1648        help=(
1649            "Shut down the server after N seconds with no kernels or "
1650            "terminals running and no activity. "
1651            "This can be used together with culling idle kernels "
1652            "(MappingKernelManager.cull_idle_timeout) to "
1653            "shutdown the Jupyter server when it's not in use. This is not "
1654            "precisely timed: it may shut down up to a minute later. "
1655            "0 (the default) disables this automatic shutdown."
1656        ),
1657    )
1658
1659    terminals_enabled = Bool(
1660        True,
1661        config=True,
1662        help=_i18n(
1663            """Set to False to disable terminals.
1664
1665         This does *not* make the server more secure by itself.
1666         Anything the user can in a terminal, they can also do in a notebook.
1667
1668         Terminals may also be automatically disabled if the terminado package
1669         is not available.
1670         """
1671        ),
1672    )
1673
1674    # Since use of terminals is also a function of whether the terminado package is
1675    # available, this variable holds the "final indication" of whether terminal functionality
1676    # should be considered (particularly during shutdown/cleanup).  It is enabled only
1677    # once both the terminals "service" can be initialized and terminals_enabled is True.
1678    # Note: this variable is slightly different from 'terminals_available' in the web settings
1679    # in that this variable *could* remain false if terminado is available, yet the terminal
1680    # service's initialization still fails.  As a result, this variable holds the truth.
1681    terminals_available = False
1682
1683    authenticate_prometheus = Bool(
1684        True,
1685        help=""""
1686        Require authentication to access prometheus metrics.
1687        """,
1688        config=True,
1689    )
1690
1691    _starter_app = Instance(
1692        default_value=None,
1693        allow_none=True,
1694        klass="jupyter_server.extension.application.ExtensionApp",
1695    )
1696
1697    @property
1698    def starter_app(self):
1699        """Get the Extension that started this server."""
1700        return self._starter_app
1701
1702    def parse_command_line(self, argv=None):
1703
1704        super(ServerApp, self).parse_command_line(argv)
1705
1706        if self.extra_args:
1707            arg0 = self.extra_args[0]
1708            f = os.path.abspath(arg0)
1709            self.argv.remove(arg0)
1710            if not os.path.exists(f):
1711                self.log.critical(_i18n("No such file or directory: %s"), f)
1712                self.exit(1)
1713
1714            # Use config here, to ensure that it takes higher priority than
1715            # anything that comes from the config dirs.
1716            c = Config()
1717            if os.path.isdir(f):
1718                c.ServerApp.root_dir = f
1719            elif os.path.isfile(f):
1720                c.ServerApp.file_to_run = f
1721            self.update_config(c)
1722
1723    def init_configurables(self):
1724
1725        # If gateway server is configured, replace appropriate managers to perform redirection.  To make
1726        # this determination, instantiate the GatewayClient config singleton.
1727        self.gateway_config = GatewayClient.instance(parent=self)
1728
1729        if self.gateway_config.gateway_enabled:
1730            self.kernel_manager_class = (
1731                "jupyter_server.gateway.managers.GatewayMappingKernelManager"
1732            )
1733            self.session_manager_class = "jupyter_server.gateway.managers.GatewaySessionManager"
1734            self.kernel_spec_manager_class = (
1735                "jupyter_server.gateway.managers.GatewayKernelSpecManager"
1736            )
1737
1738        self.kernel_spec_manager = self.kernel_spec_manager_class(
1739            parent=self,
1740        )
1741        self.kernel_manager = self.kernel_manager_class(
1742            parent=self,
1743            log=self.log,
1744            connection_dir=self.runtime_dir,
1745            kernel_spec_manager=self.kernel_spec_manager,
1746        )
1747        self.contents_manager = self.contents_manager_class(
1748            parent=self,
1749            log=self.log,
1750        )
1751        self.session_manager = self.session_manager_class(
1752            parent=self,
1753            log=self.log,
1754            kernel_manager=self.kernel_manager,
1755            contents_manager=self.contents_manager,
1756        )
1757        self.config_manager = self.config_manager_class(
1758            parent=self,
1759            log=self.log,
1760        )
1761
1762    def init_logging(self):
1763        # This prevents double log messages because tornado use a root logger that
1764        # self.log is a child of. The logging module dipatches log messages to a log
1765        # and all of its ancenstors until propagate is set to False.
1766        self.log.propagate = False
1767
1768        for log in app_log, access_log, gen_log:
1769            # consistent log output name (ServerApp instead of tornado.access, etc.)
1770            log.name = self.log.name
1771        # hook up tornado 3's loggers to our app handlers
1772        logger = logging.getLogger("tornado")
1773        logger.propagate = True
1774        logger.parent = self.log
1775        logger.setLevel(self.log.level)
1776
1777    def init_webapp(self):
1778        """initialize tornado webapp"""
1779        self.tornado_settings["allow_origin"] = self.allow_origin
1780        self.tornado_settings["websocket_compression_options"] = self.websocket_compression_options
1781        if self.allow_origin_pat:
1782            self.tornado_settings["allow_origin_pat"] = re.compile(self.allow_origin_pat)
1783        self.tornado_settings["allow_credentials"] = self.allow_credentials
1784        self.tornado_settings["autoreload"] = self.autoreload
1785        self.tornado_settings["cookie_options"] = self.cookie_options
1786        self.tornado_settings["get_secure_cookie_kwargs"] = self.get_secure_cookie_kwargs
1787        self.tornado_settings["token"] = self.token
1788
1789        # ensure default_url starts with base_url
1790        if not self.default_url.startswith(self.base_url):
1791            self.default_url = url_path_join(self.base_url, self.default_url)
1792
1793        if self.password_required and (not self.password):
1794            self.log.critical(
1795                _i18n("Jupyter servers are configured to only be run with a password.")
1796            )
1797            self.log.critical(_i18n("Hint: run the following command to set a password"))
1798            self.log.critical(_i18n("\t$ python -m jupyter_server.auth password"))
1799            sys.exit(1)
1800
1801        # Socket options validation.
1802        if self.sock:
1803            if self.port != DEFAULT_JUPYTER_SERVER_PORT:
1804                self.log.critical(
1805                    ("Options --port and --sock are mutually exclusive. Aborting."),
1806                )
1807                sys.exit(1)
1808            else:
1809                # Reset the default port if we're using a UNIX socket.
1810                self.port = 0
1811
1812            if self.open_browser:
1813                # If we're bound to a UNIX socket, we can't reliably connect from a browser.
1814                self.log.info(
1815                    ("Ignoring --ServerApp.open_browser due to --sock being used."),
1816                )
1817
1818            if self.file_to_run:
1819                self.log.critical(
1820                    ("Options --ServerApp.file_to_run and --sock are mutually exclusive."),
1821                )
1822                sys.exit(1)
1823
1824            if sys.platform.startswith("win"):
1825                self.log.critical(
1826                    (
1827                        "Option --sock is not supported on Windows, but got value of %s. Aborting."
1828                        % self.sock
1829                    ),
1830                )
1831                sys.exit(1)
1832
1833        self.web_app = ServerWebApplication(
1834            self,
1835            self.default_services,
1836            self.kernel_manager,
1837            self.contents_manager,
1838            self.session_manager,
1839            self.kernel_spec_manager,
1840            self.config_manager,
1841            self.extra_services,
1842            self.log,
1843            self.base_url,
1844            self.default_url,
1845            self.tornado_settings,
1846            self.jinja_environment_options,
1847        )
1848        if self.certfile:
1849            self.ssl_options["certfile"] = self.certfile
1850        if self.keyfile:
1851            self.ssl_options["keyfile"] = self.keyfile
1852        if self.client_ca:
1853            self.ssl_options["ca_certs"] = self.client_ca
1854        if not self.ssl_options:
1855            # could be an empty dict or None
1856            # None indicates no SSL config
1857            self.ssl_options = None
1858        else:
1859            # SSL may be missing, so only import it if it's to be used
1860            import ssl
1861
1862            # PROTOCOL_TLS selects the highest ssl/tls protocol version that both the client and
1863            # server support. When PROTOCOL_TLS is not available use PROTOCOL_SSLv23.
1864            self.ssl_options.setdefault(
1865                "ssl_version", getattr(ssl, "PROTOCOL_TLS", ssl.PROTOCOL_SSLv23)
1866            )
1867            if self.ssl_options.get("ca_certs", False):
1868                self.ssl_options.setdefault("cert_reqs", ssl.CERT_REQUIRED)
1869            ssl_options = self.ssl_options
1870
1871        self.login_handler_class.validate_security(self, ssl_options=self.ssl_options)
1872
1873    def init_resources(self):
1874        """initialize system resources"""
1875        if resource is None:
1876            self.log.debug(
1877                "Ignoring min_open_files_limit because the limit cannot be adjusted (for example, on Windows)"
1878            )
1879            return
1880
1881        old_soft, old_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
1882        soft = self.min_open_files_limit
1883        hard = old_hard
1884        if old_soft < soft:
1885            if hard < soft:
1886                hard = soft
1887            self.log.debug(
1888                "Raising open file limit: soft {}->{}; hard {}->{}".format(
1889                    old_soft, soft, old_hard, hard
1890                )
1891            )
1892            resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
1893
1894    def _get_urlparts(self, path=None, include_token=False):
1895        """Constructs a urllib named tuple, ParseResult,
1896        with default values set by server config.
1897        The returned tuple can be manipulated using the `_replace` method.
1898        """
1899        if self.sock:
1900            scheme = "http+unix"
1901            netloc = urlencode_unix_socket_path(self.sock)
1902        else:
1903            # Handle nonexplicit hostname.
1904            if self.ip in ("", "0.0.0.0"):
1905                ip = "%s" % socket.gethostname()
1906            else:
1907                ip = self.ip
1908            netloc = "{ip}:{port}".format(ip=ip, port=self.port)
1909            if self.certfile:
1910                scheme = "https"
1911            else:
1912                scheme = "http"
1913        if not path:
1914            path = self.default_url
1915        query = None
1916        if include_token:
1917            if self.token:  # Don't log full token if it came from config
1918                token = self.token if self._token_generated else "..."
1919                query = urllib.parse.urlencode({"token": token})
1920        # Build the URL Parts to dump.
1921        urlparts = urllib.parse.ParseResult(
1922            scheme=scheme, netloc=netloc, path=path, params=None, query=query, fragment=None
1923        )
1924        return urlparts
1925
1926    @property
1927    def public_url(self):
1928        parts = self._get_urlparts(include_token=True)
1929        # Update with custom pieces.
1930        if self.custom_display_url:
1931            # Parse custom display_url
1932            custom = urllib.parse.urlparse(self.custom_display_url)._asdict()
1933            # Get pieces that are matter (non None)
1934            custom_updates = {key: item for key, item in custom.items() if item}
1935            # Update public URL parts with custom pieces.
1936            parts = parts._replace(**custom_updates)
1937        return parts.geturl()
1938
1939    @property
1940    def local_url(self):
1941        parts = self._get_urlparts(include_token=True)
1942        # Update with custom pieces.
1943        if not self.sock:
1944            parts = parts._replace(netloc="127.0.0.1:{port}".format(port=self.port))
1945        return parts.geturl()
1946
1947    @property
1948    def display_url(self):
1949        """Human readable string with URLs for interacting
1950        with the running Jupyter Server
1951        """
1952        url = self.public_url + "\n or " + self.local_url
1953        return url
1954
1955    @property
1956    def connection_url(self):
1957        urlparts = self._get_urlparts(path=self.base_url)
1958        return urlparts.geturl()
1959
1960    def init_terminals(self):
1961        if not self.terminals_enabled:
1962            return
1963
1964        try:
1965            from jupyter_server.terminal import initialize
1966
1967            initialize(self.web_app, self.root_dir, self.connection_url, self.terminado_settings)
1968            self.terminals_available = True
1969        except ImportError as e:
1970            self.log.warning(_i18n("Terminals not available (error was %s)"), e)
1971
1972    def init_signal(self):
1973        if not sys.platform.startswith("win") and sys.stdin and sys.stdin.isatty():
1974            signal.signal(signal.SIGINT, self._handle_sigint)
1975        signal.signal(signal.SIGTERM, self._signal_stop)
1976        if hasattr(signal, "SIGUSR1"):
1977            # Windows doesn't support SIGUSR1
1978            signal.signal(signal.SIGUSR1, self._signal_info)
1979        if hasattr(signal, "SIGINFO"):
1980            # only on BSD-based systems
1981            signal.signal(signal.SIGINFO, self._signal_info)
1982
1983    def _handle_sigint(self, sig, frame):
1984        """SIGINT handler spawns confirmation dialog"""
1985        # register more forceful signal handler for ^C^C case
1986        signal.signal(signal.SIGINT, self._signal_stop)
1987        # request confirmation dialog in bg thread, to avoid
1988        # blocking the App
1989        thread = threading.Thread(target=self._confirm_exit)
1990        thread.daemon = True
1991        thread.start()
1992
1993    def _restore_sigint_handler(self):
1994        """callback for restoring original SIGINT handler"""
1995        signal.signal(signal.SIGINT, self._handle_sigint)
1996
1997    def _confirm_exit(self):
1998        """confirm shutdown on ^C
1999
2000        A second ^C, or answering 'y' within 5s will cause shutdown,
2001        otherwise original SIGINT handler will be restored.
2002
2003        This doesn't work on Windows.
2004        """
2005        info = self.log.info
2006        info(_i18n("interrupted"))
2007        # Check if answer_yes is set
2008        if self.answer_yes:
2009            self.log.critical(_i18n("Shutting down..."))
2010            # schedule stop on the main thread,
2011            # since this might be called from a signal handler
2012            self.stop(from_signal=True)
2013            return
2014        print(self.running_server_info())
2015        yes = _i18n("y")
2016        no = _i18n("n")
2017        sys.stdout.write(_i18n("Shutdown this Jupyter server (%s/[%s])? ") % (yes, no))
2018        sys.stdout.flush()
2019        r, w, x = select.select([sys.stdin], [], [], 5)
2020        if r:
2021            line = sys.stdin.readline()
2022            if line.lower().startswith(yes) and no not in line.lower():
2023                self.log.critical(_i18n("Shutdown confirmed"))
2024                # schedule stop on the main thread,
2025                # since this might be called from a signal handler
2026                self.stop(from_signal=True)
2027                return
2028        else:
2029            print(_i18n("No answer for 5s:"), end=" ")
2030        print(_i18n("resuming operation..."))
2031        # no answer, or answer is no:
2032        # set it back to original SIGINT handler
2033        # use IOLoop.add_callback because signal.signal must be called
2034        # from main thread
2035        self.io_loop.add_callback_from_signal(self._restore_sigint_handler)
2036
2037    def _signal_stop(self, sig, frame):
2038        self.log.critical(_i18n("received signal %s, stopping"), sig)
2039        self.stop(from_signal=True)
2040
2041    def _signal_info(self, sig, frame):
2042        print(self.running_server_info())
2043
2044    def init_components(self):
2045        """Check the components submodule, and warn if it's unclean"""
2046        # TODO: this should still check, but now we use bower, not git submodule
2047        pass
2048
2049    def find_server_extensions(self):
2050        """
2051        Searches Jupyter paths for jpserver_extensions.
2052        """
2053
2054        # Walk through all config files looking for jpserver_extensions.
2055        #
2056        # Each extension will likely have a JSON config file enabling itself in
2057        # the "jupyter_server_config.d" directory. Find each of these and
2058        # merge there results in order of precedence.
2059        #
2060        # Load server extensions with ConfigManager.
2061        # This enables merging on keys, which we want for extension enabling.
2062        # Regular config loading only merges at the class level,
2063        # so each level clobbers the previous.
2064        config_paths = jupyter_config_path()
2065        if self.config_dir not in config_paths:
2066            # add self.config_dir to the front, if set manually
2067            config_paths.insert(0, self.config_dir)
2068        manager = ExtensionConfigManager(read_config_path=config_paths)
2069        extensions = manager.get_jpserver_extensions()
2070
2071        for modulename, enabled in sorted(extensions.items()):
2072            if modulename not in self.jpserver_extensions:
2073                self.config.ServerApp.jpserver_extensions.update({modulename: enabled})
2074                self.jpserver_extensions.update({modulename: enabled})
2075
2076    def init_server_extensions(self):
2077        """
2078        If an extension's metadata includes an 'app' key,
2079        the value must be a subclass of ExtensionApp. An instance
2080        of the class will be created at this step. The config for
2081        this instance will inherit the ServerApp's config object
2082        and load its own config.
2083        """
2084        # Create an instance of the ExtensionManager.
2085        self.extension_manager = ExtensionManager(log=self.log, serverapp=self)
2086        self.extension_manager.from_jpserver_extensions(self.jpserver_extensions)
2087        self.extension_manager.link_all_extensions()
2088
2089    def load_server_extensions(self):
2090        """Load any extensions specified by config.
2091
2092        Import the module, then call the load_jupyter_server_extension function,
2093        if one exists.
2094
2095        The extension API is experimental, and may change in future releases.
2096        """
2097        self.extension_manager.load_all_extensions()
2098
2099    def init_mime_overrides(self):
2100        # On some Windows machines, an application has registered incorrect
2101        # mimetypes in the registry.
2102        # Tornado uses this when serving .css and .js files, causing browsers to
2103        # reject these files. We know the mimetype always needs to be text/css for css
2104        # and application/javascript for JS, so we override it here
2105        # and explicitly tell the mimetypes to not trust the Windows registry
2106        if os.name == "nt":
2107            # do not trust windows registry, which regularly has bad info
2108            mimetypes.init(files=[])
2109        # ensure css, js are correct, which are required for pages to function
2110        mimetypes.add_type("text/css", ".css")
2111        mimetypes.add_type("application/javascript", ".js")
2112        # for python <3.8
2113        mimetypes.add_type("application/wasm", ".wasm")
2114
2115    def shutdown_no_activity(self):
2116        """Shutdown server on timeout when there are no kernels or terminals."""
2117        km = self.kernel_manager
2118        if len(km) != 0:
2119            return  # Kernels still running
2120
2121        if self.terminals_available:
2122            term_mgr = self.web_app.settings["terminal_manager"]
2123            if term_mgr.terminals:
2124                return  # Terminals still running
2125
2126        seconds_since_active = (utcnow() - self.web_app.last_activity()).total_seconds()
2127        self.log.debug("No activity for %d seconds.", seconds_since_active)
2128        if seconds_since_active > self.shutdown_no_activity_timeout:
2129            self.log.info(
2130                "No kernels or terminals for %d seconds; shutting down.", seconds_since_active
2131            )
2132            self.stop()
2133
2134    def init_shutdown_no_activity(self):
2135        if self.shutdown_no_activity_timeout > 0:
2136            self.log.info(
2137                "Will shut down after %d seconds with no kernels or terminals.",
2138                self.shutdown_no_activity_timeout,
2139            )
2140            pc = ioloop.PeriodicCallback(self.shutdown_no_activity, 60000)
2141            pc.start()
2142
2143    @property
2144    def http_server(self):
2145        """An instance of Tornado's HTTPServer class for the Server Web Application."""
2146        try:
2147            return self._http_server
2148        except AttributeError as e:
2149            raise AttributeError(
2150                "An HTTPServer instance has not been created for the "
2151                "Server Web Application. To create an HTTPServer for this "
2152                "application, call `.init_httpserver()`."
2153            ) from e
2154
2155    def init_httpserver(self):
2156        """Creates an instance of a Tornado HTTPServer for the Server Web Application
2157        and sets the http_server attribute.
2158        """
2159        # Check that a web_app has been initialized before starting a server.
2160        if not hasattr(self, "web_app"):
2161            raise AttributeError(
2162                "A tornado web application has not be initialized. "
2163                "Try calling `.init_webapp()` first."
2164            )
2165
2166        # Create an instance of the server.
2167        self._http_server = httpserver.HTTPServer(
2168            self.web_app,
2169            ssl_options=self.ssl_options,
2170            xheaders=self.trust_xheaders,
2171            max_body_size=self.max_body_size,
2172            max_buffer_size=self.max_buffer_size,
2173        )
2174
2175        success = self._bind_http_server()
2176        if not success:
2177            self.log.critical(
2178                _i18n(
2179                    "ERROR: the Jupyter server could not be started because "
2180                    "no available port could be found."
2181                )
2182            )
2183            self.exit(1)
2184
2185    def _bind_http_server(self):
2186        return self._bind_http_server_unix() if self.sock else self._bind_http_server_tcp()
2187
2188    def _bind_http_server_unix(self):
2189        if unix_socket_in_use(self.sock):
2190            self.log.warning(_i18n("The socket %s is already in use.") % self.sock)
2191            return False
2192
2193        try:
2194            sock = bind_unix_socket(self.sock, mode=int(self.sock_mode.encode(), 8))
2195            self.http_server.add_socket(sock)
2196        except socket.error as e:
2197            if e.errno == errno.EADDRINUSE:
2198                self.log.warning(_i18n("The socket %s is already in use.") % self.sock)
2199                return False
2200            elif e.errno in (errno.EACCES, getattr(errno, "WSAEACCES", errno.EACCES)):
2201                self.log.warning(_i18n("Permission to listen on sock %s denied") % self.sock)
2202                return False
2203            else:
2204                raise
2205        else:
2206            return True
2207
2208    def _bind_http_server_tcp(self):
2209        success = None
2210        for port in random_ports(self.port, self.port_retries + 1):
2211            try:
2212                self.http_server.listen(port, self.ip)
2213            except socket.error as e:
2214                if e.errno == errno.EADDRINUSE:
2215                    if self.port_retries:
2216                        self.log.info(
2217                            _i18n("The port %i is already in use, trying another port.") % port
2218                        )
2219                    else:
2220                        self.log.info(_i18n("The port %i is already in use.") % port)
2221                    continue
2222                elif e.errno in (errno.EACCES, getattr(errno, "WSAEACCES", errno.EACCES)):
2223                    self.log.warning(_i18n("Permission to listen on port %i denied.") % port)
2224                    continue
2225                else:
2226                    raise
2227            else:
2228                self.port = port
2229                success = True
2230                break
2231        if not success:
2232            if self.port_retries:
2233                self.log.critical(
2234                    _i18n(
2235                        "ERROR: the Jupyter server could not be started because "
2236                        "no available port could be found."
2237                    )
2238                )
2239            else:
2240                self.log.critical(
2241                    _i18n(
2242                        "ERROR: the Jupyter server could not be started because "
2243                        "port %i is not available."
2244                    )
2245                    % port
2246                )
2247            self.exit(1)
2248        return success
2249
2250    @staticmethod
2251    def _init_asyncio_patch():
2252        """set default asyncio policy to be compatible with tornado
2253
2254        Tornado 6.0 is not compatible with default asyncio
2255        ProactorEventLoop, which lacks basic *_reader methods.
2256        Tornado 6.1 adds a workaround to add these methods in a thread,
2257        but SelectorEventLoop should still be preferred
2258        to avoid the extra thread for ~all of our events,
2259        at least until asyncio adds *_reader methods
2260        to proactor.
2261        """
2262        if sys.platform.startswith("win") and sys.version_info >= (3, 8):
2263            import asyncio
2264
2265            try:
2266                from asyncio import (
2267                    WindowsProactorEventLoopPolicy,
2268                    WindowsSelectorEventLoopPolicy,
2269                )
2270            except ImportError:
2271                pass
2272                # not affected
2273            else:
2274                if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
2275                    # prefer Selector to Proactor for tornado + pyzmq
2276                    asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
2277
2278    @catch_config_error
2279    def initialize(
2280        self, argv=None, find_extensions=True, new_httpserver=True, starter_extension=None
2281    ):
2282        """Initialize the Server application class, configurables, web application, and http server.
2283
2284        Parameters
2285        ----------
2286        argv : list or None
2287            CLI arguments to parse.
2288        find_extensions : bool
2289            If True, find and load extensions listed in Jupyter config paths. If False,
2290            only load extensions that are passed to ServerApp directy through
2291            the `argv`, `config`, or `jpserver_extensions` arguments.
2292        new_httpserver : bool
2293            If True, a tornado HTTPServer instance will be created and configured for the Server Web
2294            Application. This will set the http_server attribute of this class.
2295        starter_extension : str
2296            If given, it references the name of an extension point that started the Server.
2297            We will try to load configuration from extension point
2298        """
2299        self._init_asyncio_patch()
2300        # Parse command line, load ServerApp config files,
2301        # and update ServerApp config.
2302        super(ServerApp, self).initialize(argv=argv)
2303        if self._dispatching:
2304            return
2305        # Then, use extensions' config loading mechanism to
2306        # update config. ServerApp config takes precedence.
2307        if find_extensions:
2308            self.find_server_extensions()
2309        self.init_logging()
2310        self.init_server_extensions()
2311
2312        # Special case the starter extension and load
2313        # any server configuration is provides.
2314        if starter_extension:
2315            # Configure ServerApp based on named extension.
2316            point = self.extension_manager.extension_points[starter_extension]
2317            # Set starter_app property.
2318            if point.app:
2319                self._starter_app = point.app
2320            # Load any configuration that comes from the Extension point.
2321            self.update_config(Config(point.config))
2322
2323        # Initialize other pieces of the server.
2324        self.init_resources()
2325        self.init_configurables()
2326        self.init_components()
2327        self.init_webapp()
2328        self.init_terminals()
2329        self.init_signal()
2330        self.init_ioloop()
2331        self.load_server_extensions()
2332        self.init_mime_overrides()
2333        self.init_shutdown_no_activity()
2334        if new_httpserver:
2335            self.init_httpserver()
2336
2337    async def cleanup_kernels(self):
2338        """Shutdown all kernels.
2339
2340        The kernels will shutdown themselves when this process no longer exists,
2341        but explicit shutdown allows the KernelManagers to cleanup the connection files.
2342        """
2343        n_kernels = len(self.kernel_manager.list_kernel_ids())
2344        kernel_msg = trans.ngettext(
2345            "Shutting down %d kernel", "Shutting down %d kernels", n_kernels
2346        )
2347        self.log.info(kernel_msg % n_kernels)
2348        await run_sync_in_loop(self.kernel_manager.shutdown_all())
2349
2350    async def cleanup_terminals(self):
2351        """Shutdown all terminals.
2352
2353        The terminals will shutdown themselves when this process no longer exists,
2354        but explicit shutdown allows the TerminalManager to cleanup.
2355        """
2356        if not self.terminals_available:
2357            return
2358
2359        terminal_manager = self.web_app.settings["terminal_manager"]
2360        n_terminals = len(terminal_manager.list())
2361        terminal_msg = trans.ngettext(
2362            "Shutting down %d terminal", "Shutting down %d terminals", n_terminals
2363        )
2364        self.log.info(terminal_msg % n_terminals)
2365        await run_sync_in_loop(terminal_manager.terminate_all())
2366
2367    async def cleanup_extensions(self):
2368        """Call shutdown hooks in all extensions."""
2369        n_extensions = len(self.extension_manager.extension_apps)
2370        extension_msg = trans.ngettext(
2371            "Shutting down %d extension", "Shutting down %d extensions", n_extensions
2372        )
2373        self.log.info(extension_msg % n_extensions)
2374        await run_sync_in_loop(self.extension_manager.stop_all_extensions())
2375
2376    def running_server_info(self, kernel_count=True):
2377        "Return the current working directory and the server url information"
2378        info = self.contents_manager.info_string() + "\n"
2379        if kernel_count:
2380            n_kernels = len(self.kernel_manager.list_kernel_ids())
2381            kernel_msg = trans.ngettext("%d active kernel", "%d active kernels", n_kernels)
2382            info += kernel_msg % n_kernels
2383            info += "\n"
2384        # Format the info so that the URL fits on a single line in 80 char display
2385        info += _i18n(
2386            "Jupyter Server {version} is running at:\n{url}".format(
2387                version=ServerApp.version, url=self.display_url
2388            )
2389        )
2390        if self.gateway_config.gateway_enabled:
2391            info += (
2392                _i18n("\nKernels will be managed by the Gateway server running at:\n%s")
2393                % self.gateway_config.url
2394            )
2395        return info
2396
2397    def server_info(self):
2398        """Return a JSONable dict of information about this server."""
2399        return {
2400            "url": self.connection_url,
2401            "hostname": self.ip if self.ip else "localhost",
2402            "port": self.port,
2403            "sock": self.sock,
2404            "secure": bool(self.certfile),
2405            "base_url": self.base_url,
2406            "token": self.token,
2407            "root_dir": os.path.abspath(self.root_dir),
2408            "password": bool(self.password),
2409            "pid": os.getpid(),
2410            "version": ServerApp.version,
2411        }
2412
2413    def write_server_info_file(self):
2414        """Write the result of server_info() to the JSON file info_file."""
2415        try:
2416            with secure_write(self.info_file) as f:
2417                json.dump(self.server_info(), f, indent=2, sort_keys=True)
2418        except OSError as e:
2419            self.log.error(_i18n("Failed to write server-info to %s: %s"), self.info_file, e)
2420
2421    def remove_server_info_file(self):
2422        """Remove the jpserver-<pid>.json file created for this server.
2423
2424        Ignores the error raised when the file has already been removed.
2425        """
2426        try:
2427            os.unlink(self.info_file)
2428        except OSError as e:
2429            if e.errno != errno.ENOENT:
2430                raise
2431
2432    def _resolve_file_to_run_and_root_dir(self):
2433        """Returns a relative path from file_to_run
2434        to root_dir. If root_dir and file_to_run
2435        are incompatible, i.e. on different subtrees,
2436        crash the app and log a critical message. Note
2437        that if root_dir is not configured and file_to_run
2438        is configured, root_dir will be set to the parent
2439        directory of file_to_run.
2440        """
2441        rootdir_abspath = pathlib.Path(self.root_dir).resolve()
2442        file_rawpath = pathlib.Path(self.file_to_run)
2443        combined_path = (rootdir_abspath / file_rawpath).resolve()
2444        is_child = str(combined_path).startswith(str(rootdir_abspath))
2445
2446        if is_child:
2447            if combined_path.parent != rootdir_abspath:
2448                self.log.debug(
2449                    "The `root_dir` trait is set to a directory that's not "
2450                    "the immediate parent directory of `file_to_run`. Note that "
2451                    "the server will start at `root_dir` and open the "
2452                    "the file from the relative path to the `root_dir`."
2453                )
2454            return str(combined_path.relative_to(rootdir_abspath))
2455
2456        self.log.critical(
2457            "`root_dir` and `file_to_run` are incompatible. They "
2458            "don't share the same subtrees. Make sure `file_to_run` "
2459            "is on the same path as `root_dir`."
2460        )
2461        self.exit(1)
2462
2463    def _write_browser_open_file(self, url, fh):
2464        if self.token:
2465            url = url_concat(url, {"token": self.token})
2466        url = url_path_join(self.connection_url, url)
2467
2468        jinja2_env = self.web_app.settings["jinja2_env"]
2469        template = jinja2_env.get_template("browser-open.html")
2470        fh.write(template.render(open_url=url, base_url=self.base_url))
2471
2472    def write_browser_open_files(self):
2473        """Write an `browser_open_file` and `browser_open_file_to_run` files
2474
2475        This can be used to open a file directly in a browser.
2476        """
2477        # default_url contains base_url, but so does connection_url
2478        self.write_browser_open_file()
2479
2480        # Create a second browser open file if
2481        # file_to_run is set.
2482        if self.file_to_run:
2483            # Make sure file_to_run and root_dir are compatible.
2484            file_to_run_relpath = self._resolve_file_to_run_and_root_dir()
2485
2486            file_open_url = url_escape(
2487                url_path_join(self.file_url_prefix, *file_to_run_relpath.split(os.sep))
2488            )
2489
2490            with open(self.browser_open_file_to_run, "w", encoding="utf-8") as f:
2491                self._write_browser_open_file(file_open_url, f)
2492
2493    def write_browser_open_file(self):
2494        """Write an jpserver-<pid>-open.html file
2495
2496        This can be used to open the notebook in a browser
2497        """
2498        # default_url contains base_url, but so does connection_url
2499        open_url = self.default_url[len(self.base_url) :]
2500
2501        with open(self.browser_open_file, "w", encoding="utf-8") as f:
2502            self._write_browser_open_file(open_url, f)
2503
2504    def remove_browser_open_files(self):
2505        """Remove the `browser_open_file` and `browser_open_file_to_run` files
2506        created for this server.
2507
2508        Ignores the error raised when the file has already been removed.
2509        """
2510        self.remove_browser_open_file()
2511        try:
2512            os.unlink(self.browser_open_file_to_run)
2513        except OSError as e:
2514            if e.errno != errno.ENOENT:
2515                raise
2516
2517    def remove_browser_open_file(self):
2518        """Remove the jpserver-<pid>-open.html file created for this server.
2519
2520        Ignores the error raised when the file has already been removed.
2521        """
2522        try:
2523            os.unlink(self.browser_open_file)
2524        except OSError as e:
2525            if e.errno != errno.ENOENT:
2526                raise
2527
2528    def _prepare_browser_open(self):
2529        if not self.use_redirect_file:
2530            uri = self.default_url[len(self.base_url) :]
2531
2532            if self.token:
2533                uri = url_concat(uri, {"token": self.token})
2534
2535        if self.file_to_run:
2536            # Create a separate, temporary open-browser-file
2537            # pointing at a specific file.
2538            open_file = self.browser_open_file_to_run
2539        else:
2540            # otherwise, just return the usual open browser file.
2541            open_file = self.browser_open_file
2542
2543        if self.use_redirect_file:
2544            assembled_url = urljoin("file:", pathname2url(open_file))
2545        else:
2546            assembled_url = url_path_join(self.connection_url, uri)
2547
2548        return assembled_url, open_file
2549
2550    def launch_browser(self):
2551        try:
2552            browser = webbrowser.get(self.browser or None)
2553        except webbrowser.Error as e:
2554            self.log.warning(_i18n("No web browser found: %s.") % e)
2555            browser = None
2556
2557        if not browser:
2558            return
2559
2560        assembled_url, _ = self._prepare_browser_open()
2561
2562        b = lambda: browser.open(assembled_url, new=self.webbrowser_open_new)
2563        threading.Thread(target=b).start()
2564
2565    def start_app(self):
2566        super(ServerApp, self).start()
2567
2568        if not self.allow_root:
2569            # check if we are running as root, and abort if it's not allowed
2570            try:
2571                uid = os.geteuid()
2572            except AttributeError:
2573                uid = -1  # anything nonzero here, since we can't check UID assume non-root
2574            if uid == 0:
2575                self.log.critical(
2576                    _i18n("Running as root is not recommended. Use --allow-root to bypass.")
2577                )
2578                self.exit(1)
2579
2580        info = self.log.info
2581        for line in self.running_server_info(kernel_count=False).split("\n"):
2582            info(line)
2583        info(
2584            _i18n(
2585                "Use Control-C to stop this server and shut down all kernels (twice to skip confirmation)."
2586            )
2587        )
2588        if "dev" in __version__:
2589            info(
2590                _i18n(
2591                    "Welcome to Project Jupyter! Explore the various tools available"
2592                    " and their corresponding documentation. If you are interested"
2593                    " in contributing to the platform, please visit the community"
2594                    " resources section at https://jupyter.org/community.html."
2595                )
2596            )
2597
2598        self.write_server_info_file()
2599        self.write_browser_open_files()
2600
2601        # Handle the browser opening.
2602        if self.open_browser and not self.sock:
2603            self.launch_browser()
2604
2605        if self.token and self._token_generated:
2606            # log full URL with generated token, so there's a copy/pasteable link
2607            # with auth info.
2608            if self.sock:
2609                self.log.critical(
2610                    "\n".join(
2611                        [
2612                            "\n",
2613                            "Jupyter Server is listening on %s" % self.display_url,
2614                            "",
2615                            (
2616                                "UNIX sockets are not browser-connectable, but you can tunnel to "
2617                                "the instance via e.g.`ssh -L 8888:%s -N user@this_host` and then "
2618                                "open e.g. %s in a browser."
2619                            )
2620                            % (self.sock, self.connection_url),
2621                        ]
2622                    )
2623                )
2624            else:
2625                self.log.critical(
2626                    "\n".join(
2627                        [
2628                            "\n",
2629                            "To access the server, open this file in a browser:",
2630                            "    %s" % urljoin("file:", pathname2url(self.browser_open_file)),
2631                            "Or copy and paste one of these URLs:",
2632                            "    %s" % self.display_url,
2633                        ]
2634                    )
2635                )
2636
2637    async def _cleanup(self):
2638        """General cleanup of files, extensions and kernels created
2639        by this instance ServerApp.
2640        """
2641        self.remove_server_info_file()
2642        self.remove_browser_open_files()
2643        await self.cleanup_extensions()
2644        await self.cleanup_kernels()
2645        await self.cleanup_terminals()
2646
2647    def start_ioloop(self):
2648        """Start the IO Loop."""
2649        if sys.platform.startswith("win"):
2650            # add no-op to wake every 5s
2651            # to handle signals that may be ignored by the inner loop
2652            pc = ioloop.PeriodicCallback(lambda: None, 5000)
2653            pc.start()
2654        try:
2655            self.io_loop.start()
2656        except KeyboardInterrupt:
2657            self.log.info(_i18n("Interrupted..."))
2658
2659    def init_ioloop(self):
2660        """init self.io_loop so that an extension can use it by io_loop.call_later() to create background tasks"""
2661        self.io_loop = ioloop.IOLoop.current()
2662
2663    def start(self):
2664        """Start the Jupyter server app, after initialization
2665
2666        This method takes no arguments so all configuration and initialization
2667        must be done prior to calling this method."""
2668        self.start_app()
2669        self.start_ioloop()
2670
2671    async def _stop(self):
2672        """Cleanup resources and stop the IO Loop."""
2673        await self._cleanup()
2674        self.io_loop.stop()
2675
2676    def stop(self, from_signal=False):
2677        """Cleanup resources and stop the server."""
2678        if hasattr(self, "_http_server"):
2679            # Stop a server if its set.
2680            self.http_server.stop()
2681        if getattr(self, "io_loop", None):
2682            # use IOLoop.add_callback because signal.signal must be called
2683            # from main thread
2684            if from_signal:
2685                self.io_loop.add_callback_from_signal(self._stop)
2686            else:
2687                self.io_loop.add_callback(self._stop)
2688
2689
2690def list_running_servers(runtime_dir=None, log=None):
2691    """Iterate over the server info files of running Jupyter servers.
2692
2693    Given a runtime directory, find jpserver-* files in the security directory,
2694    and yield dicts of their information, each one pertaining to
2695    a currently running Jupyter server instance.
2696    """
2697    if runtime_dir is None:
2698        runtime_dir = jupyter_runtime_dir()
2699
2700    # The runtime dir might not exist
2701    if not os.path.isdir(runtime_dir):
2702        return
2703
2704    for file_name in os.listdir(runtime_dir):
2705        if re.match("jpserver-(.+).json", file_name):
2706            with io.open(os.path.join(runtime_dir, file_name), encoding="utf-8") as f:
2707                info = json.load(f)
2708
2709            # Simple check whether that process is really still running
2710            # Also remove leftover files from IPython 2.x without a pid field
2711            if ("pid" in info) and check_pid(info["pid"]):
2712                yield info
2713            else:
2714                # If the process has died, try to delete its info file
2715                try:
2716                    os.unlink(os.path.join(runtime_dir, file_name))
2717                except OSError as e:
2718                    if log:
2719                        log.warning(_i18n("Deleting server info file failed: %s.") % e)
2720
2721
2722# -----------------------------------------------------------------------------
2723# Main entry point
2724# -----------------------------------------------------------------------------
2725
2726main = launch_new_instance = ServerApp.launch_instance
2727