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