notebookapp.py
958 lines
| 36.4 KiB
| text/x-python
|
PythonLexer
|
r6067 | # coding: utf-8 | ||
|
r16374 | """A tornado based IPython notebook server.""" | ||
|
r4609 | |||
|
r16374 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r4609 | |||
|
r13348 | from __future__ import print_function | ||
|
r16375 | |||
|
r17141 | import base64 | ||
|
r4548 | import errno | ||
|
r14063 | import io | ||
import json | ||||
|
r4339 | import logging | ||
import os | ||||
|
r7234 | import random | ||
|
r17106 | import re | ||
|
r6508 | import select | ||
|
r4345 | import signal | ||
|
r4548 | import socket | ||
|
r4345 | import sys | ||
|
r5212 | import threading | ||
|
r6509 | import time | ||
|
r5065 | import webbrowser | ||
|
r4339 | |||
|
r10199 | |||
|
r10201 | # check for pyzmq 2.1.11 | ||
from IPython.utils.zmqrelated import check_for_zmq | ||||
|
r11035 | check_for_zmq('2.1.11', 'IPython.html') | ||
|
r10199 | |||
|
r8840 | from jinja2 import Environment, FileSystemLoader | ||
|
r4339 | |||
# Install the pyzmq ioloop. This has to be done before anything else from | ||||
# tornado is imported. | ||||
from zmq.eventloop import ioloop | ||||
|
r6631 | ioloop.install() | ||
|
r4339 | |||
|
r13313 | # check for tornado 3.1.0 | ||
msg = "The IPython Notebook requires tornado >= 3.1.0" | ||||
|
r10199 | try: | ||
import tornado | ||||
except ImportError: | ||||
raise ImportError(msg) | ||||
try: | ||||
version_info = tornado.version_info | ||||
except AttributeError: | ||||
raise ImportError(msg + ", but you have < 1.1.0") | ||||
|
r13313 | if version_info < (3,1,0): | ||
|
r10199 | raise ImportError(msg + ", but you have %s" % tornado.version) | ||
|
r4339 | from tornado import httpserver | ||
from tornado import web | ||||
|
r18117 | from tornado.log import LogFormatter, app_log, access_log, gen_log | ||
|
r4339 | |||
|
r11035 | from IPython.html import DEFAULT_STATIC_FILES_PATH | ||
|
r13939 | from .base.handlers import Template404 | ||
|
r14645 | from .log import log_request | ||
|
r10666 | from .services.kernels.kernelmanager import MappingKernelManager | ||
|
r17524 | from .services.contents.manager import ContentsManager | ||
from .services.contents.filemanager import FileContentsManager | ||||
|
r10666 | from .services.clusters.clustermanager import ClusterManager | ||
|
r12982 | from .services.sessions.sessionmanager import SessionManager | ||
|
r10642 | |||
|
r10650 | from .base.handlers import AuthenticatedFileHandler, FileFindHandler | ||
|
r4339 | |||
|
r16041 | from IPython.config import Config | ||
|
r5762 | from IPython.config.application import catch_config_error, boolean_flag | ||
|
r16375 | from IPython.core.application import ( | ||
BaseIPythonApplication, base_flags, base_aliases, | ||||
|
r4345 | ) | ||
|
r16375 | from IPython.core.profiledir import ProfileDir | ||
from IPython.kernel import KernelManager | ||||
|
r16684 | from IPython.kernel.kernelspec import KernelSpecManager | ||
|
r16375 | from IPython.kernel.zmq.session import default_secure, Session | ||
|
r15791 | from IPython.nbformat.sign import NotebookNotary | ||
|
r8180 | from IPython.utils.importstring import import_item | ||
|
r10557 | from IPython.utils import submodule | ||
|
r17181 | from IPython.utils.process import check_pid | ||
|
r8180 | from IPython.utils.traitlets import ( | ||
|
r16684 | Dict, Unicode, Integer, List, Bool, Bytes, Instance, | ||
|
r15420 | DottedObjectName, TraitError, | ||
|
r8180 | ) | ||
|
r6067 | from IPython.utils import py3compat | ||
|
r12800 | from IPython.utils.path import filefind, get_ipython_dir | ||
|
r4344 | |||
|
r10642 | from .utils import url_path_join | ||
|
r4344 | #----------------------------------------------------------------------------- | ||
# Module globals | ||||
#----------------------------------------------------------------------------- | ||||
|
r4339 | |||
|
r4519 | _examples = """ | ||
ipython notebook # start the notebook | ||||
ipython notebook --profile=sympy # use the sympy profile | ||||
ipython notebook --certfile=mycert.pem # use SSL/TLS certificate | ||||
""" | ||||
|
r4344 | #----------------------------------------------------------------------------- | ||
|
r6004 | # Helper functions | ||
#----------------------------------------------------------------------------- | ||||
|
r7234 | def random_ports(port, n): | ||
"""Generate a list of n random ports near the given port. | ||||
The first 5 ports will be sequential, and the remaining n-5 will be | ||||
randomly selected in the range [port-2*n, port+2*n]. | ||||
""" | ||||
for i in range(min(5, n)): | ||||
yield port + i | ||||
for i in range(n-5): | ||||
|
r12843 | yield max(1, port + random.randint(-2*n, 2*n)) | ||
|
r7234 | |||
|
r10647 | def load_handlers(name): | ||
"""Load the (URL pattern, handler) tuples for each component.""" | ||||
|
r11035 | name = 'IPython.html.' + name | ||
|
r10647 | mod = __import__(name, fromlist=['default_handlers']) | ||
return mod.default_handlers | ||||
|
r6004 | #----------------------------------------------------------------------------- | ||
|
r4344 | # The Tornado web application | ||
#----------------------------------------------------------------------------- | ||||
|
r4339 | |||
|
r4342 | class NotebookWebApplication(web.Application): | ||
|
r4339 | |||
|
r17524 | def __init__(self, ipython_app, kernel_manager, contents_manager, | ||
|
r16684 | cluster_manager, session_manager, kernel_spec_manager, log, | ||
|
r17778 | base_url, default_url, settings_overrides, jinja_env_options): | ||
|
r10647 | |||
|
r10656 | settings = self.init_settings( | ||
|
r17524 | ipython_app, kernel_manager, contents_manager, cluster_manager, | ||
|
r17778 | session_manager, kernel_spec_manager, log, base_url, default_url, | ||
|
r16684 | settings_overrides, jinja_env_options) | ||
|
r10656 | handlers = self.init_handlers(settings) | ||
super(NotebookWebApplication, self).__init__(handlers, **settings) | ||||
|
r5664 | |||
|
r17524 | def init_settings(self, ipython_app, kernel_manager, contents_manager, | ||
|
r16684 | cluster_manager, session_manager, kernel_spec_manager, | ||
|
r17778 | log, base_url, default_url, settings_overrides, | ||
|
r16684 | jinja_env_options=None): | ||
|
r18038 | |||
|
r17896 | _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates")) | ||
if isinstance(_template_path, str): | ||||
_template_path = (_template_path,) | ||||
template_path = [os.path.expanduser(path) for path in _template_path] | ||||
|
r15449 | jenv_opt = jinja_env_options if jinja_env_options else {} | ||
|
r17896 | env = Environment(loader=FileSystemLoader(template_path), **jenv_opt) | ||
|
r7797 | settings = dict( | ||
|
r10355 | # basics | ||
|
r14645 | log_function=log_request, | ||
|
r15238 | base_url=base_url, | ||
|
r17778 | default_url=default_url, | ||
|
r10355 | template_path=template_path, | ||
|
r7930 | static_path=ipython_app.static_file_path, | ||
|
r7923 | static_handler_class = FileFindHandler, | ||
|
r15238 | static_url_prefix = url_path_join(base_url,'/static/'), | ||
|
r10355 | |||
# authentication | ||||
|
r10784 | cookie_secret=ipython_app.cookie_secret, | ||
|
r15238 | login_url=url_path_join(base_url,'/login'), | ||
|
r10355 | password=ipython_app.password, | ||
# managers | ||||
kernel_manager=kernel_manager, | ||||
|
r17524 | contents_manager=contents_manager, | ||
|
r10355 | cluster_manager=cluster_manager, | ||
|
r12982 | session_manager=session_manager, | ||
|
r16684 | kernel_spec_manager=kernel_spec_manager, | ||
|
r12982 | |||
|
r10355 | # IPython stuff | ||
|
r12811 | nbextensions_path = ipython_app.nbextensions_path, | ||
|
r17303 | websocket_url=ipython_app.websocket_url, | ||
|
r10388 | mathjax_url=ipython_app.mathjax_url, | ||
|
r10355 | config=ipython_app.config, | ||
|
r15449 | jinja2_env=env, | ||
|
r7797 | ) | ||
# allow custom overrides for the tornado web app. | ||||
settings.update(settings_overrides) | ||||
|
r10656 | return settings | ||
|
r7797 | |||
|
r10656 | def init_handlers(self, settings): | ||
# Load the (URL pattern, handler) tuples for each component. | ||||
handlers = [] | ||||
handlers.extend(load_handlers('base.handlers')) | ||||
handlers.extend(load_handlers('tree.handlers')) | ||||
handlers.extend(load_handlers('auth.login')) | ||||
handlers.extend(load_handlers('auth.logout')) | ||||
|
r18363 | handlers.extend(load_handlers('files.handlers')) | ||
|
r10665 | handlers.extend(load_handlers('notebook.handlers')) | ||
|
r13827 | handlers.extend(load_handlers('nbconvert.handlers')) | ||
|
r16706 | handlers.extend(load_handlers('kernelspecs.handlers')) | ||
|
r10665 | handlers.extend(load_handlers('services.kernels.handlers')) | ||
|
r17524 | handlers.extend(load_handlers('services.contents.handlers')) | ||
|
r10665 | handlers.extend(load_handlers('services.clusters.handlers')) | ||
|
r12982 | handlers.extend(load_handlers('services.sessions.handlers')) | ||
|
r13837 | handlers.extend(load_handlers('services.nbconvert.handlers')) | ||
|
r16684 | handlers.extend(load_handlers('services.kernelspecs.handlers')) | ||
|
r18138 | handlers.append( | ||
|
r12811 | (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}), | ||
|
r17524 | ) | ||
|
r17778 | # set the URL that will be redirected from `/` | ||
handlers.append( | ||||
(r'/?', web.RedirectHandler, { | ||||
'url' : url_path_join(settings['base_url'], settings['default_url']), | ||||
'permanent': False, # want 302, not 301 | ||||
}) | ||||
) | ||||
|
r15238 | # prepend base_url onto the patterns that we match | ||
|
r6004 | new_handlers = [] | ||
for handler in handlers: | ||||
|
r15238 | pattern = url_path_join(settings['base_url'], handler[0]) | ||
|
r10355 | new_handler = tuple([pattern] + list(handler[1:])) | ||
new_handlers.append(new_handler) | ||||
|
r13939 | # add 404 on the end, which will catch everything that falls through | ||
new_handlers.append((r'(.*)', Template404)) | ||||
|
r10656 | return new_handlers | ||
|
r4339 | |||
|
r14177 | class NbserverListApp(BaseIPythonApplication): | ||
description="List currently running notebook servers in this profile." | ||||
flags = dict( | ||||
json=({'NbserverListApp': {'json': True}}, | ||||
"Produce machine-readable JSON output."), | ||||
) | ||||
json = Bool(False, config=True, | ||||
help="If True, each line of output will be a JSON object with the " | ||||
"details from the server info file.") | ||||
def start(self): | ||||
if not self.json: | ||||
print("Currently running servers:") | ||||
|
r14179 | for serverinfo in list_running_servers(self.profile): | ||
|
r14177 | if self.json: | ||
print(json.dumps(serverinfo)) | ||||
else: | ||||
|
r14179 | print(serverinfo['url'], "::", serverinfo['notebook_dir']) | ||
|
r4346 | |||
|
r4344 | #----------------------------------------------------------------------------- | ||
# Aliases and Flags | ||||
#----------------------------------------------------------------------------- | ||||
|
r16375 | flags = dict(base_flags) | ||
|
r5065 | flags['no-browser']=( | ||
|
r5128 | {'NotebookApp' : {'open_browser' : False}}, | ||
|
r5065 | "Don't open the notebook in a browser after startup." | ||
) | ||||
|
r16375 | flags['pylab']=( | ||
{'NotebookApp' : {'pylab' : 'warn'}}, | ||||
"DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib." | ||||
) | ||||
|
r5547 | flags['no-mathjax']=( | ||
{'NotebookApp' : {'enable_mathjax' : False}}, | ||||
"""Disable MathJax | ||||
MathJax is the javascript library IPython uses to render math/LaTeX. It is | ||||
very large, so you may want to disable it if you have a slow internet | ||||
connection, or for offline use of the notebook. | ||||
When disabled, equations etc. will appear as their untransformed TeX source. | ||||
""" | ||||
) | ||||
|
r4344 | |||
|
r17537 | # Add notebook manager flags | ||
flags.update(boolean_flag('script', 'FileContentsManager.save_script', | ||||
'DEPRECATED, IGNORED', | ||||
'DEPRECATED, IGNORED')) | ||||
|
r16375 | aliases = dict(base_aliases) | ||
|
r4344 | |||
|
r4519 | aliases.update({ | ||
|
r5104 | 'ip': 'NotebookApp.ip', | ||
'port': 'NotebookApp.port', | ||||
|
r7234 | 'port-retries': 'NotebookApp.port_retries', | ||
|
r9177 | 'transport': 'KernelManager.transport', | ||
|
r5104 | 'keyfile': 'NotebookApp.keyfile', | ||
'certfile': 'NotebookApp.certfile', | ||||
|
r15420 | 'notebook-dir': 'NotebookApp.notebook_dir', | ||
|
r6116 | 'browser': 'NotebookApp.browser', | ||
|
r16375 | 'pylab': 'NotebookApp.pylab', | ||
|
r4519 | }) | ||
|
r4344 | |||
#----------------------------------------------------------------------------- | ||||
|
r5104 | # NotebookApp | ||
|
r4344 | #----------------------------------------------------------------------------- | ||
|
r5104 | class NotebookApp(BaseIPythonApplication): | ||
|
r4519 | |||
|
r4344 | name = 'ipython-notebook' | ||
description = """ | ||||
The IPython HTML Notebook. | ||||
This launches a Tornado based HTML Notebook Server that serves up an | ||||
HTML5/Javascript Notebook client. | ||||
""" | ||||
|
r4519 | examples = _examples | ||
|
r16375 | aliases = aliases | ||
flags = flags | ||||
|
r4344 | |||
|
r16375 | classes = [ | ||
KernelManager, ProfileDir, Session, MappingKernelManager, | ||||
|
r17524 | ContentsManager, FileContentsManager, NotebookNotary, | ||
|
r16375 | ] | ||
|
r4344 | flags = Dict(flags) | ||
aliases = Dict(aliases) | ||||
|
r14177 | |||
subcommands = dict( | ||||
list=(NbserverListApp, NbserverListApp.description.splitlines()[0]), | ||||
) | ||||
|
r4344 | |||
kernel_argv = List(Unicode) | ||||
|
r16358 | |||
_log_formatter_cls = LogFormatter | ||||
|
r4344 | |||
|
r10355 | def _log_level_default(self): | ||
return logging.INFO | ||||
|
r4345 | |||
|
r16358 | def _log_datefmt_default(self): | ||
"""Exclude date from default date format""" | ||||
return "%H:%M:%S" | ||||
|
r10358 | def _log_format_default(self): | ||
"""override default log format to include time""" | ||||
|
r16358 | return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" | ||
|
r10358 | |||
|
r5847 | # create requested profiles by default, if they don't exist: | ||
auto_create = Bool(True) | ||||
|
r6724 | # file to be opened in the notebook server | ||
|
r15461 | file_to_run = Unicode('', config=True) | ||
|
r15460 | def _file_to_run_changed(self, name, old, new): | ||
path, base = os.path.split(new) | ||||
if path: | ||||
self.file_to_run = base | ||||
|
r15461 | self.notebook_dir = path | ||
|
r6724 | |||
|
r17106 | # Network related information | ||
|
r17116 | allow_origin = Unicode('', config=True, | ||
|
r17106 | help="""Set the Access-Control-Allow-Origin header | ||
Use '*' to allow any origin to access your server. | ||||
|
r17116 | Takes precedence over allow_origin_pat. | ||
|
r17106 | """ | ||
) | ||||
|
r17116 | allow_origin_pat = Unicode('', config=True, | ||
|
r17106 | help="""Use a regular expression for the Access-Control-Allow-Origin header | ||
Requests from an origin matching the expression will get replies with: | ||||
Access-Control-Allow-Origin: origin | ||||
where `origin` is the origin of the request. | ||||
|
r17116 | Ignored if allow_origin is set. | ||
|
r17106 | """ | ||
) | ||||
|
r17116 | allow_credentials = Bool(False, config=True, | ||
|
r17106 | help="Set the Access-Control-Allow-Credentials: true header" | ||
) | ||||
|
r17778 | default_url = Unicode('/tree', config=True, | ||
help="The default URL to redirect to from `/`" | ||||
) | ||||
|
r16334 | ip = Unicode('localhost', config=True, | ||
|
r4344 | help="The IP address the notebook server will listen on." | ||
) | ||||
|
r4519 | def _ip_changed(self, name, old, new): | ||
if new == u'*': self.ip = u'' | ||||
|
r5344 | port = Integer(8888, config=True, | ||
|
r4344 | help="The port the notebook server will listen on." | ||
) | ||||
|
r7234 | port_retries = Integer(50, config=True, | ||
help="The number of additional ports to try if the specified port is not available." | ||||
) | ||||
|
r4344 | |||
|
r4519 | certfile = Unicode(u'', config=True, | ||
help="""The full path to an SSL/TLS certificate file.""" | ||||
) | ||||
keyfile = Unicode(u'', config=True, | ||||
help="""The full path to a private key file for usage with SSL/TLS.""" | ||||
) | ||||
|
r10784 | |||
|
r17141 | cookie_secret_file = Unicode(config=True, | ||
help="""The file where the cookie secret is stored.""" | ||||
) | ||||
def _cookie_secret_file_default(self): | ||||
if self.profile_dir is None: | ||||
return '' | ||||
return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret') | ||||
|
r10784 | cookie_secret = Bytes(b'', config=True, | ||
help="""The random bytes used to secure cookies. | ||||
By default this is a new random number every time you start the Notebook. | ||||
Set it to a value in a config file to enable logins to persist across server sessions. | ||||
|
r10863 | |||
Note: Cookie secrets should be kept private, do not share config files with | ||||
cookie_secret stored in plaintext (you can read the value from a file). | ||||
|
r10784 | """ | ||
) | ||||
def _cookie_secret_default(self): | ||||
|
r17141 | if os.path.exists(self.cookie_secret_file): | ||
with io.open(self.cookie_secret_file, 'rb') as f: | ||||
return f.read() | ||||
else: | ||||
secret = base64.encodestring(os.urandom(1024)) | ||||
self._write_cookie_secret_file(secret) | ||||
return secret | ||||
def _write_cookie_secret_file(self, secret): | ||||
"""write my secret to my secret_file""" | ||||
self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file) | ||||
with io.open(self.cookie_secret_file, 'wb') as f: | ||||
f.write(secret) | ||||
try: | ||||
os.chmod(self.cookie_secret_file, 0o600) | ||||
except OSError: | ||||
self.log.warn( | ||||
"Could not set permissions on %s", | ||||
self.cookie_secret_file | ||||
) | ||||
|
r4344 | |||
|
r4705 | password = Unicode(u'', config=True, | ||
|
r5321 | help="""Hashed password to use for web authentication. | ||
|
r5337 | To generate, type in a python/IPython shell: | ||
|
r5321 | |||
|
r5337 | from IPython.lib import passwd; passwd() | ||
|
r5321 | |||
The string should be of the form type:salt:hashed-password. | ||||
""" | ||||
|
r4690 | ) | ||
|
r6116 | |||
|
r5065 | open_browser = Bool(True, config=True, | ||
|
r6115 | help="""Whether to open in a browser after starting. | ||
The specific browser used is platform dependent and | ||||
determined by the python standard library `webbrowser` | ||||
|
r6116 | module, unless it is overridden using the --browser | ||
(NotebookApp.browser) configuration option. | ||||
|
r6115 | """) | ||
|
r6116 | |||
browser = Unicode(u'', config=True, | ||||
|
r6117 | help="""Specify what command to use to invoke a web | ||
browser when opening the notebook. If not specified, the | ||||
default browser will be determined by the `webbrowser` | ||||
standard library module, which allows setting of the | ||||
BROWSER environment variable to override it. | ||||
|
r6116 | """) | ||
|
r5191 | |||
|
r5665 | webapp_settings = Dict(config=True, | ||
|
r17888 | help="DEPRECATED, use tornado_settings" | ||
) | ||||
def _webapp_settings_changed(self, name, old, new): | ||||
self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n") | ||||
self.tornado_settings = new | ||||
tornado_settings = Dict(config=True, | ||||
|
r5665 | help="Supply overrides for the tornado.web.Application that the " | ||
"IPython notebook uses.") | ||||
|
r15449 | |||
jinja_environment_options = Dict(config=True, | ||||
help="Supply extra arguments that will be passed to Jinja environment.") | ||||
|
r5665 | |||
|
r5547 | enable_mathjax = Bool(True, config=True, | ||
help="""Whether to enable MathJax for typesetting math/TeX | ||||
MathJax is the javascript library IPython uses to render math/LaTeX. It is | ||||
very large, so you may want to disable it if you have a slow internet | ||||
connection, or for offline use of the notebook. | ||||
When disabled, equations etc. will appear as their untransformed TeX source. | ||||
""" | ||||
) | ||||
|
r5557 | def _enable_mathjax_changed(self, name, old, new): | ||
"""set mathjax url to empty if mathjax is disabled""" | ||||
if not new: | ||||
self.mathjax_url = u'' | ||||
|
r6006 | |||
|
r15238 | base_url = Unicode('/', config=True, | ||
|
r8938 | help='''The base URL for the notebook server. | ||
Leading and trailing slashes can be omitted, | ||||
and will automatically be added. | ||||
''') | ||||
|
r15238 | def _base_url_changed(self, name, old, new): | ||
|
r8938 | if not new.startswith('/'): | ||
|
r15238 | self.base_url = '/'+new | ||
|
r8938 | elif not new.endswith('/'): | ||
|
r15238 | self.base_url = new+'/' | ||
base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""") | ||||
def _base_project_url_changed(self, name, old, new): | ||||
self.log.warn("base_project_url is deprecated, use base_url") | ||||
self.base_url = new | ||||
|
r8938 | |||
|
r7923 | extra_static_paths = List(Unicode, config=True, | ||
help="""Extra paths to search for serving static files. | ||||
This allows adding javascript/css to be available from the notebook server machine, | ||||
or overriding individual files in the IPython""" | ||||
) | ||||
def _extra_static_paths_default(self): | ||||
return [os.path.join(self.profile_dir.location, 'static')] | ||||
@property | ||||
def static_file_path(self): | ||||
"""return extra paths + the default location""" | ||||
|
r9681 | return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH] | ||
|
r12800 | |||
|
r12811 | nbextensions_path = List(Unicode, config=True, | ||
help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions""" | ||||
|
r12800 | ) | ||
|
r12811 | def _nbextensions_path_default(self): | ||
return [os.path.join(get_ipython_dir(), 'nbextensions')] | ||||
|
r6004 | |||
|
r17303 | websocket_url = Unicode("", config=True, | ||
help="""The base URL for websockets, | ||||
if it differs from the HTTP server (hint: it almost certainly doesn't). | ||||
Should be in the form of an HTTP origin: ws[s]://hostname[:port] | ||||
""" | ||||
) | ||||
|
r5557 | mathjax_url = Unicode("", config=True, | ||
help="""The url for MathJax.js.""" | ||||
) | ||||
def _mathjax_url_default(self): | ||||
if not self.enable_mathjax: | ||||
return u'' | ||||
|
r17916 | static_url_prefix = self.tornado_settings.get("static_url_prefix", | ||
|
r15238 | url_path_join(self.base_url, "static") | ||
|
r11153 | ) | ||
|
r12810 | |||
|
r12811 | # try local mathjax, either in nbextensions/mathjax or static/mathjax | ||
|
r12810 | for (url_prefix, search_path) in [ | ||
|
r15238 | (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path), | ||
|
r12810 | (static_url_prefix, self.static_file_path), | ||
]: | ||||
self.log.debug("searching for local mathjax in %s", search_path) | ||||
try: | ||||
mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path) | ||||
except IOError: | ||||
continue | ||||
|
r6155 | else: | ||
|
r12810 | url = url_path_join(url_prefix, u"mathjax/MathJax.js") | ||
self.log.info("Serving local MathJax from %s at %s", mathjax, url) | ||||
return url | ||||
# no local mathjax, serve from CDN | ||||
|
r17539 | url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js" | ||
|
r12810 | self.log.info("Using MathJax from CDN: %s", url) | ||
return url | ||||
|
r5557 | |||
def _mathjax_url_changed(self, name, old, new): | ||||
if new and not self.enable_mathjax: | ||||
# enable_mathjax=False overrides mathjax_url | ||||
self.mathjax_url = u'' | ||||
else: | ||||
self.log.info("Using MathJax: %s", new) | ||||
|
r4690 | |||
|
r17524 | contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager', | ||
|
r8180 | config=True, | ||
|
r16423 | help='The notebook manager class to use.' | ||
) | ||||
kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager', | ||||
config=True, | ||||
help='The kernel manager class to use.' | ||||
) | ||||
session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager', | ||||
config=True, | ||||
help='The session manager class to use.' | ||||
) | ||||
cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager', | ||||
config=True, | ||||
help='The cluster manager class to use.' | ||||
) | ||||
|
r8180 | |||
|
r16684 | kernel_spec_manager = Instance(KernelSpecManager) | ||
def _kernel_spec_manager_default(self): | ||||
return KernelSpecManager(ipython_dir=self.ipython_dir) | ||||
|
r10163 | trust_xheaders = Bool(False, config=True, | ||
help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers" | ||||
|
r14108 | "sent by the upstream reverse proxy. Necessary if the proxy handles SSL") | ||
|
r10163 | ) | ||
|
r12284 | |||
|
r14063 | info_file = Unicode() | ||
def _info_file_default(self): | ||||
info_file = "nbserver-%s.json"%os.getpid() | ||||
return os.path.join(self.profile_dir.security_dir, info_file) | ||||
|
r15420 | |||
notebook_dir = Unicode(py3compat.getcwd(), config=True, | ||||
help="The directory to use for notebooks and kernels." | ||||
) | ||||
|
r16375 | |||
pylab = Unicode('disabled', config=True, | ||||
help=""" | ||||
DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib. | ||||
""" | ||||
) | ||||
def _pylab_changed(self, name, old, new): | ||||
|
r16380 | """when --pylab is specified, display a warning and exit""" | ||
|
r16375 | if new != 'warn': | ||
backend = ' %s' % new | ||||
else: | ||||
backend = '' | ||||
|
r16380 | self.log.error("Support for specifying --pylab on the command line has been removed.") | ||
self.log.error( | ||||
|
r16375 | "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend) | ||
) | ||||
|
r16380 | self.exit(1) | ||
|
r15420 | |||
def _notebook_dir_changed(self, name, old, new): | ||||
"""Do a bit of validation of the notebook dir.""" | ||||
if not os.path.isabs(new): | ||||
# If we receive a non-absolute path, make it absolute. | ||||
self.notebook_dir = os.path.abspath(new) | ||||
return | ||||
|
r15423 | if not os.path.isdir(new): | ||
raise TraitError("No such notebook dir: %r" % new) | ||||
|
r15420 | |||
|
r15422 | # setting App.notebook_dir implies setting notebook and kernel dirs as well | ||
|
r17524 | self.config.FileContentsManager.root_dir = new | ||
|
r15422 | self.config.MappingKernelManager.root_dir = new | ||
|
r14063 | |||
|
r4344 | def parse_command_line(self, argv=None): | ||
|
r5104 | super(NotebookApp, self).parse_command_line(argv) | ||
|
r12284 | |||
|
r6724 | if self.extra_args: | ||
|
r13158 | arg0 = self.extra_args[0] | ||
f = os.path.abspath(arg0) | ||||
self.argv.remove(arg0) | ||||
|
r13118 | if not os.path.exists(f): | ||
self.log.critical("No such file or directory: %s", f) | ||||
self.exit(1) | ||||
|
r15461 | |||
# Use config here, to ensure that it takes higher priority than | ||||
# anything that comes from the profile. | ||||
|
r16041 | c = Config() | ||
|
r7556 | if os.path.isdir(f): | ||
|
r16041 | c.NotebookApp.notebook_dir = f | ||
|
r13019 | elif os.path.isfile(f): | ||
|
r16041 | c.NotebookApp.file_to_run = f | ||
self.update_config(c) | ||||
|
r6724 | |||
|
r12284 | def init_kernel_argv(self): | ||
"""construct the kernel arguments""" | ||||
# Kernel should get *absolute* path to profile directory | ||||
|
r17100 | self.kernel_argv = ["--profile-dir", self.profile_dir.location] | ||
|
r12284 | |||
|
r4494 | def init_configurables(self): | ||
|
r4963 | # force Session default to be secure | ||
default_secure(self.config) | ||||
|
r16423 | kls = import_item(self.kernel_manager_class) | ||
self.kernel_manager = kls( | ||||
|
r11064 | parent=self, log=self.log, kernel_argv=self.kernel_argv, | ||
|
r4960 | connection_dir = self.profile_dir.security_dir, | ||
|
r4494 | ) | ||
|
r17524 | kls = import_item(self.contents_manager_class) | ||
self.contents_manager = kls(parent=self, log=self.log) | ||||
|
r16423 | kls = import_item(self.session_manager_class) | ||
|
r17222 | self.session_manager = kls(parent=self, log=self.log, | ||
kernel_manager=self.kernel_manager, | ||||
|
r17524 | contents_manager=self.contents_manager) | ||
|
r16423 | kls = import_item(self.cluster_manager_class) | ||
self.cluster_manager = kls(parent=self, log=self.log) | ||||
|
r6199 | self.cluster_manager.update_profiles() | ||
|
r4344 | |||
|
r4345 | def init_logging(self): | ||
# This prevents double log messages because tornado use a root logger that | ||||
# self.log is a child of. The logging module dipatches log messages to a log | ||||
# and all of its ancenstors until propagate is set to False. | ||||
self.log.propagate = False | ||||
|
r10358 | |||
|
r18117 | for log in app_log, access_log, gen_log: | ||
# consistent log output name (NotebookApp instead of tornado.access, etc.) | ||||
log.name = self.log.name | ||||
|
r10358 | # hook up tornado 3's loggers to our app handlers | ||
|
r16358 | logger = logging.getLogger('tornado') | ||
logger.propagate = True | ||||
logger.parent = self.log | ||||
logger.setLevel(self.log.level) | ||||
|
r5799 | |||
def init_webapp(self): | ||||
"""initialize tornado webapp and httpserver""" | ||||
|
r17916 | self.tornado_settings['allow_origin'] = self.allow_origin | ||
|
r17184 | if self.allow_origin_pat: | ||
|
r17916 | self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat) | ||
self.tornado_settings['allow_credentials'] = self.allow_credentials | ||||
|
r17106 | |||
|
r4347 | self.web_app = NotebookWebApplication( | ||
|
r17524 | self, self.kernel_manager, self.contents_manager, | ||
|
r16684 | self.cluster_manager, self.session_manager, self.kernel_spec_manager, | ||
|
r17916 | self.log, self.base_url, self.default_url, self.tornado_settings, | ||
|
r15449 | self.jinja_environment_options | ||
|
r4347 | ) | ||
|
r4519 | if self.certfile: | ||
ssl_options = dict(certfile=self.certfile) | ||||
if self.keyfile: | ||||
ssl_options['keyfile'] = self.keyfile | ||||
else: | ||||
ssl_options = None | ||||
|
r4705 | self.web_app.password = self.password | ||
|
r10163 | self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options, | ||
xheaders=self.trust_xheaders) | ||||
|
r8723 | if not self.ip: | ||
|
r8724 | warning = "WARNING: The notebook server is listening on all IP addresses" | ||
|
r8723 | if ssl_options is None: | ||
|
r10564 | self.log.critical(warning + " and not using encryption. This " | ||
|
r8726 | "is not recommended.") | ||
|
r11644 | if not self.password: | ||
|
r10564 | self.log.critical(warning + " and not using authentication. " | ||
|
r8726 | "This is highly insecure and not recommended.") | ||
|
r7234 | success = None | ||
for port in random_ports(self.port, self.port_retries+1): | ||||
|
r4548 | try: | ||
self.http_server.listen(port, self.ip) | ||||
|
r7787 | except socket.error as e: | ||
|
r12845 | if e.errno == errno.EADDRINUSE: | ||
self.log.info('The port %i is already in use, trying another random port.' % port) | ||||
continue | ||||
elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)): | ||||
self.log.warn("Permission to listen on port %i denied" % port) | ||||
continue | ||||
else: | ||||
|
r4548 | raise | ||
else: | ||||
self.port = port | ||||
|
r7234 | success = True | ||
|
r4548 | break | ||
|
r7234 | if not success: | ||
self.log.critical('ERROR: the notebook server could not be started because ' | ||||
'no available port could be found.') | ||||
|
r7240 | self.exit(1) | ||
|
r5799 | |||
|
r14063 | @property | ||
def display_url(self): | ||||
ip = self.ip if self.ip else '[all ip addresses on your system]' | ||||
return self._url(ip) | ||||
@property | ||||
def connection_url(self): | ||||
|
r16334 | ip = self.ip if self.ip else 'localhost' | ||
|
r14063 | return self._url(ip) | ||
def _url(self, ip): | ||||
proto = 'https' if self.certfile else 'http' | ||||
|
r15238 | return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url) | ||
|
r14063 | |||
|
r6498 | def init_signal(self): | ||
|
r10200 | if not sys.platform.startswith('win'): | ||
|
r6509 | signal.signal(signal.SIGINT, self._handle_sigint) | ||
|
r6508 | signal.signal(signal.SIGTERM, self._signal_stop) | ||
|
r10031 | if hasattr(signal, 'SIGUSR1'): | ||
# Windows doesn't support SIGUSR1 | ||||
signal.signal(signal.SIGUSR1, self._signal_info) | ||||
|
r9913 | if hasattr(signal, 'SIGINFO'): | ||
# only on BSD-based systems | ||||
signal.signal(signal.SIGINFO, self._signal_info) | ||||
|
r6498 | |||
|
r6508 | def _handle_sigint(self, sig, frame): | ||
"""SIGINT handler spawns confirmation dialog""" | ||||
# register more forceful signal handler for ^C^C case | ||||
signal.signal(signal.SIGINT, self._signal_stop) | ||||
# request confirmation dialog in bg thread, to avoid | ||||
# blocking the App | ||||
thread = threading.Thread(target=self._confirm_exit) | ||||
thread.daemon = True | ||||
thread.start() | ||||
def _restore_sigint_handler(self): | ||||
"""callback for restoring original SIGINT handler""" | ||||
signal.signal(signal.SIGINT, self._handle_sigint) | ||||
def _confirm_exit(self): | ||||
"""confirm shutdown on ^C | ||||
A second ^C, or answering 'y' within 5s will cause shutdown, | ||||
otherwise original SIGINT handler will be restored. | ||||
|
r7689 | |||
This doesn't work on Windows. | ||||
|
r6508 | """ | ||
|
r9912 | info = self.log.info | ||
info('interrupted') | ||||
|
r13348 | print(self.notebook_info()) | ||
|
r10019 | sys.stdout.write("Shutdown this notebook server (y/[n])? ") | ||
|
r6508 | sys.stdout.flush() | ||
r,w,x = select.select([sys.stdin], [], [], 5) | ||||
if r: | ||||
line = sys.stdin.readline() | ||||
|
r16211 | if line.lower().startswith('y') and 'n' not in line.lower(): | ||
|
r6508 | self.log.critical("Shutdown confirmed") | ||
ioloop.IOLoop.instance().stop() | ||||
return | ||||
else: | ||||
|
r13348 | print("No answer for 5s:", end=' ') | ||
print("resuming operation...") | ||||
|
r6508 | # no answer, or answer is no: | ||
# set it back to original SIGINT handler | ||||
# use IOLoop.add_callback because signal.signal must be called | ||||
# from main thread | ||||
ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler) | ||||
def _signal_stop(self, sig, frame): | ||||
|
r6498 | self.log.critical("received signal %s, stopping", sig) | ||
ioloop.IOLoop.instance().stop() | ||||
|
r9913 | |||
def _signal_info(self, sig, frame): | ||||
|
r13348 | print(self.notebook_info()) | ||
|
r6498 | |||
|
r10557 | def init_components(self): | ||
"""Check the components submodule, and warn if it's unclean""" | ||||
status = submodule.check_submodule_status() | ||||
if status == 'missing': | ||||
self.log.warn("components submodule missing, running `git submodule update`") | ||||
submodule.update_submodules(submodule.ipython_parent()) | ||||
elif status == 'unclean': | ||||
self.log.warn("components submodule unclean, you may see 404s on static/components") | ||||
self.log.warn("run `setup.py submodule` or `git submodule update` to update") | ||||
|
r5799 | @catch_config_error | ||
def initialize(self, argv=None): | ||||
super(NotebookApp, self).initialize(argv) | ||||
|
r13307 | self.init_logging() | ||
|
r12284 | self.init_kernel_argv() | ||
|
r5799 | self.init_configurables() | ||
|
r10557 | self.init_components() | ||
|
r5799 | self.init_webapp() | ||
|
r6498 | self.init_signal() | ||
|
r5799 | |||
def cleanup_kernels(self): | ||||
|
r9112 | """Shutdown all kernels. | ||
|
r5799 | |||
The kernels will shutdown themselves when this process no longer exists, | ||||
but explicit shutdown allows the KernelManagers to cleanup the connection files. | ||||
""" | ||||
self.log.info('Shutting down kernels') | ||||
|
r9112 | self.kernel_manager.shutdown_all() | ||
|
r4344 | |||
|
r10019 | def notebook_info(self): | ||
"Return the current working directory and the server url information" | ||||
|
r17524 | info = self.contents_manager.info_string() + "\n" | ||
|
r13261 | info += "%d active kernels \n" % len(self.kernel_manager._kernels) | ||
|
r14063 | return info + "The IPython Notebook is running at: %s" % self.display_url | ||
def server_info(self): | ||||
"""Return a JSONable dict of information about this server.""" | ||||
return {'url': self.connection_url, | ||||
'hostname': self.ip if self.ip else 'localhost', | ||||
'port': self.port, | ||||
'secure': bool(self.certfile), | ||||
|
r15238 | 'base_url': self.base_url, | ||
|
r15420 | 'notebook_dir': os.path.abspath(self.notebook_dir), | ||
|
r17181 | 'pid': os.getpid() | ||
|
r14063 | } | ||
def write_server_info_file(self): | ||||
"""Write the result of server_info() to the JSON file info_file.""" | ||||
|
r14079 | with open(self.info_file, 'w') as f: | ||
json.dump(self.server_info(), f, indent=2) | ||||
|
r14063 | |||
def remove_server_info_file(self): | ||||
"""Remove the nbserver-<pid>.json file created for this server. | ||||
Ignores the error raised when the file has already been removed. | ||||
""" | ||||
try: | ||||
os.unlink(self.info_file) | ||||
except OSError as e: | ||||
if e.errno != errno.ENOENT: | ||||
raise | ||||
|
r9912 | |||
|
r4344 | def start(self): | ||
|
r10358 | """ Start the IPython Notebook server app, after initialization | ||
|
r9910 | |||
This method takes no arguments so all configuration and initialization | ||||
must be done prior to calling this method.""" | ||||
|
r14177 | if self.subapp is not None: | ||
return self.subapp.start() | ||||
|
r5786 | info = self.log.info | ||
|
r10019 | for line in self.notebook_info().split("\n"): | ||
info(line) | ||||
|
r11228 | info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).") | ||
|
r5786 | |||
|
r14063 | self.write_server_info_file() | ||
|
r7556 | if self.open_browser or self.file_to_run: | ||
|
r7658 | try: | ||
browser = webbrowser.get(self.browser or None) | ||||
except webbrowser.Error as e: | ||||
self.log.warn('No web browser found: %s.' % e) | ||||
browser = None | ||||
|
r13118 | |||
|
r15460 | if self.file_to_run: | ||
fullpath = os.path.join(self.notebook_dir, self.file_to_run) | ||||
if not os.path.exists(fullpath): | ||||
self.log.critical("%s does not exist" % fullpath) | ||||
self.exit(1) | ||||
uri = url_path_join('notebooks', self.file_to_run) | ||||
|
r6724 | else: | ||
|
r15460 | uri = 'tree' | ||
|
r7658 | if browser: | ||
|
r15460 | b = lambda : browser.open(url_path_join(self.connection_url, uri), | ||
|
r14063 | new=2) | ||
|
r7658 | threading.Thread(target=b).start() | ||
|
r5799 | try: | ||
ioloop.IOLoop.instance().start() | ||||
except KeyboardInterrupt: | ||||
info("Interrupted...") | ||||
finally: | ||||
self.cleanup_kernels() | ||||
|
r14063 | self.remove_server_info_file() | ||
|
r14177 | def list_running_servers(profile='default'): | ||
|
r14063 | """Iterate over the server info files of running notebook servers. | ||
|
r5799 | |||
|
r14063 | Given a profile name, find nbserver-* files in the security directory of | ||
that profile, and yield dicts of their information, each one pertaining to | ||||
a currently running notebook server instance. | ||||
""" | ||||
pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile) | ||||
for file in os.listdir(pd.security_dir): | ||||
if file.startswith('nbserver-'): | ||||
with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f: | ||||
|
r17181 | info = json.load(f) | ||
|
r4344 | |||
|
r17181 | # Simple check whether that process is really still running | ||
if check_pid(info['pid']): | ||||
yield info | ||||
else: | ||||
# If the process has died, try to delete its info file | ||||
try: | ||||
os.unlink(file) | ||||
except OSError: | ||||
pass # TODO: This should warn or log or something | ||||
|
r4344 | #----------------------------------------------------------------------------- | ||
# Main entry point | ||||
#----------------------------------------------------------------------------- | ||||
|
r4339 | |||
|
r11176 | launch_new_instance = NotebookApp.launch_instance | ||
|
r4339 | |||