notebookapp.py
874 lines
| 33.8 KiB
| text/x-python
|
PythonLexer
|
r6067 | # coding: utf-8 | ||
|
r4609 | """A tornado based IPython notebook server. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
|
r13348 | from __future__ import print_function | ||
|
r4344 | #----------------------------------------------------------------------------- | ||
|
r10557 | # Copyright (C) 2013 The IPython Development Team | ||
|
r4348 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
|
r4609 | # the file COPYING, distributed as part of this software. | ||
|
r4348 | #----------------------------------------------------------------------------- | ||
#----------------------------------------------------------------------------- | ||||
|
r4344 | # Imports | ||
#----------------------------------------------------------------------------- | ||||
|
r5212 | # stdlib | ||
|
r4548 | import errno | ||
|
r14063 | import io | ||
import json | ||||
|
r4339 | import logging | ||
import os | ||||
|
r7234 | import random | ||
|
r6508 | import select | ||
|
r4345 | import signal | ||
|
r4548 | import socket | ||
|
r4345 | import sys | ||
|
r5212 | import threading | ||
|
r6509 | import time | ||
|
r5065 | import webbrowser | ||
|
r4339 | |||
|
r10199 | |||
|
r5212 | # Third party | ||
|
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 | ||||
|
r5212 | # Our own libraries | ||
|
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 | ||
from .services.notebooks.nbmanager import NotebookManager | ||||
from .services.notebooks.filenbmanager import FileNotebookManager | ||||
from .services.clusters.clustermanager import ClusterManager | ||||
|
r12982 | from .services.sessions.sessionmanager import SessionManager | ||
|
r10642 | |||
|
r10650 | from .base.handlers import AuthenticatedFileHandler, FileFindHandler | ||
|
r4339 | |||
|
r5762 | from IPython.config.application import catch_config_error, boolean_flag | ||
|
r4344 | from IPython.core.application import BaseIPythonApplication | ||
|
r14063 | from IPython.core.profiledir import ProfileDir | ||
|
r11023 | from IPython.consoleapp import IPythonConsoleApp | ||
|
r9376 | from IPython.kernel import swallow_argv | ||
|
r10642 | from IPython.kernel.zmq.session import default_secure | ||
|
r9372 | from IPython.kernel.zmq.kernelapp import ( | ||
|
r9357 | kernel_flags, | ||
kernel_aliases, | ||||
|
r4345 | ) | ||
|
r14858 | from IPython.nbformat import current, sign | ||
|
r8180 | from IPython.utils.importstring import import_item | ||
|
r12591 | from IPython.utils.localinterfaces import localhost | ||
|
r10557 | from IPython.utils import submodule | ||
|
r8180 | from IPython.utils.traitlets import ( | ||
|
r10784 | Dict, Unicode, Integer, List, Bool, Bytes, | ||
|
r14858 | DottedObjectName, Instance, | ||
|
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 | |||
|
r9284 | def __init__(self, ipython_app, kernel_manager, notebook_manager, | ||
|
r13042 | cluster_manager, session_manager, log, base_project_url, | ||
settings_overrides): | ||||
|
r10647 | |||
|
r10656 | settings = self.init_settings( | ||
ipython_app, kernel_manager, notebook_manager, cluster_manager, | ||||
|
r13042 | session_manager, log, base_project_url, settings_overrides) | ||
|
r10656 | handlers = self.init_handlers(settings) | ||
super(NotebookWebApplication, self).__init__(handlers, **settings) | ||||
|
r5664 | |||
|
r10656 | def init_settings(self, ipython_app, kernel_manager, notebook_manager, | ||
|
r13042 | cluster_manager, session_manager, log, base_project_url, | ||
settings_overrides): | ||||
|
r6067 | # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and | ||
# base_project_url will always be unicode, which will in turn | ||||
# make the patterns unicode, and ultimately result in unicode | ||||
# keys in kwargs to handler._execute(**kwargs) in tornado. | ||||
# This enforces that base_project_url be ascii in that situation. | ||||
# | ||||
# Note that the URLs these patterns check against are escaped, | ||||
# and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'. | ||||
base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii') | ||||
|
r12194 | template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates")) | ||
|
r7797 | settings = dict( | ||
|
r10355 | # basics | ||
|
r14645 | log_function=log_request, | ||
|
r10355 | base_project_url=base_project_url, | ||
base_kernel_url=ipython_app.base_kernel_url, | ||||
template_path=template_path, | ||||
|
r7930 | static_path=ipython_app.static_file_path, | ||
|
r7923 | static_handler_class = FileFindHandler, | ||
|
r8938 | static_url_prefix = url_path_join(base_project_url,'/static/'), | ||
|
r10355 | |||
# authentication | ||||
|
r10784 | cookie_secret=ipython_app.cookie_secret, | ||
|
r8938 | login_url=url_path_join(base_project_url,'/login'), | ||
|
r10355 | password=ipython_app.password, | ||
# managers | ||||
kernel_manager=kernel_manager, | ||||
notebook_manager=notebook_manager, | ||||
cluster_manager=cluster_manager, | ||||
|
r12982 | session_manager=session_manager, | ||
|
r10355 | # IPython stuff | ||
|
r12811 | nbextensions_path = ipython_app.nbextensions_path, | ||
|
r10388 | mathjax_url=ipython_app.mathjax_url, | ||
|
r10355 | config=ipython_app.config, | ||
|
r12195 | jinja2_env=Environment(loader=FileSystemLoader(template_path)), | ||
|
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')) | ||||
|
r10665 | handlers.extend(load_handlers('notebook.handlers')) | ||
|
r13827 | handlers.extend(load_handlers('nbconvert.handlers')) | ||
|
r10665 | handlers.extend(load_handlers('services.kernels.handlers')) | ||
handlers.extend(load_handlers('services.notebooks.handlers')) | ||||
handlers.extend(load_handlers('services.clusters.handlers')) | ||||
|
r12982 | handlers.extend(load_handlers('services.sessions.handlers')) | ||
|
r13837 | handlers.extend(load_handlers('services.nbconvert.handlers')) | ||
|
r10656 | handlers.extend([ | ||
(r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}), | ||||
|
r12811 | (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}), | ||
|
r10656 | ]) | ||
|
r6004 | # prepend base_project_url onto the patterns that we match | ||
new_handlers = [] | ||||
for handler in handlers: | ||||
|
r10656 | pattern = url_path_join(settings['base_project_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 | |||
|
r14858 | #----------------------------------------------------------------------------- | ||
# Subcommands | ||||
#----------------------------------------------------------------------------- | ||||
|
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 | |||
|
r14858 | |||
class NotebookTrustApp(BaseIPythonApplication): | ||||
description="""Sign one or more IPython notebooks with your key, | ||||
to trust their dynamic (HTML, Javascript) output.""" | ||||
examples="""ipython notebook trust mynotebook.ipynb""" | ||||
notary = Instance(sign.NotebookNotary) | ||||
def _notary_default(self): | ||||
return sign.NotebookNotary(parent=self, profile_dir=self.profile_dir) | ||||
def sign_notebook(self, notebook_path): | ||||
if not os.path.exists(notebook_path): | ||||
self.log.error("Notebook missing: %s" % notebook_path) | ||||
self.exit(1) | ||||
with io.open(notebook_path, encoding='utf8') as f: | ||||
nb = current.read(f, 'json') | ||||
sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme) | ||||
with io.open(notebook_path, 'w', encoding='utf8') as f: | ||||
current.write(nb, f, 'json') | ||||
def start(self): | ||||
if not self.extra_args: | ||||
self.log.critical("Specify at least one notebook to sign.") | ||||
self.exit(1) | ||||
for notebook_path in self.extra_args: | ||||
print("Signing notebook: %s" % notebook_path) | ||||
self.sign_notebook(notebook_path) | ||||
|
r4344 | #----------------------------------------------------------------------------- | ||
# Aliases and Flags | ||||
#----------------------------------------------------------------------------- | ||||
|
r9357 | flags = dict(kernel_flags) | ||
|
r5065 | flags['no-browser']=( | ||
|
r5128 | {'NotebookApp' : {'open_browser' : False}}, | ||
|
r5065 | "Don't open the notebook in a browser after startup." | ||
) | ||||
|
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 | |||
|
r5762 | # Add notebook manager flags | ||
|
r8183 | flags.update(boolean_flag('script', 'FileNotebookManager.save_script', | ||
|
r5762 | 'Auto-save a .py script everytime the .ipynb notebook is saved', | ||
'Do not auto-save .py scripts for every notebook')) | ||||
|
r5758 | |||
|
r4344 | # the flags that are specific to the frontend | ||
# these must be scrubbed before being passed to the kernel, | ||||
# or it will raise an error on unrecognized flags | ||||
|
r11644 | notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script'] | ||
|
r4344 | |||
|
r9357 | aliases = dict(kernel_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', | ||||
|
r8194 | 'notebook-dir': 'NotebookManager.notebook_dir', | ||
|
r6116 | 'browser': 'NotebookApp.browser', | ||
|
r4519 | }) | ||
|
r4344 | |||
|
r4963 | # remove ipkernel flags that are singletons, and don't make sense in | ||
# multi-kernel evironment: | ||||
aliases.pop('f', None) | ||||
|
r7234 | notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile', | ||
|
r12284 | u'notebook-dir', u'profile', u'profile-dir'] | ||
|
r4579 | |||
|
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 | ||
|
r4344 | |||
|
r8194 | classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager, | ||
|
r8183 | FileNotebookManager] | ||
|
r4344 | flags = Dict(flags) | ||
aliases = Dict(aliases) | ||||
|
r14177 | |||
subcommands = dict( | ||||
list=(NbserverListApp, NbserverListApp.description.splitlines()[0]), | ||||
|
r14858 | trust=(NotebookTrustApp, NotebookTrustApp.description), | ||
|
r14177 | ) | ||
|
r4344 | |||
kernel_argv = List(Unicode) | ||||
|
r10355 | def _log_level_default(self): | ||
return logging.INFO | ||||
|
r4345 | |||
|
r10358 | def _log_format_default(self): | ||
"""override default log format to include time""" | ||||
|
r10560 | return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)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 | ||
file_to_run = Unicode('') | ||||
|
r4519 | # Network related information. | ||
|
r12591 | ip = Unicode(config=True, | ||
|
r4344 | help="The IP address the notebook server will listen on." | ||
) | ||||
|
r12591 | def _ip_default(self): | ||
return localhost() | ||||
|
r4344 | |||
|
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 | |||
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): | ||||
return os.urandom(1024) | ||||
|
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, | ||
help="Supply overrides for the tornado.web.Application that the " | ||||
"IPython notebook uses.") | ||||
|
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 | |||
|
r6004 | base_project_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. | ||||
''') | ||||
def _base_project_url_changed(self, name, old, new): | ||||
if not new.startswith('/'): | ||||
self.base_project_url = '/'+new | ||||
elif not new.endswith('/'): | ||||
self.base_project_url = new+'/' | ||||
|
r6004 | base_kernel_url = Unicode('/', config=True, | ||
|
r8938 | help='''The base URL for the kernel server | ||
Leading and trailing slashes can be omitted, | ||||
and will automatically be added. | ||||
''') | ||||
def _base_kernel_url_changed(self, name, old, new): | ||||
if not new.startswith('/'): | ||||
self.base_kernel_url = '/'+new | ||||
elif not new.endswith('/'): | ||||
self.base_kernel_url = new+'/' | ||||
|
r10808 | websocket_url = Unicode("", config=True, | ||
help="""The base URL for the websocket server, | ||||
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] | ||||
""" | ||||
|
r6006 | ) | ||
|
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 | |||
|
r5557 | mathjax_url = Unicode("", config=True, | ||
help="""The url for MathJax.js.""" | ||||
) | ||||
def _mathjax_url_default(self): | ||||
if not self.enable_mathjax: | ||||
return u'' | ||||
|
r6002 | static_url_prefix = self.webapp_settings.get("static_url_prefix", | ||
|
r11153 | url_path_join(self.base_project_url, "static") | ||
) | ||||
|
r12810 | |||
|
r12811 | # try local mathjax, either in nbextensions/mathjax or static/mathjax | ||
|
r12810 | for (url_prefix, search_path) in [ | ||
|
r12811 | (url_path_join(self.base_project_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 | ||||
if self.certfile: | ||||
# HTTPS: load from Rackspace CDN, because SSL certificate requires it | ||||
host = u"https://c328740.ssl.cf1.rackcdn.com" | ||||
|
r7923 | else: | ||
|
r12810 | host = u"http://cdn.mathjax.org" | ||
url = host + u"/mathjax/latest/MathJax.js" | ||||
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 | |||
|
r11035 | notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager', | ||
|
r8180 | config=True, | ||
help='The notebook manager class to use.') | ||||
|
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) | ||||
|
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) | ||||
|
r7556 | if os.path.isdir(f): | ||
|
r13118 | self.config.FileNotebookManager.notebook_dir = f | ||
|
r13019 | elif os.path.isfile(f): | ||
|
r7556 | self.file_to_run = f | ||
|
r6724 | |||
|
r12284 | def init_kernel_argv(self): | ||
"""construct the kernel arguments""" | ||||
# Scrub frontend-specific flags | ||||
self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags) | ||||
|
r14108 | if any(arg.startswith(u'--pylab') for arg in self.kernel_argv): | ||
self.log.warn('\n '.join([ | ||||
|
r14172 | "Starting all kernels in pylab mode is not recommended,", | ||
"and will be disabled in a future release.", | ||||
|
r14108 | "Please use the %matplotlib magic to enable matplotlib instead.", | ||
"pylab implies many imports, which can have confusing side effects", | ||||
"and harm the reproducibility of your notebooks.", | ||||
])) | ||||
|
r12284 | # Kernel should inherit default config file from frontend | ||
self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name) | ||||
# Kernel should get *absolute* path to profile directory | ||||
self.kernel_argv.extend(["--profile-dir", self.profile_dir.location]) | ||||
|
r4494 | def init_configurables(self): | ||
|
r4963 | # force Session default to be secure | ||
default_secure(self.config) | ||||
|
r4545 | self.kernel_manager = MappingKernelManager( | ||
|
r11064 | parent=self, log=self.log, kernel_argv=self.kernel_argv, | ||
|
r4960 | connection_dir = self.profile_dir.security_dir, | ||
|
r4494 | ) | ||
|
r8180 | kls = import_item(self.notebook_manager_class) | ||
|
r11064 | self.notebook_manager = kls(parent=self, log=self.log) | ||
|
r12982 | self.session_manager = SessionManager(parent=self, log=self.log) | ||
|
r11064 | self.cluster_manager = ClusterManager(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 | |||
# hook up tornado 3's loggers to our app handlers | ||||
for name in ('access', 'application', 'general'): | ||||
|
r13081 | logger = logging.getLogger('tornado.%s' % name) | ||
|
r13307 | logger.parent = self.log | ||
|
r13081 | logger.setLevel(self.log.level) | ||
|
r5799 | |||
def init_webapp(self): | ||||
"""initialize tornado webapp and httpserver""" | ||||
|
r4347 | self.web_app = NotebookWebApplication( | ||
|
r12982 | self, self.kernel_manager, self.notebook_manager, | ||
|
r13042 | self.cluster_manager, self.session_manager, | ||
|
r13006 | self.log, self.base_project_url, self.webapp_settings | ||
|
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): | ||||
ip = self.ip if self.ip else localhost() | ||||
return self._url(ip) | ||||
def _url(self, ip): | ||||
proto = 'https' if self.certfile else 'http' | ||||
return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url) | ||||
|
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 | """ | ||
|
r6509 | # FIXME: remove this delay when pyzmq dependency is >= 2.1.11 | ||
time.sleep(0.1) | ||||
|
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() | ||||
if line.lower().startswith('y'): | ||||
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" | ||||
|
r13261 | info = self.notebook_manager.info_string() + "\n" | ||
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), | ||||
|
r14179 | 'base_project_url': self.base_project_url, | ||
'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir), | ||||
|
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 | |||
f = self.file_to_run | ||||
|
r13622 | if f: | ||
|
r14063 | nbdir = os.path.abspath(self.notebook_manager.notebook_dir) | ||
|
r13622 | if f.startswith(nbdir): | ||
f = f[len(nbdir):] | ||||
else: | ||||
self.log.warn( | ||||
"Probably won't be able to open notebook %s " | ||||
"because it is not in notebook_dir %s", | ||||
f, nbdir, | ||||
) | ||||
|
r6724 | |||
|
r13118 | if os.path.isfile(self.file_to_run): | ||
url = url_path_join('notebooks', f) | ||||
|
r6724 | else: | ||
|
r13118 | url = url_path_join('tree', f) | ||
|
r7658 | if browser: | ||
|
r14063 | b = lambda : browser.open("%s%s" % (self.connection_url, url), | ||
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: | ||||
yield json.load(f) | ||||
|
r4344 | |||
#----------------------------------------------------------------------------- | ||||
# Main entry point | ||||
#----------------------------------------------------------------------------- | ||||
|
r4339 | |||
|
r11176 | launch_new_instance = NotebookApp.launch_instance | ||
|
r4339 | |||