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