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