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