##// END OF EJS Templates
add NotebookNotary to NotebookApp's class list...
MinRK -
Show More
@@ -1,850 +1,851 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 from __future__ import print_function
8 from __future__ import print_function
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # stdlib
20 # stdlib
21 import errno
21 import errno
22 import io
22 import io
23 import json
23 import json
24 import logging
24 import logging
25 import os
25 import os
26 import random
26 import random
27 import select
27 import select
28 import signal
28 import signal
29 import socket
29 import socket
30 import sys
30 import sys
31 import threading
31 import threading
32 import time
32 import time
33 import webbrowser
33 import webbrowser
34
34
35
35
36 # Third party
36 # Third party
37 # check for pyzmq 2.1.11
37 # check for pyzmq 2.1.11
38 from IPython.utils.zmqrelated import check_for_zmq
38 from IPython.utils.zmqrelated import check_for_zmq
39 check_for_zmq('2.1.11', 'IPython.html')
39 check_for_zmq('2.1.11', 'IPython.html')
40
40
41 from jinja2 import Environment, FileSystemLoader
41 from jinja2 import Environment, FileSystemLoader
42
42
43 # Install the pyzmq ioloop. This has to be done before anything else from
43 # Install the pyzmq ioloop. This has to be done before anything else from
44 # tornado is imported.
44 # tornado is imported.
45 from zmq.eventloop import ioloop
45 from zmq.eventloop import ioloop
46 ioloop.install()
46 ioloop.install()
47
47
48 # check for tornado 3.1.0
48 # check for tornado 3.1.0
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 try:
50 try:
51 import tornado
51 import tornado
52 except ImportError:
52 except ImportError:
53 raise ImportError(msg)
53 raise ImportError(msg)
54 try:
54 try:
55 version_info = tornado.version_info
55 version_info = tornado.version_info
56 except AttributeError:
56 except AttributeError:
57 raise ImportError(msg + ", but you have < 1.1.0")
57 raise ImportError(msg + ", but you have < 1.1.0")
58 if version_info < (3,1,0):
58 if version_info < (3,1,0):
59 raise ImportError(msg + ", but you have %s" % tornado.version)
59 raise ImportError(msg + ", but you have %s" % tornado.version)
60
60
61 from tornado import httpserver
61 from tornado import httpserver
62 from tornado import web
62 from tornado import web
63
63
64 # Our own libraries
64 # Our own libraries
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 from .base.handlers import Template404
66 from .base.handlers import Template404
67 from .log import log_request
67 from .log import log_request
68 from .services.kernels.kernelmanager import MappingKernelManager
68 from .services.kernels.kernelmanager import MappingKernelManager
69 from .services.notebooks.nbmanager import NotebookManager
69 from .services.notebooks.nbmanager import NotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
71 from .services.clusters.clustermanager import ClusterManager
71 from .services.clusters.clustermanager import ClusterManager
72 from .services.sessions.sessionmanager import SessionManager
72 from .services.sessions.sessionmanager import SessionManager
73
73
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75
75
76 from IPython.config.application import catch_config_error, boolean_flag
76 from IPython.config.application import catch_config_error, boolean_flag
77 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.application import BaseIPythonApplication
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.consoleapp import IPythonConsoleApp
79 from IPython.consoleapp import IPythonConsoleApp
80 from IPython.kernel import swallow_argv
80 from IPython.kernel import swallow_argv
81 from IPython.kernel.zmq.session import default_secure
81 from IPython.kernel.zmq.session import default_secure
82 from IPython.kernel.zmq.kernelapp import (
82 from IPython.kernel.zmq.kernelapp import (
83 kernel_flags,
83 kernel_flags,
84 kernel_aliases,
84 kernel_aliases,
85 )
85 )
86 from IPython.nbformat.sign import NotebookNotary
86 from IPython.utils.importstring import import_item
87 from IPython.utils.importstring import import_item
87 from IPython.utils.localinterfaces import localhost
88 from IPython.utils.localinterfaces import localhost
88 from IPython.utils import submodule
89 from IPython.utils import submodule
89 from IPython.utils.traitlets import (
90 from IPython.utils.traitlets import (
90 Dict, Unicode, Integer, List, Bool, Bytes,
91 Dict, Unicode, Integer, List, Bool, Bytes,
91 DottedObjectName, TraitError,
92 DottedObjectName, TraitError,
92 )
93 )
93 from IPython.utils import py3compat
94 from IPython.utils import py3compat
94 from IPython.utils.path import filefind, get_ipython_dir
95 from IPython.utils.path import filefind, get_ipython_dir
95
96
96 from .utils import url_path_join
97 from .utils import url_path_join
97
98
98 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
99 # Module globals
100 # Module globals
100 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
101
102
102 _examples = """
103 _examples = """
103 ipython notebook # start the notebook
104 ipython notebook # start the notebook
104 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 """
107 """
107
108
108 #-----------------------------------------------------------------------------
109 #-----------------------------------------------------------------------------
109 # Helper functions
110 # Helper functions
110 #-----------------------------------------------------------------------------
111 #-----------------------------------------------------------------------------
111
112
112 def random_ports(port, n):
113 def random_ports(port, n):
113 """Generate a list of n random ports near the given port.
114 """Generate a list of n random ports near the given port.
114
115
115 The first 5 ports will be sequential, and the remaining n-5 will be
116 The first 5 ports will be sequential, and the remaining n-5 will be
116 randomly selected in the range [port-2*n, port+2*n].
117 randomly selected in the range [port-2*n, port+2*n].
117 """
118 """
118 for i in range(min(5, n)):
119 for i in range(min(5, n)):
119 yield port + i
120 yield port + i
120 for i in range(n-5):
121 for i in range(n-5):
121 yield max(1, port + random.randint(-2*n, 2*n))
122 yield max(1, port + random.randint(-2*n, 2*n))
122
123
123 def load_handlers(name):
124 def load_handlers(name):
124 """Load the (URL pattern, handler) tuples for each component."""
125 """Load the (URL pattern, handler) tuples for each component."""
125 name = 'IPython.html.' + name
126 name = 'IPython.html.' + name
126 mod = __import__(name, fromlist=['default_handlers'])
127 mod = __import__(name, fromlist=['default_handlers'])
127 return mod.default_handlers
128 return mod.default_handlers
128
129
129 #-----------------------------------------------------------------------------
130 #-----------------------------------------------------------------------------
130 # The Tornado web application
131 # The Tornado web application
131 #-----------------------------------------------------------------------------
132 #-----------------------------------------------------------------------------
132
133
133 class NotebookWebApplication(web.Application):
134 class NotebookWebApplication(web.Application):
134
135
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, log, base_url,
137 cluster_manager, session_manager, log, base_url,
137 settings_overrides, jinja_env_options):
138 settings_overrides, jinja_env_options):
138
139
139 settings = self.init_settings(
140 settings = self.init_settings(
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 session_manager, log, base_url, settings_overrides, jinja_env_options)
142 session_manager, log, base_url, settings_overrides, jinja_env_options)
142 handlers = self.init_handlers(settings)
143 handlers = self.init_handlers(settings)
143
144
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 super(NotebookWebApplication, self).__init__(handlers, **settings)
145
146
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, session_manager, log, base_url,
148 cluster_manager, session_manager, log, base_url,
148 settings_overrides, jinja_env_options=None):
149 settings_overrides, jinja_env_options=None):
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # base_url will always be unicode, which will in turn
151 # base_url will always be unicode, which will in turn
151 # make the patterns unicode, and ultimately result in unicode
152 # make the patterns unicode, and ultimately result in unicode
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # This enforces that base_url be ascii in that situation.
154 # This enforces that base_url be ascii in that situation.
154 #
155 #
155 # Note that the URLs these patterns check against are escaped,
156 # Note that the URLs these patterns check against are escaped,
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 jenv_opt = jinja_env_options if jinja_env_options else {}
160 jenv_opt = jinja_env_options if jinja_env_options else {}
160 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
161 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
161 settings = dict(
162 settings = dict(
162 # basics
163 # basics
163 log_function=log_request,
164 log_function=log_request,
164 base_url=base_url,
165 base_url=base_url,
165 template_path=template_path,
166 template_path=template_path,
166 static_path=ipython_app.static_file_path,
167 static_path=ipython_app.static_file_path,
167 static_handler_class = FileFindHandler,
168 static_handler_class = FileFindHandler,
168 static_url_prefix = url_path_join(base_url,'/static/'),
169 static_url_prefix = url_path_join(base_url,'/static/'),
169
170
170 # authentication
171 # authentication
171 cookie_secret=ipython_app.cookie_secret,
172 cookie_secret=ipython_app.cookie_secret,
172 login_url=url_path_join(base_url,'/login'),
173 login_url=url_path_join(base_url,'/login'),
173 password=ipython_app.password,
174 password=ipython_app.password,
174
175
175 # managers
176 # managers
176 kernel_manager=kernel_manager,
177 kernel_manager=kernel_manager,
177 notebook_manager=notebook_manager,
178 notebook_manager=notebook_manager,
178 cluster_manager=cluster_manager,
179 cluster_manager=cluster_manager,
179 session_manager=session_manager,
180 session_manager=session_manager,
180
181
181 # IPython stuff
182 # IPython stuff
182 nbextensions_path = ipython_app.nbextensions_path,
183 nbextensions_path = ipython_app.nbextensions_path,
183 mathjax_url=ipython_app.mathjax_url,
184 mathjax_url=ipython_app.mathjax_url,
184 config=ipython_app.config,
185 config=ipython_app.config,
185 jinja2_env=env,
186 jinja2_env=env,
186 )
187 )
187
188
188 # allow custom overrides for the tornado web app.
189 # allow custom overrides for the tornado web app.
189 settings.update(settings_overrides)
190 settings.update(settings_overrides)
190 return settings
191 return settings
191
192
192 def init_handlers(self, settings):
193 def init_handlers(self, settings):
193 # Load the (URL pattern, handler) tuples for each component.
194 # Load the (URL pattern, handler) tuples for each component.
194 handlers = []
195 handlers = []
195 handlers.extend(load_handlers('base.handlers'))
196 handlers.extend(load_handlers('base.handlers'))
196 handlers.extend(load_handlers('tree.handlers'))
197 handlers.extend(load_handlers('tree.handlers'))
197 handlers.extend(load_handlers('auth.login'))
198 handlers.extend(load_handlers('auth.login'))
198 handlers.extend(load_handlers('auth.logout'))
199 handlers.extend(load_handlers('auth.logout'))
199 handlers.extend(load_handlers('notebook.handlers'))
200 handlers.extend(load_handlers('notebook.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
201 handlers.extend(load_handlers('nbconvert.handlers'))
201 handlers.extend(load_handlers('services.kernels.handlers'))
202 handlers.extend(load_handlers('services.kernels.handlers'))
202 handlers.extend(load_handlers('services.notebooks.handlers'))
203 handlers.extend(load_handlers('services.notebooks.handlers'))
203 handlers.extend(load_handlers('services.clusters.handlers'))
204 handlers.extend(load_handlers('services.clusters.handlers'))
204 handlers.extend(load_handlers('services.sessions.handlers'))
205 handlers.extend(load_handlers('services.sessions.handlers'))
205 handlers.extend(load_handlers('services.nbconvert.handlers'))
206 handlers.extend(load_handlers('services.nbconvert.handlers'))
206 # FIXME: /files/ should be handled by the Contents service when it exists
207 # FIXME: /files/ should be handled by the Contents service when it exists
207 nbm = settings['notebook_manager']
208 nbm = settings['notebook_manager']
208 if hasattr(nbm, 'notebook_dir'):
209 if hasattr(nbm, 'notebook_dir'):
209 handlers.extend([
210 handlers.extend([
210 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
211 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
211 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
212 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
212 ])
213 ])
213 # prepend base_url onto the patterns that we match
214 # prepend base_url onto the patterns that we match
214 new_handlers = []
215 new_handlers = []
215 for handler in handlers:
216 for handler in handlers:
216 pattern = url_path_join(settings['base_url'], handler[0])
217 pattern = url_path_join(settings['base_url'], handler[0])
217 new_handler = tuple([pattern] + list(handler[1:]))
218 new_handler = tuple([pattern] + list(handler[1:]))
218 new_handlers.append(new_handler)
219 new_handlers.append(new_handler)
219 # add 404 on the end, which will catch everything that falls through
220 # add 404 on the end, which will catch everything that falls through
220 new_handlers.append((r'(.*)', Template404))
221 new_handlers.append((r'(.*)', Template404))
221 return new_handlers
222 return new_handlers
222
223
223
224
224 class NbserverListApp(BaseIPythonApplication):
225 class NbserverListApp(BaseIPythonApplication):
225
226
226 description="List currently running notebook servers in this profile."
227 description="List currently running notebook servers in this profile."
227
228
228 flags = dict(
229 flags = dict(
229 json=({'NbserverListApp': {'json': True}},
230 json=({'NbserverListApp': {'json': True}},
230 "Produce machine-readable JSON output."),
231 "Produce machine-readable JSON output."),
231 )
232 )
232
233
233 json = Bool(False, config=True,
234 json = Bool(False, config=True,
234 help="If True, each line of output will be a JSON object with the "
235 help="If True, each line of output will be a JSON object with the "
235 "details from the server info file.")
236 "details from the server info file.")
236
237
237 def start(self):
238 def start(self):
238 if not self.json:
239 if not self.json:
239 print("Currently running servers:")
240 print("Currently running servers:")
240 for serverinfo in list_running_servers(self.profile):
241 for serverinfo in list_running_servers(self.profile):
241 if self.json:
242 if self.json:
242 print(json.dumps(serverinfo))
243 print(json.dumps(serverinfo))
243 else:
244 else:
244 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
245
246
246 #-----------------------------------------------------------------------------
247 #-----------------------------------------------------------------------------
247 # Aliases and Flags
248 # Aliases and Flags
248 #-----------------------------------------------------------------------------
249 #-----------------------------------------------------------------------------
249
250
250 flags = dict(kernel_flags)
251 flags = dict(kernel_flags)
251 flags['no-browser']=(
252 flags['no-browser']=(
252 {'NotebookApp' : {'open_browser' : False}},
253 {'NotebookApp' : {'open_browser' : False}},
253 "Don't open the notebook in a browser after startup."
254 "Don't open the notebook in a browser after startup."
254 )
255 )
255 flags['no-mathjax']=(
256 flags['no-mathjax']=(
256 {'NotebookApp' : {'enable_mathjax' : False}},
257 {'NotebookApp' : {'enable_mathjax' : False}},
257 """Disable MathJax
258 """Disable MathJax
258
259
259 MathJax is the javascript library IPython uses to render math/LaTeX. It is
260 MathJax is the javascript library IPython uses to render math/LaTeX. It is
260 very large, so you may want to disable it if you have a slow internet
261 very large, so you may want to disable it if you have a slow internet
261 connection, or for offline use of the notebook.
262 connection, or for offline use of the notebook.
262
263
263 When disabled, equations etc. will appear as their untransformed TeX source.
264 When disabled, equations etc. will appear as their untransformed TeX source.
264 """
265 """
265 )
266 )
266
267
267 # Add notebook manager flags
268 # Add notebook manager flags
268 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
269 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
269 'Auto-save a .py script everytime the .ipynb notebook is saved',
270 'Auto-save a .py script everytime the .ipynb notebook is saved',
270 'Do not auto-save .py scripts for every notebook'))
271 'Do not auto-save .py scripts for every notebook'))
271
272
272 # the flags that are specific to the frontend
273 # the flags that are specific to the frontend
273 # these must be scrubbed before being passed to the kernel,
274 # these must be scrubbed before being passed to the kernel,
274 # or it will raise an error on unrecognized flags
275 # or it will raise an error on unrecognized flags
275 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
276 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
276
277
277 aliases = dict(kernel_aliases)
278 aliases = dict(kernel_aliases)
278
279
279 aliases.update({
280 aliases.update({
280 'ip': 'NotebookApp.ip',
281 'ip': 'NotebookApp.ip',
281 'port': 'NotebookApp.port',
282 'port': 'NotebookApp.port',
282 'port-retries': 'NotebookApp.port_retries',
283 'port-retries': 'NotebookApp.port_retries',
283 'transport': 'KernelManager.transport',
284 'transport': 'KernelManager.transport',
284 'keyfile': 'NotebookApp.keyfile',
285 'keyfile': 'NotebookApp.keyfile',
285 'certfile': 'NotebookApp.certfile',
286 'certfile': 'NotebookApp.certfile',
286 'notebook-dir': 'NotebookApp.notebook_dir',
287 'notebook-dir': 'NotebookApp.notebook_dir',
287 'browser': 'NotebookApp.browser',
288 'browser': 'NotebookApp.browser',
288 })
289 })
289
290
290 # remove ipkernel flags that are singletons, and don't make sense in
291 # remove ipkernel flags that are singletons, and don't make sense in
291 # multi-kernel evironment:
292 # multi-kernel evironment:
292 aliases.pop('f', None)
293 aliases.pop('f', None)
293
294
294 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
295 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
295 u'notebook-dir', u'profile', u'profile-dir']
296 u'notebook-dir', u'profile', u'profile-dir']
296
297
297 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
298 # NotebookApp
299 # NotebookApp
299 #-----------------------------------------------------------------------------
300 #-----------------------------------------------------------------------------
300
301
301 class NotebookApp(BaseIPythonApplication):
302 class NotebookApp(BaseIPythonApplication):
302
303
303 name = 'ipython-notebook'
304 name = 'ipython-notebook'
304
305
305 description = """
306 description = """
306 The IPython HTML Notebook.
307 The IPython HTML Notebook.
307
308
308 This launches a Tornado based HTML Notebook Server that serves up an
309 This launches a Tornado based HTML Notebook Server that serves up an
309 HTML5/Javascript Notebook client.
310 HTML5/Javascript Notebook client.
310 """
311 """
311 examples = _examples
312 examples = _examples
312
313
313 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
314 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
314 FileNotebookManager]
315 FileNotebookManager, NotebookNotary]
315 flags = Dict(flags)
316 flags = Dict(flags)
316 aliases = Dict(aliases)
317 aliases = Dict(aliases)
317
318
318 subcommands = dict(
319 subcommands = dict(
319 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
320 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
320 )
321 )
321
322
322 kernel_argv = List(Unicode)
323 kernel_argv = List(Unicode)
323
324
324 def _log_level_default(self):
325 def _log_level_default(self):
325 return logging.INFO
326 return logging.INFO
326
327
327 def _log_format_default(self):
328 def _log_format_default(self):
328 """override default log format to include time"""
329 """override default log format to include time"""
329 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
330 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
330
331
331 # create requested profiles by default, if they don't exist:
332 # create requested profiles by default, if they don't exist:
332 auto_create = Bool(True)
333 auto_create = Bool(True)
333
334
334 # file to be opened in the notebook server
335 # file to be opened in the notebook server
335 file_to_run = Unicode('', config=True)
336 file_to_run = Unicode('', config=True)
336 def _file_to_run_changed(self, name, old, new):
337 def _file_to_run_changed(self, name, old, new):
337 path, base = os.path.split(new)
338 path, base = os.path.split(new)
338 if path:
339 if path:
339 self.file_to_run = base
340 self.file_to_run = base
340 self.notebook_dir = path
341 self.notebook_dir = path
341
342
342 # Network related information.
343 # Network related information.
343
344
344 ip = Unicode(config=True,
345 ip = Unicode(config=True,
345 help="The IP address the notebook server will listen on."
346 help="The IP address the notebook server will listen on."
346 )
347 )
347 def _ip_default(self):
348 def _ip_default(self):
348 return localhost()
349 return localhost()
349
350
350 def _ip_changed(self, name, old, new):
351 def _ip_changed(self, name, old, new):
351 if new == u'*': self.ip = u''
352 if new == u'*': self.ip = u''
352
353
353 port = Integer(8888, config=True,
354 port = Integer(8888, config=True,
354 help="The port the notebook server will listen on."
355 help="The port the notebook server will listen on."
355 )
356 )
356 port_retries = Integer(50, config=True,
357 port_retries = Integer(50, config=True,
357 help="The number of additional ports to try if the specified port is not available."
358 help="The number of additional ports to try if the specified port is not available."
358 )
359 )
359
360
360 certfile = Unicode(u'', config=True,
361 certfile = Unicode(u'', config=True,
361 help="""The full path to an SSL/TLS certificate file."""
362 help="""The full path to an SSL/TLS certificate file."""
362 )
363 )
363
364
364 keyfile = Unicode(u'', config=True,
365 keyfile = Unicode(u'', config=True,
365 help="""The full path to a private key file for usage with SSL/TLS."""
366 help="""The full path to a private key file for usage with SSL/TLS."""
366 )
367 )
367
368
368 cookie_secret = Bytes(b'', config=True,
369 cookie_secret = Bytes(b'', config=True,
369 help="""The random bytes used to secure cookies.
370 help="""The random bytes used to secure cookies.
370 By default this is a new random number every time you start the Notebook.
371 By default this is a new random number every time you start the Notebook.
371 Set it to a value in a config file to enable logins to persist across server sessions.
372 Set it to a value in a config file to enable logins to persist across server sessions.
372
373
373 Note: Cookie secrets should be kept private, do not share config files with
374 Note: Cookie secrets should be kept private, do not share config files with
374 cookie_secret stored in plaintext (you can read the value from a file).
375 cookie_secret stored in plaintext (you can read the value from a file).
375 """
376 """
376 )
377 )
377 def _cookie_secret_default(self):
378 def _cookie_secret_default(self):
378 return os.urandom(1024)
379 return os.urandom(1024)
379
380
380 password = Unicode(u'', config=True,
381 password = Unicode(u'', config=True,
381 help="""Hashed password to use for web authentication.
382 help="""Hashed password to use for web authentication.
382
383
383 To generate, type in a python/IPython shell:
384 To generate, type in a python/IPython shell:
384
385
385 from IPython.lib import passwd; passwd()
386 from IPython.lib import passwd; passwd()
386
387
387 The string should be of the form type:salt:hashed-password.
388 The string should be of the form type:salt:hashed-password.
388 """
389 """
389 )
390 )
390
391
391 open_browser = Bool(True, config=True,
392 open_browser = Bool(True, config=True,
392 help="""Whether to open in a browser after starting.
393 help="""Whether to open in a browser after starting.
393 The specific browser used is platform dependent and
394 The specific browser used is platform dependent and
394 determined by the python standard library `webbrowser`
395 determined by the python standard library `webbrowser`
395 module, unless it is overridden using the --browser
396 module, unless it is overridden using the --browser
396 (NotebookApp.browser) configuration option.
397 (NotebookApp.browser) configuration option.
397 """)
398 """)
398
399
399 browser = Unicode(u'', config=True,
400 browser = Unicode(u'', config=True,
400 help="""Specify what command to use to invoke a web
401 help="""Specify what command to use to invoke a web
401 browser when opening the notebook. If not specified, the
402 browser when opening the notebook. If not specified, the
402 default browser will be determined by the `webbrowser`
403 default browser will be determined by the `webbrowser`
403 standard library module, which allows setting of the
404 standard library module, which allows setting of the
404 BROWSER environment variable to override it.
405 BROWSER environment variable to override it.
405 """)
406 """)
406
407
407 webapp_settings = Dict(config=True,
408 webapp_settings = Dict(config=True,
408 help="Supply overrides for the tornado.web.Application that the "
409 help="Supply overrides for the tornado.web.Application that the "
409 "IPython notebook uses.")
410 "IPython notebook uses.")
410
411
411 jinja_environment_options = Dict(config=True,
412 jinja_environment_options = Dict(config=True,
412 help="Supply extra arguments that will be passed to Jinja environment.")
413 help="Supply extra arguments that will be passed to Jinja environment.")
413
414
414
415
415 enable_mathjax = Bool(True, config=True,
416 enable_mathjax = Bool(True, config=True,
416 help="""Whether to enable MathJax for typesetting math/TeX
417 help="""Whether to enable MathJax for typesetting math/TeX
417
418
418 MathJax is the javascript library IPython uses to render math/LaTeX. It is
419 MathJax is the javascript library IPython uses to render math/LaTeX. It is
419 very large, so you may want to disable it if you have a slow internet
420 very large, so you may want to disable it if you have a slow internet
420 connection, or for offline use of the notebook.
421 connection, or for offline use of the notebook.
421
422
422 When disabled, equations etc. will appear as their untransformed TeX source.
423 When disabled, equations etc. will appear as their untransformed TeX source.
423 """
424 """
424 )
425 )
425 def _enable_mathjax_changed(self, name, old, new):
426 def _enable_mathjax_changed(self, name, old, new):
426 """set mathjax url to empty if mathjax is disabled"""
427 """set mathjax url to empty if mathjax is disabled"""
427 if not new:
428 if not new:
428 self.mathjax_url = u''
429 self.mathjax_url = u''
429
430
430 base_url = Unicode('/', config=True,
431 base_url = Unicode('/', config=True,
431 help='''The base URL for the notebook server.
432 help='''The base URL for the notebook server.
432
433
433 Leading and trailing slashes can be omitted,
434 Leading and trailing slashes can be omitted,
434 and will automatically be added.
435 and will automatically be added.
435 ''')
436 ''')
436 def _base_url_changed(self, name, old, new):
437 def _base_url_changed(self, name, old, new):
437 if not new.startswith('/'):
438 if not new.startswith('/'):
438 self.base_url = '/'+new
439 self.base_url = '/'+new
439 elif not new.endswith('/'):
440 elif not new.endswith('/'):
440 self.base_url = new+'/'
441 self.base_url = new+'/'
441
442
442 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
443 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
443 def _base_project_url_changed(self, name, old, new):
444 def _base_project_url_changed(self, name, old, new):
444 self.log.warn("base_project_url is deprecated, use base_url")
445 self.log.warn("base_project_url is deprecated, use base_url")
445 self.base_url = new
446 self.base_url = new
446
447
447 extra_static_paths = List(Unicode, config=True,
448 extra_static_paths = List(Unicode, config=True,
448 help="""Extra paths to search for serving static files.
449 help="""Extra paths to search for serving static files.
449
450
450 This allows adding javascript/css to be available from the notebook server machine,
451 This allows adding javascript/css to be available from the notebook server machine,
451 or overriding individual files in the IPython"""
452 or overriding individual files in the IPython"""
452 )
453 )
453 def _extra_static_paths_default(self):
454 def _extra_static_paths_default(self):
454 return [os.path.join(self.profile_dir.location, 'static')]
455 return [os.path.join(self.profile_dir.location, 'static')]
455
456
456 @property
457 @property
457 def static_file_path(self):
458 def static_file_path(self):
458 """return extra paths + the default location"""
459 """return extra paths + the default location"""
459 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
460 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
460
461
461 nbextensions_path = List(Unicode, config=True,
462 nbextensions_path = List(Unicode, config=True,
462 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
463 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
463 )
464 )
464 def _nbextensions_path_default(self):
465 def _nbextensions_path_default(self):
465 return [os.path.join(get_ipython_dir(), 'nbextensions')]
466 return [os.path.join(get_ipython_dir(), 'nbextensions')]
466
467
467 mathjax_url = Unicode("", config=True,
468 mathjax_url = Unicode("", config=True,
468 help="""The url for MathJax.js."""
469 help="""The url for MathJax.js."""
469 )
470 )
470 def _mathjax_url_default(self):
471 def _mathjax_url_default(self):
471 if not self.enable_mathjax:
472 if not self.enable_mathjax:
472 return u''
473 return u''
473 static_url_prefix = self.webapp_settings.get("static_url_prefix",
474 static_url_prefix = self.webapp_settings.get("static_url_prefix",
474 url_path_join(self.base_url, "static")
475 url_path_join(self.base_url, "static")
475 )
476 )
476
477
477 # try local mathjax, either in nbextensions/mathjax or static/mathjax
478 # try local mathjax, either in nbextensions/mathjax or static/mathjax
478 for (url_prefix, search_path) in [
479 for (url_prefix, search_path) in [
479 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
480 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
480 (static_url_prefix, self.static_file_path),
481 (static_url_prefix, self.static_file_path),
481 ]:
482 ]:
482 self.log.debug("searching for local mathjax in %s", search_path)
483 self.log.debug("searching for local mathjax in %s", search_path)
483 try:
484 try:
484 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
485 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
485 except IOError:
486 except IOError:
486 continue
487 continue
487 else:
488 else:
488 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
489 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
489 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
490 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
490 return url
491 return url
491
492
492 # no local mathjax, serve from CDN
493 # no local mathjax, serve from CDN
493 if self.certfile:
494 if self.certfile:
494 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
495 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
495 host = u"https://c328740.ssl.cf1.rackcdn.com"
496 host = u"https://c328740.ssl.cf1.rackcdn.com"
496 else:
497 else:
497 host = u"http://cdn.mathjax.org"
498 host = u"http://cdn.mathjax.org"
498
499
499 url = host + u"/mathjax/latest/MathJax.js"
500 url = host + u"/mathjax/latest/MathJax.js"
500 self.log.info("Using MathJax from CDN: %s", url)
501 self.log.info("Using MathJax from CDN: %s", url)
501 return url
502 return url
502
503
503 def _mathjax_url_changed(self, name, old, new):
504 def _mathjax_url_changed(self, name, old, new):
504 if new and not self.enable_mathjax:
505 if new and not self.enable_mathjax:
505 # enable_mathjax=False overrides mathjax_url
506 # enable_mathjax=False overrides mathjax_url
506 self.mathjax_url = u''
507 self.mathjax_url = u''
507 else:
508 else:
508 self.log.info("Using MathJax: %s", new)
509 self.log.info("Using MathJax: %s", new)
509
510
510 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
511 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
511 config=True,
512 config=True,
512 help='The notebook manager class to use.')
513 help='The notebook manager class to use.')
513
514
514 trust_xheaders = Bool(False, config=True,
515 trust_xheaders = Bool(False, config=True,
515 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
516 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
516 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
517 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
517 )
518 )
518
519
519 info_file = Unicode()
520 info_file = Unicode()
520
521
521 def _info_file_default(self):
522 def _info_file_default(self):
522 info_file = "nbserver-%s.json"%os.getpid()
523 info_file = "nbserver-%s.json"%os.getpid()
523 return os.path.join(self.profile_dir.security_dir, info_file)
524 return os.path.join(self.profile_dir.security_dir, info_file)
524
525
525 notebook_dir = Unicode(py3compat.getcwd(), config=True,
526 notebook_dir = Unicode(py3compat.getcwd(), config=True,
526 help="The directory to use for notebooks and kernels."
527 help="The directory to use for notebooks and kernels."
527 )
528 )
528
529
529 def _notebook_dir_changed(self, name, old, new):
530 def _notebook_dir_changed(self, name, old, new):
530 """Do a bit of validation of the notebook dir."""
531 """Do a bit of validation of the notebook dir."""
531 if not os.path.isabs(new):
532 if not os.path.isabs(new):
532 # If we receive a non-absolute path, make it absolute.
533 # If we receive a non-absolute path, make it absolute.
533 self.notebook_dir = os.path.abspath(new)
534 self.notebook_dir = os.path.abspath(new)
534 return
535 return
535 if not os.path.isdir(new):
536 if not os.path.isdir(new):
536 raise TraitError("No such notebook dir: %r" % new)
537 raise TraitError("No such notebook dir: %r" % new)
537
538
538 # setting App.notebook_dir implies setting notebook and kernel dirs as well
539 # setting App.notebook_dir implies setting notebook and kernel dirs as well
539 self.config.FileNotebookManager.notebook_dir = new
540 self.config.FileNotebookManager.notebook_dir = new
540 self.config.MappingKernelManager.root_dir = new
541 self.config.MappingKernelManager.root_dir = new
541
542
542
543
543 def parse_command_line(self, argv=None):
544 def parse_command_line(self, argv=None):
544 super(NotebookApp, self).parse_command_line(argv)
545 super(NotebookApp, self).parse_command_line(argv)
545
546
546 if self.extra_args:
547 if self.extra_args:
547 arg0 = self.extra_args[0]
548 arg0 = self.extra_args[0]
548 f = os.path.abspath(arg0)
549 f = os.path.abspath(arg0)
549 self.argv.remove(arg0)
550 self.argv.remove(arg0)
550 if not os.path.exists(f):
551 if not os.path.exists(f):
551 self.log.critical("No such file or directory: %s", f)
552 self.log.critical("No such file or directory: %s", f)
552 self.exit(1)
553 self.exit(1)
553
554
554 # Use config here, to ensure that it takes higher priority than
555 # Use config here, to ensure that it takes higher priority than
555 # anything that comes from the profile.
556 # anything that comes from the profile.
556 if os.path.isdir(f):
557 if os.path.isdir(f):
557 self.config.NotebookApp.notebook_dir = f
558 self.config.NotebookApp.notebook_dir = f
558 elif os.path.isfile(f):
559 elif os.path.isfile(f):
559 self.config.NotebookApp.file_to_run = f
560 self.config.NotebookApp.file_to_run = f
560
561
561 def init_kernel_argv(self):
562 def init_kernel_argv(self):
562 """construct the kernel arguments"""
563 """construct the kernel arguments"""
563 # Scrub frontend-specific flags
564 # Scrub frontend-specific flags
564 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
565 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
565 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
566 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
566 self.log.warn('\n '.join([
567 self.log.warn('\n '.join([
567 "Starting all kernels in pylab mode is not recommended,",
568 "Starting all kernels in pylab mode is not recommended,",
568 "and will be disabled in a future release.",
569 "and will be disabled in a future release.",
569 "Please use the %matplotlib magic to enable matplotlib instead.",
570 "Please use the %matplotlib magic to enable matplotlib instead.",
570 "pylab implies many imports, which can have confusing side effects",
571 "pylab implies many imports, which can have confusing side effects",
571 "and harm the reproducibility of your notebooks.",
572 "and harm the reproducibility of your notebooks.",
572 ]))
573 ]))
573 # Kernel should inherit default config file from frontend
574 # Kernel should inherit default config file from frontend
574 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
575 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
575 # Kernel should get *absolute* path to profile directory
576 # Kernel should get *absolute* path to profile directory
576 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
577 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
577
578
578 def init_configurables(self):
579 def init_configurables(self):
579 # force Session default to be secure
580 # force Session default to be secure
580 default_secure(self.config)
581 default_secure(self.config)
581 self.kernel_manager = MappingKernelManager(
582 self.kernel_manager = MappingKernelManager(
582 parent=self, log=self.log, kernel_argv=self.kernel_argv,
583 parent=self, log=self.log, kernel_argv=self.kernel_argv,
583 connection_dir = self.profile_dir.security_dir,
584 connection_dir = self.profile_dir.security_dir,
584 )
585 )
585 kls = import_item(self.notebook_manager_class)
586 kls = import_item(self.notebook_manager_class)
586 self.notebook_manager = kls(parent=self, log=self.log)
587 self.notebook_manager = kls(parent=self, log=self.log)
587 self.session_manager = SessionManager(parent=self, log=self.log)
588 self.session_manager = SessionManager(parent=self, log=self.log)
588 self.cluster_manager = ClusterManager(parent=self, log=self.log)
589 self.cluster_manager = ClusterManager(parent=self, log=self.log)
589 self.cluster_manager.update_profiles()
590 self.cluster_manager.update_profiles()
590
591
591 def init_logging(self):
592 def init_logging(self):
592 # This prevents double log messages because tornado use a root logger that
593 # This prevents double log messages because tornado use a root logger that
593 # self.log is a child of. The logging module dipatches log messages to a log
594 # self.log is a child of. The logging module dipatches log messages to a log
594 # and all of its ancenstors until propagate is set to False.
595 # and all of its ancenstors until propagate is set to False.
595 self.log.propagate = False
596 self.log.propagate = False
596
597
597 # hook up tornado 3's loggers to our app handlers
598 # hook up tornado 3's loggers to our app handlers
598 for name in ('access', 'application', 'general'):
599 for name in ('access', 'application', 'general'):
599 logger = logging.getLogger('tornado.%s' % name)
600 logger = logging.getLogger('tornado.%s' % name)
600 logger.parent = self.log
601 logger.parent = self.log
601 logger.setLevel(self.log.level)
602 logger.setLevel(self.log.level)
602
603
603 def init_webapp(self):
604 def init_webapp(self):
604 """initialize tornado webapp and httpserver"""
605 """initialize tornado webapp and httpserver"""
605 self.web_app = NotebookWebApplication(
606 self.web_app = NotebookWebApplication(
606 self, self.kernel_manager, self.notebook_manager,
607 self, self.kernel_manager, self.notebook_manager,
607 self.cluster_manager, self.session_manager,
608 self.cluster_manager, self.session_manager,
608 self.log, self.base_url, self.webapp_settings,
609 self.log, self.base_url, self.webapp_settings,
609 self.jinja_environment_options
610 self.jinja_environment_options
610 )
611 )
611 if self.certfile:
612 if self.certfile:
612 ssl_options = dict(certfile=self.certfile)
613 ssl_options = dict(certfile=self.certfile)
613 if self.keyfile:
614 if self.keyfile:
614 ssl_options['keyfile'] = self.keyfile
615 ssl_options['keyfile'] = self.keyfile
615 else:
616 else:
616 ssl_options = None
617 ssl_options = None
617 self.web_app.password = self.password
618 self.web_app.password = self.password
618 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
619 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
619 xheaders=self.trust_xheaders)
620 xheaders=self.trust_xheaders)
620 if not self.ip:
621 if not self.ip:
621 warning = "WARNING: The notebook server is listening on all IP addresses"
622 warning = "WARNING: The notebook server is listening on all IP addresses"
622 if ssl_options is None:
623 if ssl_options is None:
623 self.log.critical(warning + " and not using encryption. This "
624 self.log.critical(warning + " and not using encryption. This "
624 "is not recommended.")
625 "is not recommended.")
625 if not self.password:
626 if not self.password:
626 self.log.critical(warning + " and not using authentication. "
627 self.log.critical(warning + " and not using authentication. "
627 "This is highly insecure and not recommended.")
628 "This is highly insecure and not recommended.")
628 success = None
629 success = None
629 for port in random_ports(self.port, self.port_retries+1):
630 for port in random_ports(self.port, self.port_retries+1):
630 try:
631 try:
631 self.http_server.listen(port, self.ip)
632 self.http_server.listen(port, self.ip)
632 except socket.error as e:
633 except socket.error as e:
633 if e.errno == errno.EADDRINUSE:
634 if e.errno == errno.EADDRINUSE:
634 self.log.info('The port %i is already in use, trying another random port.' % port)
635 self.log.info('The port %i is already in use, trying another random port.' % port)
635 continue
636 continue
636 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
637 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
637 self.log.warn("Permission to listen on port %i denied" % port)
638 self.log.warn("Permission to listen on port %i denied" % port)
638 continue
639 continue
639 else:
640 else:
640 raise
641 raise
641 else:
642 else:
642 self.port = port
643 self.port = port
643 success = True
644 success = True
644 break
645 break
645 if not success:
646 if not success:
646 self.log.critical('ERROR: the notebook server could not be started because '
647 self.log.critical('ERROR: the notebook server could not be started because '
647 'no available port could be found.')
648 'no available port could be found.')
648 self.exit(1)
649 self.exit(1)
649
650
650 @property
651 @property
651 def display_url(self):
652 def display_url(self):
652 ip = self.ip if self.ip else '[all ip addresses on your system]'
653 ip = self.ip if self.ip else '[all ip addresses on your system]'
653 return self._url(ip)
654 return self._url(ip)
654
655
655 @property
656 @property
656 def connection_url(self):
657 def connection_url(self):
657 ip = self.ip if self.ip else localhost()
658 ip = self.ip if self.ip else localhost()
658 return self._url(ip)
659 return self._url(ip)
659
660
660 def _url(self, ip):
661 def _url(self, ip):
661 proto = 'https' if self.certfile else 'http'
662 proto = 'https' if self.certfile else 'http'
662 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
663 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
663
664
664 def init_signal(self):
665 def init_signal(self):
665 if not sys.platform.startswith('win'):
666 if not sys.platform.startswith('win'):
666 signal.signal(signal.SIGINT, self._handle_sigint)
667 signal.signal(signal.SIGINT, self._handle_sigint)
667 signal.signal(signal.SIGTERM, self._signal_stop)
668 signal.signal(signal.SIGTERM, self._signal_stop)
668 if hasattr(signal, 'SIGUSR1'):
669 if hasattr(signal, 'SIGUSR1'):
669 # Windows doesn't support SIGUSR1
670 # Windows doesn't support SIGUSR1
670 signal.signal(signal.SIGUSR1, self._signal_info)
671 signal.signal(signal.SIGUSR1, self._signal_info)
671 if hasattr(signal, 'SIGINFO'):
672 if hasattr(signal, 'SIGINFO'):
672 # only on BSD-based systems
673 # only on BSD-based systems
673 signal.signal(signal.SIGINFO, self._signal_info)
674 signal.signal(signal.SIGINFO, self._signal_info)
674
675
675 def _handle_sigint(self, sig, frame):
676 def _handle_sigint(self, sig, frame):
676 """SIGINT handler spawns confirmation dialog"""
677 """SIGINT handler spawns confirmation dialog"""
677 # register more forceful signal handler for ^C^C case
678 # register more forceful signal handler for ^C^C case
678 signal.signal(signal.SIGINT, self._signal_stop)
679 signal.signal(signal.SIGINT, self._signal_stop)
679 # request confirmation dialog in bg thread, to avoid
680 # request confirmation dialog in bg thread, to avoid
680 # blocking the App
681 # blocking the App
681 thread = threading.Thread(target=self._confirm_exit)
682 thread = threading.Thread(target=self._confirm_exit)
682 thread.daemon = True
683 thread.daemon = True
683 thread.start()
684 thread.start()
684
685
685 def _restore_sigint_handler(self):
686 def _restore_sigint_handler(self):
686 """callback for restoring original SIGINT handler"""
687 """callback for restoring original SIGINT handler"""
687 signal.signal(signal.SIGINT, self._handle_sigint)
688 signal.signal(signal.SIGINT, self._handle_sigint)
688
689
689 def _confirm_exit(self):
690 def _confirm_exit(self):
690 """confirm shutdown on ^C
691 """confirm shutdown on ^C
691
692
692 A second ^C, or answering 'y' within 5s will cause shutdown,
693 A second ^C, or answering 'y' within 5s will cause shutdown,
693 otherwise original SIGINT handler will be restored.
694 otherwise original SIGINT handler will be restored.
694
695
695 This doesn't work on Windows.
696 This doesn't work on Windows.
696 """
697 """
697 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
698 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
698 time.sleep(0.1)
699 time.sleep(0.1)
699 info = self.log.info
700 info = self.log.info
700 info('interrupted')
701 info('interrupted')
701 print(self.notebook_info())
702 print(self.notebook_info())
702 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
703 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
703 sys.stdout.flush()
704 sys.stdout.flush()
704 r,w,x = select.select([sys.stdin], [], [], 5)
705 r,w,x = select.select([sys.stdin], [], [], 5)
705 if r:
706 if r:
706 line = sys.stdin.readline()
707 line = sys.stdin.readline()
707 if line.lower().startswith('y'):
708 if line.lower().startswith('y'):
708 self.log.critical("Shutdown confirmed")
709 self.log.critical("Shutdown confirmed")
709 ioloop.IOLoop.instance().stop()
710 ioloop.IOLoop.instance().stop()
710 return
711 return
711 else:
712 else:
712 print("No answer for 5s:", end=' ')
713 print("No answer for 5s:", end=' ')
713 print("resuming operation...")
714 print("resuming operation...")
714 # no answer, or answer is no:
715 # no answer, or answer is no:
715 # set it back to original SIGINT handler
716 # set it back to original SIGINT handler
716 # use IOLoop.add_callback because signal.signal must be called
717 # use IOLoop.add_callback because signal.signal must be called
717 # from main thread
718 # from main thread
718 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
719 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
719
720
720 def _signal_stop(self, sig, frame):
721 def _signal_stop(self, sig, frame):
721 self.log.critical("received signal %s, stopping", sig)
722 self.log.critical("received signal %s, stopping", sig)
722 ioloop.IOLoop.instance().stop()
723 ioloop.IOLoop.instance().stop()
723
724
724 def _signal_info(self, sig, frame):
725 def _signal_info(self, sig, frame):
725 print(self.notebook_info())
726 print(self.notebook_info())
726
727
727 def init_components(self):
728 def init_components(self):
728 """Check the components submodule, and warn if it's unclean"""
729 """Check the components submodule, and warn if it's unclean"""
729 status = submodule.check_submodule_status()
730 status = submodule.check_submodule_status()
730 if status == 'missing':
731 if status == 'missing':
731 self.log.warn("components submodule missing, running `git submodule update`")
732 self.log.warn("components submodule missing, running `git submodule update`")
732 submodule.update_submodules(submodule.ipython_parent())
733 submodule.update_submodules(submodule.ipython_parent())
733 elif status == 'unclean':
734 elif status == 'unclean':
734 self.log.warn("components submodule unclean, you may see 404s on static/components")
735 self.log.warn("components submodule unclean, you may see 404s on static/components")
735 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
736 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
736
737
737 @catch_config_error
738 @catch_config_error
738 def initialize(self, argv=None):
739 def initialize(self, argv=None):
739 super(NotebookApp, self).initialize(argv)
740 super(NotebookApp, self).initialize(argv)
740 self.init_logging()
741 self.init_logging()
741 self.init_kernel_argv()
742 self.init_kernel_argv()
742 self.init_configurables()
743 self.init_configurables()
743 self.init_components()
744 self.init_components()
744 self.init_webapp()
745 self.init_webapp()
745 self.init_signal()
746 self.init_signal()
746
747
747 def cleanup_kernels(self):
748 def cleanup_kernels(self):
748 """Shutdown all kernels.
749 """Shutdown all kernels.
749
750
750 The kernels will shutdown themselves when this process no longer exists,
751 The kernels will shutdown themselves when this process no longer exists,
751 but explicit shutdown allows the KernelManagers to cleanup the connection files.
752 but explicit shutdown allows the KernelManagers to cleanup the connection files.
752 """
753 """
753 self.log.info('Shutting down kernels')
754 self.log.info('Shutting down kernels')
754 self.kernel_manager.shutdown_all()
755 self.kernel_manager.shutdown_all()
755
756
756 def notebook_info(self):
757 def notebook_info(self):
757 "Return the current working directory and the server url information"
758 "Return the current working directory and the server url information"
758 info = self.notebook_manager.info_string() + "\n"
759 info = self.notebook_manager.info_string() + "\n"
759 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
760 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
760 return info + "The IPython Notebook is running at: %s" % self.display_url
761 return info + "The IPython Notebook is running at: %s" % self.display_url
761
762
762 def server_info(self):
763 def server_info(self):
763 """Return a JSONable dict of information about this server."""
764 """Return a JSONable dict of information about this server."""
764 return {'url': self.connection_url,
765 return {'url': self.connection_url,
765 'hostname': self.ip if self.ip else 'localhost',
766 'hostname': self.ip if self.ip else 'localhost',
766 'port': self.port,
767 'port': self.port,
767 'secure': bool(self.certfile),
768 'secure': bool(self.certfile),
768 'base_url': self.base_url,
769 'base_url': self.base_url,
769 'notebook_dir': os.path.abspath(self.notebook_dir),
770 'notebook_dir': os.path.abspath(self.notebook_dir),
770 }
771 }
771
772
772 def write_server_info_file(self):
773 def write_server_info_file(self):
773 """Write the result of server_info() to the JSON file info_file."""
774 """Write the result of server_info() to the JSON file info_file."""
774 with open(self.info_file, 'w') as f:
775 with open(self.info_file, 'w') as f:
775 json.dump(self.server_info(), f, indent=2)
776 json.dump(self.server_info(), f, indent=2)
776
777
777 def remove_server_info_file(self):
778 def remove_server_info_file(self):
778 """Remove the nbserver-<pid>.json file created for this server.
779 """Remove the nbserver-<pid>.json file created for this server.
779
780
780 Ignores the error raised when the file has already been removed.
781 Ignores the error raised when the file has already been removed.
781 """
782 """
782 try:
783 try:
783 os.unlink(self.info_file)
784 os.unlink(self.info_file)
784 except OSError as e:
785 except OSError as e:
785 if e.errno != errno.ENOENT:
786 if e.errno != errno.ENOENT:
786 raise
787 raise
787
788
788 def start(self):
789 def start(self):
789 """ Start the IPython Notebook server app, after initialization
790 """ Start the IPython Notebook server app, after initialization
790
791
791 This method takes no arguments so all configuration and initialization
792 This method takes no arguments so all configuration and initialization
792 must be done prior to calling this method."""
793 must be done prior to calling this method."""
793 if self.subapp is not None:
794 if self.subapp is not None:
794 return self.subapp.start()
795 return self.subapp.start()
795
796
796 info = self.log.info
797 info = self.log.info
797 for line in self.notebook_info().split("\n"):
798 for line in self.notebook_info().split("\n"):
798 info(line)
799 info(line)
799 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
800 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
800
801
801 self.write_server_info_file()
802 self.write_server_info_file()
802
803
803 if self.open_browser or self.file_to_run:
804 if self.open_browser or self.file_to_run:
804 try:
805 try:
805 browser = webbrowser.get(self.browser or None)
806 browser = webbrowser.get(self.browser or None)
806 except webbrowser.Error as e:
807 except webbrowser.Error as e:
807 self.log.warn('No web browser found: %s.' % e)
808 self.log.warn('No web browser found: %s.' % e)
808 browser = None
809 browser = None
809
810
810 if self.file_to_run:
811 if self.file_to_run:
811 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
812 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
812 if not os.path.exists(fullpath):
813 if not os.path.exists(fullpath):
813 self.log.critical("%s does not exist" % fullpath)
814 self.log.critical("%s does not exist" % fullpath)
814 self.exit(1)
815 self.exit(1)
815
816
816 uri = url_path_join('notebooks', self.file_to_run)
817 uri = url_path_join('notebooks', self.file_to_run)
817 else:
818 else:
818 uri = 'tree'
819 uri = 'tree'
819 if browser:
820 if browser:
820 b = lambda : browser.open(url_path_join(self.connection_url, uri),
821 b = lambda : browser.open(url_path_join(self.connection_url, uri),
821 new=2)
822 new=2)
822 threading.Thread(target=b).start()
823 threading.Thread(target=b).start()
823 try:
824 try:
824 ioloop.IOLoop.instance().start()
825 ioloop.IOLoop.instance().start()
825 except KeyboardInterrupt:
826 except KeyboardInterrupt:
826 info("Interrupted...")
827 info("Interrupted...")
827 finally:
828 finally:
828 self.cleanup_kernels()
829 self.cleanup_kernels()
829 self.remove_server_info_file()
830 self.remove_server_info_file()
830
831
831
832
832 def list_running_servers(profile='default'):
833 def list_running_servers(profile='default'):
833 """Iterate over the server info files of running notebook servers.
834 """Iterate over the server info files of running notebook servers.
834
835
835 Given a profile name, find nbserver-* files in the security directory of
836 Given a profile name, find nbserver-* files in the security directory of
836 that profile, and yield dicts of their information, each one pertaining to
837 that profile, and yield dicts of their information, each one pertaining to
837 a currently running notebook server instance.
838 a currently running notebook server instance.
838 """
839 """
839 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
840 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
840 for file in os.listdir(pd.security_dir):
841 for file in os.listdir(pd.security_dir):
841 if file.startswith('nbserver-'):
842 if file.startswith('nbserver-'):
842 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
843 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
843 yield json.load(f)
844 yield json.load(f)
844
845
845 #-----------------------------------------------------------------------------
846 #-----------------------------------------------------------------------------
846 # Main entry point
847 # Main entry point
847 #-----------------------------------------------------------------------------
848 #-----------------------------------------------------------------------------
848
849
849 launch_new_instance = NotebookApp.launch_instance
850 launch_new_instance = NotebookApp.launch_instance
850
851
General Comments 0
You need to be logged in to leave comments. Login now