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