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