##// END OF EJS Templates
Added trust_xheaders config option to delegate it to HTTPServer....
Alberto Valverde -
Show More
@@ -1,681 +1,687
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 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import uuid
31 import uuid
32 import webbrowser
32 import webbrowser
33
33
34 # Third party
34 # Third party
35 import zmq
35 import zmq
36 from jinja2 import Environment, FileSystemLoader
36 from jinja2 import Environment, FileSystemLoader
37
37
38 # Install the pyzmq ioloop. This has to be done before anything else from
38 # Install the pyzmq ioloop. This has to be done before anything else from
39 # tornado is imported.
39 # tornado is imported.
40 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
41 ioloop.install()
41 ioloop.install()
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 FileFindHandler,
54 FileFindHandler,
55 )
55 )
56 from .nbmanager import NotebookManager
56 from .nbmanager import NotebookManager
57 from .filenbmanager import FileNotebookManager
57 from .filenbmanager import FileNotebookManager
58 from .clustermanager import ClusterManager
58 from .clustermanager import ClusterManager
59
59
60 from IPython.config.application import catch_config_error, boolean_flag
60 from IPython.config.application import catch_config_error, boolean_flag
61 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.application import BaseIPythonApplication
62 from IPython.core.profiledir import ProfileDir
62 from IPython.core.profiledir import ProfileDir
63 from IPython.frontend.consoleapp import IPythonConsoleApp
63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 from IPython.kernel import swallow_argv
64 from IPython.kernel import swallow_argv
65 from IPython.kernel.zmq.session import Session, default_secure
65 from IPython.kernel.zmq.session import Session, default_secure
66 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
66 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
67 from IPython.kernel.zmq.kernelapp import (
67 from IPython.kernel.zmq.kernelapp import (
68 kernel_flags,
68 kernel_flags,
69 kernel_aliases,
69 kernel_aliases,
70 IPKernelApp
70 IPKernelApp
71 )
71 )
72 from IPython.utils.importstring import import_item
72 from IPython.utils.importstring import import_item
73 from IPython.utils.localinterfaces import LOCALHOST
73 from IPython.utils.localinterfaces import LOCALHOST
74 from IPython.utils.traitlets import (
74 from IPython.utils.traitlets import (
75 Dict, Unicode, Integer, List, Enum, Bool,
75 Dict, Unicode, Integer, List, Enum, Bool,
76 DottedObjectName
76 DottedObjectName
77 )
77 )
78 from IPython.utils import py3compat
78 from IPython.utils import py3compat
79 from IPython.utils.path import filefind
79 from IPython.utils.path import filefind
80
80
81 #-----------------------------------------------------------------------------
81 #-----------------------------------------------------------------------------
82 # Module globals
82 # Module globals
83 #-----------------------------------------------------------------------------
83 #-----------------------------------------------------------------------------
84
84
85 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
85 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
86 _kernel_action_regex = r"(?P<action>restart|interrupt)"
86 _kernel_action_regex = r"(?P<action>restart|interrupt)"
87 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
87 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
88 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
88 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
89 _cluster_action_regex = r"(?P<action>start|stop)"
89 _cluster_action_regex = r"(?P<action>start|stop)"
90
90
91 _examples = """
91 _examples = """
92 ipython notebook # start the notebook
92 ipython notebook # start the notebook
93 ipython notebook --profile=sympy # use the sympy profile
93 ipython notebook --profile=sympy # use the sympy profile
94 ipython notebook --pylab=inline # pylab in inline plotting mode
94 ipython notebook --pylab=inline # pylab in inline plotting mode
95 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
95 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
96 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
96 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
97 """
97 """
98
98
99 # Packagers: modify this line if you store the notebook static files elsewhere
99 # Packagers: modify this line if you store the notebook static files elsewhere
100 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
100 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
101
101
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103 # Helper functions
103 # Helper functions
104 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
105
105
106 def url_path_join(a,b):
106 def url_path_join(a,b):
107 if a.endswith('/') and b.startswith('/'):
107 if a.endswith('/') and b.startswith('/'):
108 return a[:-1]+b
108 return a[:-1]+b
109 else:
109 else:
110 return a+b
110 return a+b
111
111
112 def random_ports(port, n):
112 def random_ports(port, n):
113 """Generate a list of n random ports near the given port.
113 """Generate a list of n random ports near the given port.
114
114
115 The first 5 ports will be sequential, and the remaining n-5 will be
115 The first 5 ports will be sequential, and the remaining n-5 will be
116 randomly selected in the range [port-2*n, port+2*n].
116 randomly selected in the range [port-2*n, port+2*n].
117 """
117 """
118 for i in range(min(5, n)):
118 for i in range(min(5, n)):
119 yield port + i
119 yield port + i
120 for i in range(n-5):
120 for i in range(n-5):
121 yield port + random.randint(-2*n, 2*n)
121 yield port + random.randint(-2*n, 2*n)
122
122
123 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
124 # The Tornado web application
124 # The Tornado web application
125 #-----------------------------------------------------------------------------
125 #-----------------------------------------------------------------------------
126
126
127 class NotebookWebApplication(web.Application):
127 class NotebookWebApplication(web.Application):
128
128
129 def __init__(self, ipython_app, kernel_manager, notebook_manager,
129 def __init__(self, ipython_app, kernel_manager, notebook_manager,
130 cluster_manager, log,
130 cluster_manager, log,
131 base_project_url, settings_overrides):
131 base_project_url, settings_overrides):
132 handlers = [
132 handlers = [
133 (r"/", ProjectDashboardHandler),
133 (r"/", ProjectDashboardHandler),
134 (r"/login", LoginHandler),
134 (r"/login", LoginHandler),
135 (r"/logout", LogoutHandler),
135 (r"/logout", LogoutHandler),
136 (r"/new", NewHandler),
136 (r"/new", NewHandler),
137 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
137 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
138 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
138 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
139 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
139 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
140 (r"/kernels", MainKernelHandler),
140 (r"/kernels", MainKernelHandler),
141 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
141 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
142 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
142 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
143 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
143 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
144 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
144 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
145 (r"/notebooks", NotebookRootHandler),
145 (r"/notebooks", NotebookRootHandler),
146 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
146 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
147 (r"/rstservice/render", RSTHandler),
147 (r"/rstservice/render", RSTHandler),
148 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
148 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
149 (r"/clusters", MainClusterHandler),
149 (r"/clusters", MainClusterHandler),
150 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
150 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
151 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
151 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
152 ]
152 ]
153
153
154 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
155 # base_project_url will always be unicode, which will in turn
155 # base_project_url will always be unicode, which will in turn
156 # make the patterns unicode, and ultimately result in unicode
156 # make the patterns unicode, and ultimately result in unicode
157 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # keys in kwargs to handler._execute(**kwargs) in tornado.
158 # This enforces that base_project_url be ascii in that situation.
158 # This enforces that base_project_url be ascii in that situation.
159 #
159 #
160 # Note that the URLs these patterns check against are escaped,
160 # Note that the URLs these patterns check against are escaped,
161 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
161 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
162 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
163
163
164 settings = dict(
164 settings = dict(
165 template_path=os.path.join(os.path.dirname(__file__), "templates"),
165 template_path=os.path.join(os.path.dirname(__file__), "templates"),
166 static_path=ipython_app.static_file_path,
166 static_path=ipython_app.static_file_path,
167 static_handler_class = FileFindHandler,
167 static_handler_class = FileFindHandler,
168 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 static_url_prefix = url_path_join(base_project_url,'/static/'),
169 cookie_secret=os.urandom(1024),
169 cookie_secret=os.urandom(1024),
170 login_url=url_path_join(base_project_url,'/login'),
170 login_url=url_path_join(base_project_url,'/login'),
171 cookie_name='username-%s' % uuid.uuid4(),
171 cookie_name='username-%s' % uuid.uuid4(),
172 )
172 )
173
173
174 # allow custom overrides for the tornado web app.
174 # allow custom overrides for the tornado web app.
175 settings.update(settings_overrides)
175 settings.update(settings_overrides)
176
176
177 # prepend base_project_url onto the patterns that we match
177 # prepend base_project_url onto the patterns that we match
178 new_handlers = []
178 new_handlers = []
179 for handler in handlers:
179 for handler in handlers:
180 pattern = url_path_join(base_project_url, handler[0])
180 pattern = url_path_join(base_project_url, handler[0])
181 new_handler = tuple([pattern]+list(handler[1:]))
181 new_handler = tuple([pattern]+list(handler[1:]))
182 new_handlers.append( new_handler )
182 new_handlers.append( new_handler )
183
183
184 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
184 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
185
185
186 self.kernel_manager = kernel_manager
186 self.kernel_manager = kernel_manager
187 self.notebook_manager = notebook_manager
187 self.notebook_manager = notebook_manager
188 self.cluster_manager = cluster_manager
188 self.cluster_manager = cluster_manager
189 self.ipython_app = ipython_app
189 self.ipython_app = ipython_app
190 self.read_only = self.ipython_app.read_only
190 self.read_only = self.ipython_app.read_only
191 self.config = self.ipython_app.config
191 self.config = self.ipython_app.config
192 self.use_less = self.ipython_app.use_less
192 self.use_less = self.ipython_app.use_less
193 self.log = log
193 self.log = log
194 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
194 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
195
195
196
196
197
197
198 #-----------------------------------------------------------------------------
198 #-----------------------------------------------------------------------------
199 # Aliases and Flags
199 # Aliases and Flags
200 #-----------------------------------------------------------------------------
200 #-----------------------------------------------------------------------------
201
201
202 flags = dict(kernel_flags)
202 flags = dict(kernel_flags)
203 flags['no-browser']=(
203 flags['no-browser']=(
204 {'NotebookApp' : {'open_browser' : False}},
204 {'NotebookApp' : {'open_browser' : False}},
205 "Don't open the notebook in a browser after startup."
205 "Don't open the notebook in a browser after startup."
206 )
206 )
207 flags['no-mathjax']=(
207 flags['no-mathjax']=(
208 {'NotebookApp' : {'enable_mathjax' : False}},
208 {'NotebookApp' : {'enable_mathjax' : False}},
209 """Disable MathJax
209 """Disable MathJax
210
210
211 MathJax is the javascript library IPython uses to render math/LaTeX. It is
211 MathJax is the javascript library IPython uses to render math/LaTeX. It is
212 very large, so you may want to disable it if you have a slow internet
212 very large, so you may want to disable it if you have a slow internet
213 connection, or for offline use of the notebook.
213 connection, or for offline use of the notebook.
214
214
215 When disabled, equations etc. will appear as their untransformed TeX source.
215 When disabled, equations etc. will appear as their untransformed TeX source.
216 """
216 """
217 )
217 )
218 flags['read-only'] = (
218 flags['read-only'] = (
219 {'NotebookApp' : {'read_only' : True}},
219 {'NotebookApp' : {'read_only' : True}},
220 """Allow read-only access to notebooks.
220 """Allow read-only access to notebooks.
221
221
222 When using a password to protect the notebook server, this flag
222 When using a password to protect the notebook server, this flag
223 allows unauthenticated clients to view the notebook list, and
223 allows unauthenticated clients to view the notebook list, and
224 individual notebooks, but not edit them, start kernels, or run
224 individual notebooks, but not edit them, start kernels, or run
225 code.
225 code.
226
226
227 If no password is set, the server will be entirely read-only.
227 If no password is set, the server will be entirely read-only.
228 """
228 """
229 )
229 )
230
230
231 # Add notebook manager flags
231 # Add notebook manager flags
232 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
232 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
233 'Auto-save a .py script everytime the .ipynb notebook is saved',
233 'Auto-save a .py script everytime the .ipynb notebook is saved',
234 'Do not auto-save .py scripts for every notebook'))
234 'Do not auto-save .py scripts for every notebook'))
235
235
236 # the flags that are specific to the frontend
236 # the flags that are specific to the frontend
237 # these must be scrubbed before being passed to the kernel,
237 # these must be scrubbed before being passed to the kernel,
238 # or it will raise an error on unrecognized flags
238 # or it will raise an error on unrecognized flags
239 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
239 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
240
240
241 aliases = dict(kernel_aliases)
241 aliases = dict(kernel_aliases)
242
242
243 aliases.update({
243 aliases.update({
244 'ip': 'NotebookApp.ip',
244 'ip': 'NotebookApp.ip',
245 'port': 'NotebookApp.port',
245 'port': 'NotebookApp.port',
246 'port-retries': 'NotebookApp.port_retries',
246 'port-retries': 'NotebookApp.port_retries',
247 'transport': 'KernelManager.transport',
247 'transport': 'KernelManager.transport',
248 'keyfile': 'NotebookApp.keyfile',
248 'keyfile': 'NotebookApp.keyfile',
249 'certfile': 'NotebookApp.certfile',
249 'certfile': 'NotebookApp.certfile',
250 'notebook-dir': 'NotebookManager.notebook_dir',
250 'notebook-dir': 'NotebookManager.notebook_dir',
251 'browser': 'NotebookApp.browser',
251 'browser': 'NotebookApp.browser',
252 })
252 })
253
253
254 # remove ipkernel flags that are singletons, and don't make sense in
254 # remove ipkernel flags that are singletons, and don't make sense in
255 # multi-kernel evironment:
255 # multi-kernel evironment:
256 aliases.pop('f', None)
256 aliases.pop('f', None)
257
257
258 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
258 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
259 u'notebook-dir']
259 u'notebook-dir']
260
260
261 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
262 # NotebookApp
262 # NotebookApp
263 #-----------------------------------------------------------------------------
263 #-----------------------------------------------------------------------------
264
264
265 class NotebookApp(BaseIPythonApplication):
265 class NotebookApp(BaseIPythonApplication):
266
266
267 name = 'ipython-notebook'
267 name = 'ipython-notebook'
268 default_config_file_name='ipython_notebook_config.py'
268 default_config_file_name='ipython_notebook_config.py'
269
269
270 description = """
270 description = """
271 The IPython HTML Notebook.
271 The IPython HTML Notebook.
272
272
273 This launches a Tornado based HTML Notebook Server that serves up an
273 This launches a Tornado based HTML Notebook Server that serves up an
274 HTML5/Javascript Notebook client.
274 HTML5/Javascript Notebook client.
275 """
275 """
276 examples = _examples
276 examples = _examples
277
277
278 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
278 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
279 FileNotebookManager]
279 FileNotebookManager]
280 flags = Dict(flags)
280 flags = Dict(flags)
281 aliases = Dict(aliases)
281 aliases = Dict(aliases)
282
282
283 kernel_argv = List(Unicode)
283 kernel_argv = List(Unicode)
284
284
285 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
285 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
286 default_value=logging.INFO,
286 default_value=logging.INFO,
287 config=True,
287 config=True,
288 help="Set the log level by value or name.")
288 help="Set the log level by value or name.")
289
289
290 # create requested profiles by default, if they don't exist:
290 # create requested profiles by default, if they don't exist:
291 auto_create = Bool(True)
291 auto_create = Bool(True)
292
292
293 # file to be opened in the notebook server
293 # file to be opened in the notebook server
294 file_to_run = Unicode('')
294 file_to_run = Unicode('')
295
295
296 # Network related information.
296 # Network related information.
297
297
298 ip = Unicode(LOCALHOST, config=True,
298 ip = Unicode(LOCALHOST, config=True,
299 help="The IP address the notebook server will listen on."
299 help="The IP address the notebook server will listen on."
300 )
300 )
301
301
302 def _ip_changed(self, name, old, new):
302 def _ip_changed(self, name, old, new):
303 if new == u'*': self.ip = u''
303 if new == u'*': self.ip = u''
304
304
305 port = Integer(8888, config=True,
305 port = Integer(8888, config=True,
306 help="The port the notebook server will listen on."
306 help="The port the notebook server will listen on."
307 )
307 )
308 port_retries = Integer(50, config=True,
308 port_retries = Integer(50, config=True,
309 help="The number of additional ports to try if the specified port is not available."
309 help="The number of additional ports to try if the specified port is not available."
310 )
310 )
311
311
312 certfile = Unicode(u'', config=True,
312 certfile = Unicode(u'', config=True,
313 help="""The full path to an SSL/TLS certificate file."""
313 help="""The full path to an SSL/TLS certificate file."""
314 )
314 )
315
315
316 keyfile = Unicode(u'', config=True,
316 keyfile = Unicode(u'', config=True,
317 help="""The full path to a private key file for usage with SSL/TLS."""
317 help="""The full path to a private key file for usage with SSL/TLS."""
318 )
318 )
319
319
320 password = Unicode(u'', config=True,
320 password = Unicode(u'', config=True,
321 help="""Hashed password to use for web authentication.
321 help="""Hashed password to use for web authentication.
322
322
323 To generate, type in a python/IPython shell:
323 To generate, type in a python/IPython shell:
324
324
325 from IPython.lib import passwd; passwd()
325 from IPython.lib import passwd; passwd()
326
326
327 The string should be of the form type:salt:hashed-password.
327 The string should be of the form type:salt:hashed-password.
328 """
328 """
329 )
329 )
330
330
331 open_browser = Bool(True, config=True,
331 open_browser = Bool(True, config=True,
332 help="""Whether to open in a browser after starting.
332 help="""Whether to open in a browser after starting.
333 The specific browser used is platform dependent and
333 The specific browser used is platform dependent and
334 determined by the python standard library `webbrowser`
334 determined by the python standard library `webbrowser`
335 module, unless it is overridden using the --browser
335 module, unless it is overridden using the --browser
336 (NotebookApp.browser) configuration option.
336 (NotebookApp.browser) configuration option.
337 """)
337 """)
338
338
339 browser = Unicode(u'', config=True,
339 browser = Unicode(u'', config=True,
340 help="""Specify what command to use to invoke a web
340 help="""Specify what command to use to invoke a web
341 browser when opening the notebook. If not specified, the
341 browser when opening the notebook. If not specified, the
342 default browser will be determined by the `webbrowser`
342 default browser will be determined by the `webbrowser`
343 standard library module, which allows setting of the
343 standard library module, which allows setting of the
344 BROWSER environment variable to override it.
344 BROWSER environment variable to override it.
345 """)
345 """)
346
346
347 read_only = Bool(False, config=True,
347 read_only = Bool(False, config=True,
348 help="Whether to prevent editing/execution of notebooks."
348 help="Whether to prevent editing/execution of notebooks."
349 )
349 )
350
350
351 use_less = Bool(False, config=True,
351 use_less = Bool(False, config=True,
352 help="""Wether to use Browser Side less-css parsing
352 help="""Wether to use Browser Side less-css parsing
353 instead of compiled css version in templates that allows
353 instead of compiled css version in templates that allows
354 it. This is mainly convenient when working on the less
354 it. This is mainly convenient when working on the less
355 file to avoid a build step, or if user want to overwrite
355 file to avoid a build step, or if user want to overwrite
356 some of the less variables without having to recompile
356 some of the less variables without having to recompile
357 everything.
357 everything.
358
358
359 You will need to install the less.js component in the static directory
359 You will need to install the less.js component in the static directory
360 either in the source tree or in your profile folder.
360 either in the source tree or in your profile folder.
361 """)
361 """)
362
362
363 webapp_settings = Dict(config=True,
363 webapp_settings = Dict(config=True,
364 help="Supply overrides for the tornado.web.Application that the "
364 help="Supply overrides for the tornado.web.Application that the "
365 "IPython notebook uses.")
365 "IPython notebook uses.")
366
366
367 enable_mathjax = Bool(True, config=True,
367 enable_mathjax = Bool(True, config=True,
368 help="""Whether to enable MathJax for typesetting math/TeX
368 help="""Whether to enable MathJax for typesetting math/TeX
369
369
370 MathJax is the javascript library IPython uses to render math/LaTeX. It is
370 MathJax is the javascript library IPython uses to render math/LaTeX. It is
371 very large, so you may want to disable it if you have a slow internet
371 very large, so you may want to disable it if you have a slow internet
372 connection, or for offline use of the notebook.
372 connection, or for offline use of the notebook.
373
373
374 When disabled, equations etc. will appear as their untransformed TeX source.
374 When disabled, equations etc. will appear as their untransformed TeX source.
375 """
375 """
376 )
376 )
377 def _enable_mathjax_changed(self, name, old, new):
377 def _enable_mathjax_changed(self, name, old, new):
378 """set mathjax url to empty if mathjax is disabled"""
378 """set mathjax url to empty if mathjax is disabled"""
379 if not new:
379 if not new:
380 self.mathjax_url = u''
380 self.mathjax_url = u''
381
381
382 base_project_url = Unicode('/', config=True,
382 base_project_url = Unicode('/', config=True,
383 help='''The base URL for the notebook server.
383 help='''The base URL for the notebook server.
384
384
385 Leading and trailing slashes can be omitted,
385 Leading and trailing slashes can be omitted,
386 and will automatically be added.
386 and will automatically be added.
387 ''')
387 ''')
388 def _base_project_url_changed(self, name, old, new):
388 def _base_project_url_changed(self, name, old, new):
389 if not new.startswith('/'):
389 if not new.startswith('/'):
390 self.base_project_url = '/'+new
390 self.base_project_url = '/'+new
391 elif not new.endswith('/'):
391 elif not new.endswith('/'):
392 self.base_project_url = new+'/'
392 self.base_project_url = new+'/'
393
393
394 base_kernel_url = Unicode('/', config=True,
394 base_kernel_url = Unicode('/', config=True,
395 help='''The base URL for the kernel server
395 help='''The base URL for the kernel server
396
396
397 Leading and trailing slashes can be omitted,
397 Leading and trailing slashes can be omitted,
398 and will automatically be added.
398 and will automatically be added.
399 ''')
399 ''')
400 def _base_kernel_url_changed(self, name, old, new):
400 def _base_kernel_url_changed(self, name, old, new):
401 if not new.startswith('/'):
401 if not new.startswith('/'):
402 self.base_kernel_url = '/'+new
402 self.base_kernel_url = '/'+new
403 elif not new.endswith('/'):
403 elif not new.endswith('/'):
404 self.base_kernel_url = new+'/'
404 self.base_kernel_url = new+'/'
405
405
406 websocket_host = Unicode("", config=True,
406 websocket_host = Unicode("", config=True,
407 help="""The hostname for the websocket server."""
407 help="""The hostname for the websocket server."""
408 )
408 )
409
409
410 extra_static_paths = List(Unicode, config=True,
410 extra_static_paths = List(Unicode, config=True,
411 help="""Extra paths to search for serving static files.
411 help="""Extra paths to search for serving static files.
412
412
413 This allows adding javascript/css to be available from the notebook server machine,
413 This allows adding javascript/css to be available from the notebook server machine,
414 or overriding individual files in the IPython"""
414 or overriding individual files in the IPython"""
415 )
415 )
416 def _extra_static_paths_default(self):
416 def _extra_static_paths_default(self):
417 return [os.path.join(self.profile_dir.location, 'static')]
417 return [os.path.join(self.profile_dir.location, 'static')]
418
418
419 @property
419 @property
420 def static_file_path(self):
420 def static_file_path(self):
421 """return extra paths + the default location"""
421 """return extra paths + the default location"""
422 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
422 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
423
423
424 mathjax_url = Unicode("", config=True,
424 mathjax_url = Unicode("", config=True,
425 help="""The url for MathJax.js."""
425 help="""The url for MathJax.js."""
426 )
426 )
427 def _mathjax_url_default(self):
427 def _mathjax_url_default(self):
428 if not self.enable_mathjax:
428 if not self.enable_mathjax:
429 return u''
429 return u''
430 static_url_prefix = self.webapp_settings.get("static_url_prefix",
430 static_url_prefix = self.webapp_settings.get("static_url_prefix",
431 "/static/")
431 "/static/")
432 try:
432 try:
433 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
433 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
434 except IOError:
434 except IOError:
435 if self.certfile:
435 if self.certfile:
436 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
436 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
437 base = u"https://c328740.ssl.cf1.rackcdn.com"
437 base = u"https://c328740.ssl.cf1.rackcdn.com"
438 else:
438 else:
439 base = u"http://cdn.mathjax.org"
439 base = u"http://cdn.mathjax.org"
440
440
441 url = base + u"/mathjax/latest/MathJax.js"
441 url = base + u"/mathjax/latest/MathJax.js"
442 self.log.info("Using MathJax from CDN: %s", url)
442 self.log.info("Using MathJax from CDN: %s", url)
443 return url
443 return url
444 else:
444 else:
445 self.log.info("Using local MathJax from %s" % mathjax)
445 self.log.info("Using local MathJax from %s" % mathjax)
446 return static_url_prefix+u"mathjax/MathJax.js"
446 return static_url_prefix+u"mathjax/MathJax.js"
447
447
448 def _mathjax_url_changed(self, name, old, new):
448 def _mathjax_url_changed(self, name, old, new):
449 if new and not self.enable_mathjax:
449 if new and not self.enable_mathjax:
450 # enable_mathjax=False overrides mathjax_url
450 # enable_mathjax=False overrides mathjax_url
451 self.mathjax_url = u''
451 self.mathjax_url = u''
452 else:
452 else:
453 self.log.info("Using MathJax: %s", new)
453 self.log.info("Using MathJax: %s", new)
454
454
455 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
455 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
456 config=True,
456 config=True,
457 help='The notebook manager class to use.')
457 help='The notebook manager class to use.')
458
458
459 trust_xheaders = Bool(False, config=True,
460 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
461 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
462 )
463
459 def parse_command_line(self, argv=None):
464 def parse_command_line(self, argv=None):
460 super(NotebookApp, self).parse_command_line(argv)
465 super(NotebookApp, self).parse_command_line(argv)
461 if argv is None:
466 if argv is None:
462 argv = sys.argv[1:]
467 argv = sys.argv[1:]
463
468
464 # Scrub frontend-specific flags
469 # Scrub frontend-specific flags
465 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
470 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
466 # Kernel should inherit default config file from frontend
471 # Kernel should inherit default config file from frontend
467 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
472 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
468
473
469 if self.extra_args:
474 if self.extra_args:
470 f = os.path.abspath(self.extra_args[0])
475 f = os.path.abspath(self.extra_args[0])
471 if os.path.isdir(f):
476 if os.path.isdir(f):
472 nbdir = f
477 nbdir = f
473 else:
478 else:
474 self.file_to_run = f
479 self.file_to_run = f
475 nbdir = os.path.dirname(f)
480 nbdir = os.path.dirname(f)
476 self.config.NotebookManager.notebook_dir = nbdir
481 self.config.NotebookManager.notebook_dir = nbdir
477
482
478 def init_configurables(self):
483 def init_configurables(self):
479 # force Session default to be secure
484 # force Session default to be secure
480 default_secure(self.config)
485 default_secure(self.config)
481 self.kernel_manager = MappingKernelManager(
486 self.kernel_manager = MappingKernelManager(
482 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
487 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
483 connection_dir = self.profile_dir.security_dir,
488 connection_dir = self.profile_dir.security_dir,
484 )
489 )
485 kls = import_item(self.notebook_manager_class)
490 kls = import_item(self.notebook_manager_class)
486 self.notebook_manager = kls(config=self.config, log=self.log)
491 self.notebook_manager = kls(config=self.config, log=self.log)
487 self.notebook_manager.load_notebook_names()
492 self.notebook_manager.load_notebook_names()
488 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
493 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
489 self.cluster_manager.update_profiles()
494 self.cluster_manager.update_profiles()
490
495
491 def init_logging(self):
496 def init_logging(self):
492 # This prevents double log messages because tornado use a root logger that
497 # This prevents double log messages because tornado use a root logger that
493 # self.log is a child of. The logging module dipatches log messages to a log
498 # self.log is a child of. The logging module dipatches log messages to a log
494 # and all of its ancenstors until propagate is set to False.
499 # and all of its ancenstors until propagate is set to False.
495 self.log.propagate = False
500 self.log.propagate = False
496
501
497 def init_webapp(self):
502 def init_webapp(self):
498 """initialize tornado webapp and httpserver"""
503 """initialize tornado webapp and httpserver"""
499 self.web_app = NotebookWebApplication(
504 self.web_app = NotebookWebApplication(
500 self, self.kernel_manager, self.notebook_manager,
505 self, self.kernel_manager, self.notebook_manager,
501 self.cluster_manager, self.log,
506 self.cluster_manager, self.log,
502 self.base_project_url, self.webapp_settings
507 self.base_project_url, self.webapp_settings
503 )
508 )
504 if self.certfile:
509 if self.certfile:
505 ssl_options = dict(certfile=self.certfile)
510 ssl_options = dict(certfile=self.certfile)
506 if self.keyfile:
511 if self.keyfile:
507 ssl_options['keyfile'] = self.keyfile
512 ssl_options['keyfile'] = self.keyfile
508 else:
513 else:
509 ssl_options = None
514 ssl_options = None
510 self.web_app.password = self.password
515 self.web_app.password = self.password
511 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
516 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
517 xheaders=self.trust_xheaders)
512 if not self.ip:
518 if not self.ip:
513 warning = "WARNING: The notebook server is listening on all IP addresses"
519 warning = "WARNING: The notebook server is listening on all IP addresses"
514 if ssl_options is None:
520 if ssl_options is None:
515 self.log.critical(warning + " and not using encryption. This"
521 self.log.critical(warning + " and not using encryption. This"
516 "is not recommended.")
522 "is not recommended.")
517 if not self.password and not self.read_only:
523 if not self.password and not self.read_only:
518 self.log.critical(warning + "and not using authentication."
524 self.log.critical(warning + "and not using authentication."
519 "This is highly insecure and not recommended.")
525 "This is highly insecure and not recommended.")
520 success = None
526 success = None
521 for port in random_ports(self.port, self.port_retries+1):
527 for port in random_ports(self.port, self.port_retries+1):
522 try:
528 try:
523 self.http_server.listen(port, self.ip)
529 self.http_server.listen(port, self.ip)
524 except socket.error as e:
530 except socket.error as e:
525 if e.errno != errno.EADDRINUSE:
531 if e.errno != errno.EADDRINUSE:
526 raise
532 raise
527 self.log.info('The port %i is already in use, trying another random port.' % port)
533 self.log.info('The port %i is already in use, trying another random port.' % port)
528 else:
534 else:
529 self.port = port
535 self.port = port
530 success = True
536 success = True
531 break
537 break
532 if not success:
538 if not success:
533 self.log.critical('ERROR: the notebook server could not be started because '
539 self.log.critical('ERROR: the notebook server could not be started because '
534 'no available port could be found.')
540 'no available port could be found.')
535 self.exit(1)
541 self.exit(1)
536
542
537 def init_signal(self):
543 def init_signal(self):
538 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
544 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
539 # safely extract zmq version info:
545 # safely extract zmq version info:
540 try:
546 try:
541 zmq_v = zmq.pyzmq_version_info()
547 zmq_v = zmq.pyzmq_version_info()
542 except AttributeError:
548 except AttributeError:
543 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
549 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
544 if 'dev' in zmq.__version__:
550 if 'dev' in zmq.__version__:
545 zmq_v.append(999)
551 zmq_v.append(999)
546 zmq_v = tuple(zmq_v)
552 zmq_v = tuple(zmq_v)
547 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
553 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
548 # This won't work with 2.1.7 and
554 # This won't work with 2.1.7 and
549 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
555 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
550 # but it will work
556 # but it will work
551 signal.signal(signal.SIGINT, self._handle_sigint)
557 signal.signal(signal.SIGINT, self._handle_sigint)
552 signal.signal(signal.SIGTERM, self._signal_stop)
558 signal.signal(signal.SIGTERM, self._signal_stop)
553 if hasattr(signal, 'SIGUSR1'):
559 if hasattr(signal, 'SIGUSR1'):
554 # Windows doesn't support SIGUSR1
560 # Windows doesn't support SIGUSR1
555 signal.signal(signal.SIGUSR1, self._signal_info)
561 signal.signal(signal.SIGUSR1, self._signal_info)
556 if hasattr(signal, 'SIGINFO'):
562 if hasattr(signal, 'SIGINFO'):
557 # only on BSD-based systems
563 # only on BSD-based systems
558 signal.signal(signal.SIGINFO, self._signal_info)
564 signal.signal(signal.SIGINFO, self._signal_info)
559
565
560 def _handle_sigint(self, sig, frame):
566 def _handle_sigint(self, sig, frame):
561 """SIGINT handler spawns confirmation dialog"""
567 """SIGINT handler spawns confirmation dialog"""
562 # register more forceful signal handler for ^C^C case
568 # register more forceful signal handler for ^C^C case
563 signal.signal(signal.SIGINT, self._signal_stop)
569 signal.signal(signal.SIGINT, self._signal_stop)
564 # request confirmation dialog in bg thread, to avoid
570 # request confirmation dialog in bg thread, to avoid
565 # blocking the App
571 # blocking the App
566 thread = threading.Thread(target=self._confirm_exit)
572 thread = threading.Thread(target=self._confirm_exit)
567 thread.daemon = True
573 thread.daemon = True
568 thread.start()
574 thread.start()
569
575
570 def _restore_sigint_handler(self):
576 def _restore_sigint_handler(self):
571 """callback for restoring original SIGINT handler"""
577 """callback for restoring original SIGINT handler"""
572 signal.signal(signal.SIGINT, self._handle_sigint)
578 signal.signal(signal.SIGINT, self._handle_sigint)
573
579
574 def _confirm_exit(self):
580 def _confirm_exit(self):
575 """confirm shutdown on ^C
581 """confirm shutdown on ^C
576
582
577 A second ^C, or answering 'y' within 5s will cause shutdown,
583 A second ^C, or answering 'y' within 5s will cause shutdown,
578 otherwise original SIGINT handler will be restored.
584 otherwise original SIGINT handler will be restored.
579
585
580 This doesn't work on Windows.
586 This doesn't work on Windows.
581 """
587 """
582 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
588 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
583 time.sleep(0.1)
589 time.sleep(0.1)
584 info = self.log.info
590 info = self.log.info
585 info('interrupted')
591 info('interrupted')
586 print self.notebook_info()
592 print self.notebook_info()
587 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
593 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
588 sys.stdout.flush()
594 sys.stdout.flush()
589 r,w,x = select.select([sys.stdin], [], [], 5)
595 r,w,x = select.select([sys.stdin], [], [], 5)
590 if r:
596 if r:
591 line = sys.stdin.readline()
597 line = sys.stdin.readline()
592 if line.lower().startswith('y'):
598 if line.lower().startswith('y'):
593 self.log.critical("Shutdown confirmed")
599 self.log.critical("Shutdown confirmed")
594 ioloop.IOLoop.instance().stop()
600 ioloop.IOLoop.instance().stop()
595 return
601 return
596 else:
602 else:
597 print "No answer for 5s:",
603 print "No answer for 5s:",
598 print "resuming operation..."
604 print "resuming operation..."
599 # no answer, or answer is no:
605 # no answer, or answer is no:
600 # set it back to original SIGINT handler
606 # set it back to original SIGINT handler
601 # use IOLoop.add_callback because signal.signal must be called
607 # use IOLoop.add_callback because signal.signal must be called
602 # from main thread
608 # from main thread
603 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
609 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
604
610
605 def _signal_stop(self, sig, frame):
611 def _signal_stop(self, sig, frame):
606 self.log.critical("received signal %s, stopping", sig)
612 self.log.critical("received signal %s, stopping", sig)
607 ioloop.IOLoop.instance().stop()
613 ioloop.IOLoop.instance().stop()
608
614
609 def _signal_info(self, sig, frame):
615 def _signal_info(self, sig, frame):
610 print self.notebook_info()
616 print self.notebook_info()
611
617
612 @catch_config_error
618 @catch_config_error
613 def initialize(self, argv=None):
619 def initialize(self, argv=None):
614 self.init_logging()
620 self.init_logging()
615 super(NotebookApp, self).initialize(argv)
621 super(NotebookApp, self).initialize(argv)
616 self.init_configurables()
622 self.init_configurables()
617 self.init_webapp()
623 self.init_webapp()
618 self.init_signal()
624 self.init_signal()
619
625
620 def cleanup_kernels(self):
626 def cleanup_kernels(self):
621 """Shutdown all kernels.
627 """Shutdown all kernels.
622
628
623 The kernels will shutdown themselves when this process no longer exists,
629 The kernels will shutdown themselves when this process no longer exists,
624 but explicit shutdown allows the KernelManagers to cleanup the connection files.
630 but explicit shutdown allows the KernelManagers to cleanup the connection files.
625 """
631 """
626 self.log.info('Shutting down kernels')
632 self.log.info('Shutting down kernels')
627 self.kernel_manager.shutdown_all()
633 self.kernel_manager.shutdown_all()
628
634
629 def notebook_info(self):
635 def notebook_info(self):
630 "Return the current working directory and the server url information"
636 "Return the current working directory and the server url information"
631 mgr_info = self.notebook_manager.info_string() + "\n"
637 mgr_info = self.notebook_manager.info_string() + "\n"
632 return mgr_info +"The IPython Notebook is running at: %s" % self._url
638 return mgr_info +"The IPython Notebook is running at: %s" % self._url
633
639
634 def start(self):
640 def start(self):
635 """ Start the IPython Notebok server app, after initialization
641 """ Start the IPython Notebok server app, after initialization
636
642
637 This method takes no arguments so all configuration and initialization
643 This method takes no arguments so all configuration and initialization
638 must be done prior to calling this method."""
644 must be done prior to calling this method."""
639 ip = self.ip if self.ip else '[all ip addresses on your system]'
645 ip = self.ip if self.ip else '[all ip addresses on your system]'
640 proto = 'https' if self.certfile else 'http'
646 proto = 'https' if self.certfile else 'http'
641 info = self.log.info
647 info = self.log.info
642 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
648 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
643 self.base_project_url)
649 self.base_project_url)
644 for line in self.notebook_info().split("\n"):
650 for line in self.notebook_info().split("\n"):
645 info(line)
651 info(line)
646 info("Use Control-C to stop this server and shut down all kernels.")
652 info("Use Control-C to stop this server and shut down all kernels.")
647
653
648 if self.open_browser or self.file_to_run:
654 if self.open_browser or self.file_to_run:
649 ip = self.ip or LOCALHOST
655 ip = self.ip or LOCALHOST
650 try:
656 try:
651 browser = webbrowser.get(self.browser or None)
657 browser = webbrowser.get(self.browser or None)
652 except webbrowser.Error as e:
658 except webbrowser.Error as e:
653 self.log.warn('No web browser found: %s.' % e)
659 self.log.warn('No web browser found: %s.' % e)
654 browser = None
660 browser = None
655
661
656 if self.file_to_run:
662 if self.file_to_run:
657 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
663 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
658 url = self.notebook_manager.rev_mapping.get(name, '')
664 url = self.notebook_manager.rev_mapping.get(name, '')
659 else:
665 else:
660 url = ''
666 url = ''
661 if browser:
667 if browser:
662 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
668 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
663 self.port, self.base_project_url, url), new=2)
669 self.port, self.base_project_url, url), new=2)
664 threading.Thread(target=b).start()
670 threading.Thread(target=b).start()
665 try:
671 try:
666 ioloop.IOLoop.instance().start()
672 ioloop.IOLoop.instance().start()
667 except KeyboardInterrupt:
673 except KeyboardInterrupt:
668 info("Interrupted...")
674 info("Interrupted...")
669 finally:
675 finally:
670 self.cleanup_kernels()
676 self.cleanup_kernels()
671
677
672
678
673 #-----------------------------------------------------------------------------
679 #-----------------------------------------------------------------------------
674 # Main entry point
680 # Main entry point
675 #-----------------------------------------------------------------------------
681 #-----------------------------------------------------------------------------
676
682
677 def launch_new_instance():
683 def launch_new_instance():
678 app = NotebookApp.instance()
684 app = NotebookApp.instance()
679 app.initialize()
685 app.initialize()
680 app.start()
686 app.start()
681
687
General Comments 0
You need to be logged in to leave comments. Login now