##// END OF EJS Templates
use notebook-dir as cwd for kernels
MinRK -
Show More
@@ -1,737 +1,738 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
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 import logging
19 import logging
20 import Cookie
20 import Cookie
21 import time
21 import time
22 import uuid
22 import uuid
23
23
24 from tornado import web
24 from tornado import web
25 from tornado import websocket
25 from tornado import websocket
26
26
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
29
29
30 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
33 from IPython.utils.jsonutil import date_default
33 from IPython.utils.jsonutil import date_default
34
34
35 try:
35 try:
36 from docutils.core import publish_string
36 from docutils.core import publish_string
37 except ImportError:
37 except ImportError:
38 publish_string = None
38 publish_string = None
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # parts tornado cares about haven't really changed, so it's OK to continue
45 # parts tornado cares about haven't really changed, so it's OK to continue
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # version as of Oct 30/2011) the version check fails, see the issue report:
47 # version as of Oct 30/2011) the version check fails, see the issue report:
48
48
49 # https://github.com/facebook/tornado/issues/385
49 # https://github.com/facebook/tornado/issues/385
50
50
51 # This issue has been fixed in Tornado post 2.1.1:
51 # This issue has been fixed in Tornado post 2.1.1:
52
52
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54
54
55 # Here we manually apply the same patch as above so that users of IPython can
55 # Here we manually apply the same patch as above so that users of IPython can
56 # continue to work with an officially released Tornado. We make the
56 # continue to work with an officially released Tornado. We make the
57 # monkeypatch version check as narrow as possible to limit its effects; once
57 # monkeypatch version check as narrow as possible to limit its effects; once
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59
59
60 import tornado
60 import tornado
61
61
62 if tornado.version_info <= (2,1,1):
62 if tornado.version_info <= (2,1,1):
63
63
64 def _execute(self, transforms, *args, **kwargs):
64 def _execute(self, transforms, *args, **kwargs):
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66
66
67 self.open_args = args
67 self.open_args = args
68 self.open_kwargs = kwargs
68 self.open_kwargs = kwargs
69
69
70 # The difference between version 8 and 13 is that in 8 the
70 # The difference between version 8 and 13 is that in 8 the
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # simply "Origin".
72 # simply "Origin".
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection.accept_connection()
75 self.ws_connection.accept_connection()
76
76
77 elif self.request.headers.get("Sec-WebSocket-Version"):
77 elif self.request.headers.get("Sec-WebSocket-Version"):
78 self.stream.write(tornado.escape.utf8(
78 self.stream.write(tornado.escape.utf8(
79 "HTTP/1.1 426 Upgrade Required\r\n"
79 "HTTP/1.1 426 Upgrade Required\r\n"
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 self.stream.close()
81 self.stream.close()
82
82
83 else:
83 else:
84 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 websocket.WebSocketHandler._execute = _execute
87 websocket.WebSocketHandler._execute = _execute
88 del _execute
88 del _execute
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Decorator for disabling read-only handlers
91 # Decorator for disabling read-only handlers
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94 @decorator
94 @decorator
95 def not_if_readonly(f, self, *args, **kwargs):
95 def not_if_readonly(f, self, *args, **kwargs):
96 if self.application.read_only:
96 if self.application.read_only:
97 raise web.HTTPError(403, "Notebook server is read-only")
97 raise web.HTTPError(403, "Notebook server is read-only")
98 else:
98 else:
99 return f(self, *args, **kwargs)
99 return f(self, *args, **kwargs)
100
100
101 @decorator
101 @decorator
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 """authenticate this page *unless* readonly view is active.
103 """authenticate this page *unless* readonly view is active.
104
104
105 In read-only mode, the notebook list and print view should
105 In read-only mode, the notebook list and print view should
106 be accessible without authentication.
106 be accessible without authentication.
107 """
107 """
108
108
109 @web.authenticated
109 @web.authenticated
110 def auth_f(self, *args, **kwargs):
110 def auth_f(self, *args, **kwargs):
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112
112
113 if self.application.read_only:
113 if self.application.read_only:
114 return f(self, *args, **kwargs)
114 return f(self, *args, **kwargs)
115 else:
115 else:
116 return auth_f(self, *args, **kwargs)
116 return auth_f(self, *args, **kwargs)
117
117
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119 # Top-level handlers
119 # Top-level handlers
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121
121
122 class RequestHandler(web.RequestHandler):
122 class RequestHandler(web.RequestHandler):
123 """RequestHandler with default variable setting."""
123 """RequestHandler with default variable setting."""
124
124
125 def render(*args, **kwargs):
125 def render(*args, **kwargs):
126 kwargs.setdefault('message', '')
126 kwargs.setdefault('message', '')
127 return web.RequestHandler.render(*args, **kwargs)
127 return web.RequestHandler.render(*args, **kwargs)
128
128
129 class AuthenticatedHandler(RequestHandler):
129 class AuthenticatedHandler(RequestHandler):
130 """A RequestHandler with an authenticated user."""
130 """A RequestHandler with an authenticated user."""
131
131
132 def get_current_user(self):
132 def get_current_user(self):
133 user_id = self.get_secure_cookie("username")
133 user_id = self.get_secure_cookie("username")
134 # For now the user_id should not return empty, but it could eventually
134 # For now the user_id should not return empty, but it could eventually
135 if user_id == '':
135 if user_id == '':
136 user_id = 'anonymous'
136 user_id = 'anonymous'
137 if user_id is None:
137 if user_id is None:
138 # prevent extra Invalid cookie sig warnings:
138 # prevent extra Invalid cookie sig warnings:
139 self.clear_cookie('username')
139 self.clear_cookie('username')
140 if not self.application.password and not self.application.read_only:
140 if not self.application.password and not self.application.read_only:
141 user_id = 'anonymous'
141 user_id = 'anonymous'
142 return user_id
142 return user_id
143
143
144 @property
144 @property
145 def logged_in(self):
145 def logged_in(self):
146 """Is a user currently logged in?
146 """Is a user currently logged in?
147
147
148 """
148 """
149 user = self.get_current_user()
149 user = self.get_current_user()
150 return (user and not user == 'anonymous')
150 return (user and not user == 'anonymous')
151
151
152 @property
152 @property
153 def login_available(self):
153 def login_available(self):
154 """May a user proceed to log in?
154 """May a user proceed to log in?
155
155
156 This returns True if login capability is available, irrespective of
156 This returns True if login capability is available, irrespective of
157 whether the user is already logged in or not.
157 whether the user is already logged in or not.
158
158
159 """
159 """
160 return bool(self.application.password)
160 return bool(self.application.password)
161
161
162 @property
162 @property
163 def read_only(self):
163 def read_only(self):
164 """Is the notebook read-only?
164 """Is the notebook read-only?
165
165
166 """
166 """
167 return self.application.read_only
167 return self.application.read_only
168
168
169 @property
169 @property
170 def ws_url(self):
170 def ws_url(self):
171 """websocket url matching the current request
171 """websocket url matching the current request
172
172
173 turns http[s]://host[:port] into
173 turns http[s]://host[:port] into
174 ws[s]://host[:port]
174 ws[s]://host[:port]
175 """
175 """
176 proto = self.request.protocol.replace('http', 'ws')
176 proto = self.request.protocol.replace('http', 'ws')
177 host = self.application.ipython_app.websocket_host # default to config value
177 host = self.application.ipython_app.websocket_host # default to config value
178 if host == '':
178 if host == '':
179 host = self.request.host # get from request
179 host = self.request.host # get from request
180 return "%s://%s" % (proto, host)
180 return "%s://%s" % (proto, host)
181
181
182
182
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
184 """static files should only be accessible when logged in"""
184 """static files should only be accessible when logged in"""
185
185
186 @authenticate_unless_readonly
186 @authenticate_unless_readonly
187 def get(self, path):
187 def get(self, path):
188 return web.StaticFileHandler.get(self, path)
188 return web.StaticFileHandler.get(self, path)
189
189
190
190
191 class ProjectDashboardHandler(AuthenticatedHandler):
191 class ProjectDashboardHandler(AuthenticatedHandler):
192
192
193 @authenticate_unless_readonly
193 @authenticate_unless_readonly
194 def get(self):
194 def get(self):
195 nbm = self.application.notebook_manager
195 nbm = self.application.notebook_manager
196 project = nbm.notebook_dir
196 project = nbm.notebook_dir
197 self.render(
197 self.render(
198 'projectdashboard.html', project=project,
198 'projectdashboard.html', project=project,
199 base_project_url=self.application.ipython_app.base_project_url,
199 base_project_url=self.application.ipython_app.base_project_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
201 read_only=self.read_only,
201 read_only=self.read_only,
202 logged_in=self.logged_in,
202 logged_in=self.logged_in,
203 login_available=self.login_available
203 login_available=self.login_available
204 )
204 )
205
205
206
206
207 class LoginHandler(AuthenticatedHandler):
207 class LoginHandler(AuthenticatedHandler):
208
208
209 def _render(self, message=None):
209 def _render(self, message=None):
210 self.render('login.html',
210 self.render('login.html',
211 next=self.get_argument('next', default='/'),
211 next=self.get_argument('next', default='/'),
212 read_only=self.read_only,
212 read_only=self.read_only,
213 logged_in=self.logged_in,
213 logged_in=self.logged_in,
214 login_available=self.login_available,
214 login_available=self.login_available,
215 base_project_url=self.application.ipython_app.base_project_url,
215 base_project_url=self.application.ipython_app.base_project_url,
216 message=message
216 message=message
217 )
217 )
218
218
219 def get(self):
219 def get(self):
220 if self.current_user:
220 if self.current_user:
221 self.redirect(self.get_argument('next', default='/'))
221 self.redirect(self.get_argument('next', default='/'))
222 else:
222 else:
223 self._render()
223 self._render()
224
224
225 def post(self):
225 def post(self):
226 pwd = self.get_argument('password', default=u'')
226 pwd = self.get_argument('password', default=u'')
227 if self.application.password:
227 if self.application.password:
228 if passwd_check(self.application.password, pwd):
228 if passwd_check(self.application.password, pwd):
229 self.set_secure_cookie('username', str(uuid.uuid4()))
229 self.set_secure_cookie('username', str(uuid.uuid4()))
230 else:
230 else:
231 self._render(message={'error': 'Invalid password'})
231 self._render(message={'error': 'Invalid password'})
232 return
232 return
233
233
234 self.redirect(self.get_argument('next', default='/'))
234 self.redirect(self.get_argument('next', default='/'))
235
235
236
236
237 class LogoutHandler(AuthenticatedHandler):
237 class LogoutHandler(AuthenticatedHandler):
238
238
239 def get(self):
239 def get(self):
240 self.clear_cookie('username')
240 self.clear_cookie('username')
241 if self.login_available:
241 if self.login_available:
242 message = {'info': 'Successfully logged out.'}
242 message = {'info': 'Successfully logged out.'}
243 else:
243 else:
244 message = {'warning': 'Cannot log out. Notebook authentication '
244 message = {'warning': 'Cannot log out. Notebook authentication '
245 'is disabled.'}
245 'is disabled.'}
246
246
247 self.render('logout.html',
247 self.render('logout.html',
248 read_only=self.read_only,
248 read_only=self.read_only,
249 logged_in=self.logged_in,
249 logged_in=self.logged_in,
250 login_available=self.login_available,
250 login_available=self.login_available,
251 base_project_url=self.application.ipython_app.base_project_url,
251 base_project_url=self.application.ipython_app.base_project_url,
252 message=message)
252 message=message)
253
253
254
254
255 class NewHandler(AuthenticatedHandler):
255 class NewHandler(AuthenticatedHandler):
256
256
257 @web.authenticated
257 @web.authenticated
258 def get(self):
258 def get(self):
259 nbm = self.application.notebook_manager
259 nbm = self.application.notebook_manager
260 project = nbm.notebook_dir
260 project = nbm.notebook_dir
261 notebook_id = nbm.new_notebook()
261 notebook_id = nbm.new_notebook()
262 self.render(
262 self.render(
263 'notebook.html', project=project,
263 'notebook.html', project=project,
264 notebook_id=notebook_id,
264 notebook_id=notebook_id,
265 base_project_url=self.application.ipython_app.base_project_url,
265 base_project_url=self.application.ipython_app.base_project_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
267 kill_kernel=False,
267 kill_kernel=False,
268 read_only=False,
268 read_only=False,
269 logged_in=self.logged_in,
269 logged_in=self.logged_in,
270 login_available=self.login_available,
270 login_available=self.login_available,
271 mathjax_url=self.application.ipython_app.mathjax_url,
271 mathjax_url=self.application.ipython_app.mathjax_url,
272 )
272 )
273
273
274
274
275 class NamedNotebookHandler(AuthenticatedHandler):
275 class NamedNotebookHandler(AuthenticatedHandler):
276
276
277 @authenticate_unless_readonly
277 @authenticate_unless_readonly
278 def get(self, notebook_id):
278 def get(self, notebook_id):
279 nbm = self.application.notebook_manager
279 nbm = self.application.notebook_manager
280 project = nbm.notebook_dir
280 project = nbm.notebook_dir
281 if not nbm.notebook_exists(notebook_id):
281 if not nbm.notebook_exists(notebook_id):
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
283
283
284 self.render(
284 self.render(
285 'notebook.html', project=project,
285 'notebook.html', project=project,
286 notebook_id=notebook_id,
286 notebook_id=notebook_id,
287 base_project_url=self.application.ipython_app.base_project_url,
287 base_project_url=self.application.ipython_app.base_project_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
289 kill_kernel=False,
289 kill_kernel=False,
290 read_only=self.read_only,
290 read_only=self.read_only,
291 logged_in=self.logged_in,
291 logged_in=self.logged_in,
292 login_available=self.login_available,
292 login_available=self.login_available,
293 mathjax_url=self.application.ipython_app.mathjax_url,
293 mathjax_url=self.application.ipython_app.mathjax_url,
294 )
294 )
295
295
296
296
297 class PrintNotebookHandler(AuthenticatedHandler):
297 class PrintNotebookHandler(AuthenticatedHandler):
298
298
299 @authenticate_unless_readonly
299 @authenticate_unless_readonly
300 def get(self, notebook_id):
300 def get(self, notebook_id):
301 nbm = self.application.notebook_manager
301 nbm = self.application.notebook_manager
302 project = nbm.notebook_dir
302 project = nbm.notebook_dir
303 if not nbm.notebook_exists(notebook_id):
303 if not nbm.notebook_exists(notebook_id):
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
305
305
306 self.render(
306 self.render(
307 'printnotebook.html', project=project,
307 'printnotebook.html', project=project,
308 notebook_id=notebook_id,
308 notebook_id=notebook_id,
309 base_project_url=self.application.ipython_app.base_project_url,
309 base_project_url=self.application.ipython_app.base_project_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
311 kill_kernel=False,
311 kill_kernel=False,
312 read_only=self.read_only,
312 read_only=self.read_only,
313 logged_in=self.logged_in,
313 logged_in=self.logged_in,
314 login_available=self.login_available,
314 login_available=self.login_available,
315 mathjax_url=self.application.ipython_app.mathjax_url,
315 mathjax_url=self.application.ipython_app.mathjax_url,
316 )
316 )
317
317
318 #-----------------------------------------------------------------------------
318 #-----------------------------------------------------------------------------
319 # Kernel handlers
319 # Kernel handlers
320 #-----------------------------------------------------------------------------
320 #-----------------------------------------------------------------------------
321
321
322
322
323 class MainKernelHandler(AuthenticatedHandler):
323 class MainKernelHandler(AuthenticatedHandler):
324
324
325 @web.authenticated
325 @web.authenticated
326 def get(self):
326 def get(self):
327 km = self.application.kernel_manager
327 km = self.application.kernel_manager
328 self.finish(jsonapi.dumps(km.kernel_ids))
328 self.finish(jsonapi.dumps(km.kernel_ids))
329
329
330 @web.authenticated
330 @web.authenticated
331 def post(self):
331 def post(self):
332 km = self.application.kernel_manager
332 km = self.application.kernel_manager
333 nbm = self.application.notebook_manager
333 notebook_id = self.get_argument('notebook', default=None)
334 notebook_id = self.get_argument('notebook', default=None)
334 kernel_id = km.start_kernel(notebook_id)
335 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
335 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
336 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
336 self.set_header('Location', '/'+kernel_id)
337 self.set_header('Location', '/'+kernel_id)
337 self.finish(jsonapi.dumps(data))
338 self.finish(jsonapi.dumps(data))
338
339
339
340
340 class KernelHandler(AuthenticatedHandler):
341 class KernelHandler(AuthenticatedHandler):
341
342
342 SUPPORTED_METHODS = ('DELETE')
343 SUPPORTED_METHODS = ('DELETE')
343
344
344 @web.authenticated
345 @web.authenticated
345 def delete(self, kernel_id):
346 def delete(self, kernel_id):
346 km = self.application.kernel_manager
347 km = self.application.kernel_manager
347 km.kill_kernel(kernel_id)
348 km.kill_kernel(kernel_id)
348 self.set_status(204)
349 self.set_status(204)
349 self.finish()
350 self.finish()
350
351
351
352
352 class KernelActionHandler(AuthenticatedHandler):
353 class KernelActionHandler(AuthenticatedHandler):
353
354
354 @web.authenticated
355 @web.authenticated
355 def post(self, kernel_id, action):
356 def post(self, kernel_id, action):
356 km = self.application.kernel_manager
357 km = self.application.kernel_manager
357 if action == 'interrupt':
358 if action == 'interrupt':
358 km.interrupt_kernel(kernel_id)
359 km.interrupt_kernel(kernel_id)
359 self.set_status(204)
360 self.set_status(204)
360 if action == 'restart':
361 if action == 'restart':
361 new_kernel_id = km.restart_kernel(kernel_id)
362 new_kernel_id = km.restart_kernel(kernel_id)
362 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
363 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
363 self.set_header('Location', '/'+new_kernel_id)
364 self.set_header('Location', '/'+new_kernel_id)
364 self.write(jsonapi.dumps(data))
365 self.write(jsonapi.dumps(data))
365 self.finish()
366 self.finish()
366
367
367
368
368 class ZMQStreamHandler(websocket.WebSocketHandler):
369 class ZMQStreamHandler(websocket.WebSocketHandler):
369
370
370 def _reserialize_reply(self, msg_list):
371 def _reserialize_reply(self, msg_list):
371 """Reserialize a reply message using JSON.
372 """Reserialize a reply message using JSON.
372
373
373 This takes the msg list from the ZMQ socket, unserializes it using
374 This takes the msg list from the ZMQ socket, unserializes it using
374 self.session and then serializes the result using JSON. This method
375 self.session and then serializes the result using JSON. This method
375 should be used by self._on_zmq_reply to build messages that can
376 should be used by self._on_zmq_reply to build messages that can
376 be sent back to the browser.
377 be sent back to the browser.
377 """
378 """
378 idents, msg_list = self.session.feed_identities(msg_list)
379 idents, msg_list = self.session.feed_identities(msg_list)
379 msg = self.session.unserialize(msg_list)
380 msg = self.session.unserialize(msg_list)
380 try:
381 try:
381 msg['header'].pop('date')
382 msg['header'].pop('date')
382 except KeyError:
383 except KeyError:
383 pass
384 pass
384 try:
385 try:
385 msg['parent_header'].pop('date')
386 msg['parent_header'].pop('date')
386 except KeyError:
387 except KeyError:
387 pass
388 pass
388 msg.pop('buffers')
389 msg.pop('buffers')
389 return jsonapi.dumps(msg, default=date_default)
390 return jsonapi.dumps(msg, default=date_default)
390
391
391 def _on_zmq_reply(self, msg_list):
392 def _on_zmq_reply(self, msg_list):
392 try:
393 try:
393 msg = self._reserialize_reply(msg_list)
394 msg = self._reserialize_reply(msg_list)
394 except Exception:
395 except Exception:
395 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
396 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
396 else:
397 else:
397 self.write_message(msg)
398 self.write_message(msg)
398
399
399 def allow_draft76(self):
400 def allow_draft76(self):
400 """Allow draft 76, until browsers such as Safari update to RFC 6455.
401 """Allow draft 76, until browsers such as Safari update to RFC 6455.
401
402
402 This has been disabled by default in tornado in release 2.2.0, and
403 This has been disabled by default in tornado in release 2.2.0, and
403 support will be removed in later versions.
404 support will be removed in later versions.
404 """
405 """
405 return True
406 return True
406
407
407
408
408 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
409 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
409
410
410 def open(self, kernel_id):
411 def open(self, kernel_id):
411 self.kernel_id = kernel_id.decode('ascii')
412 self.kernel_id = kernel_id.decode('ascii')
412 try:
413 try:
413 cfg = self.application.ipython_app.config
414 cfg = self.application.ipython_app.config
414 except AttributeError:
415 except AttributeError:
415 # protect from the case where this is run from something other than
416 # protect from the case where this is run from something other than
416 # the notebook app:
417 # the notebook app:
417 cfg = None
418 cfg = None
418 self.session = Session(config=cfg)
419 self.session = Session(config=cfg)
419 self.save_on_message = self.on_message
420 self.save_on_message = self.on_message
420 self.on_message = self.on_first_message
421 self.on_message = self.on_first_message
421
422
422 def get_current_user(self):
423 def get_current_user(self):
423 user_id = self.get_secure_cookie("username")
424 user_id = self.get_secure_cookie("username")
424 if user_id == '' or (user_id is None and not self.application.password):
425 if user_id == '' or (user_id is None and not self.application.password):
425 user_id = 'anonymous'
426 user_id = 'anonymous'
426 return user_id
427 return user_id
427
428
428 def _inject_cookie_message(self, msg):
429 def _inject_cookie_message(self, msg):
429 """Inject the first message, which is the document cookie,
430 """Inject the first message, which is the document cookie,
430 for authentication."""
431 for authentication."""
431 if isinstance(msg, unicode):
432 if isinstance(msg, unicode):
432 # Cookie can't constructor doesn't accept unicode strings for some reason
433 # Cookie can't constructor doesn't accept unicode strings for some reason
433 msg = msg.encode('utf8', 'replace')
434 msg = msg.encode('utf8', 'replace')
434 try:
435 try:
435 self.request._cookies = Cookie.SimpleCookie(msg)
436 self.request._cookies = Cookie.SimpleCookie(msg)
436 except:
437 except:
437 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
438 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
438
439
439 def on_first_message(self, msg):
440 def on_first_message(self, msg):
440 self._inject_cookie_message(msg)
441 self._inject_cookie_message(msg)
441 if self.get_current_user() is None:
442 if self.get_current_user() is None:
442 logging.warn("Couldn't authenticate WebSocket connection")
443 logging.warn("Couldn't authenticate WebSocket connection")
443 raise web.HTTPError(403)
444 raise web.HTTPError(403)
444 self.on_message = self.save_on_message
445 self.on_message = self.save_on_message
445
446
446
447
447 class IOPubHandler(AuthenticatedZMQStreamHandler):
448 class IOPubHandler(AuthenticatedZMQStreamHandler):
448
449
449 def initialize(self, *args, **kwargs):
450 def initialize(self, *args, **kwargs):
450 self._kernel_alive = True
451 self._kernel_alive = True
451 self._beating = False
452 self._beating = False
452 self.iopub_stream = None
453 self.iopub_stream = None
453 self.hb_stream = None
454 self.hb_stream = None
454
455
455 def on_first_message(self, msg):
456 def on_first_message(self, msg):
456 try:
457 try:
457 super(IOPubHandler, self).on_first_message(msg)
458 super(IOPubHandler, self).on_first_message(msg)
458 except web.HTTPError:
459 except web.HTTPError:
459 self.close()
460 self.close()
460 return
461 return
461 km = self.application.kernel_manager
462 km = self.application.kernel_manager
462 self.time_to_dead = km.time_to_dead
463 self.time_to_dead = km.time_to_dead
463 self.first_beat = km.first_beat
464 self.first_beat = km.first_beat
464 kernel_id = self.kernel_id
465 kernel_id = self.kernel_id
465 try:
466 try:
466 self.iopub_stream = km.create_iopub_stream(kernel_id)
467 self.iopub_stream = km.create_iopub_stream(kernel_id)
467 self.hb_stream = km.create_hb_stream(kernel_id)
468 self.hb_stream = km.create_hb_stream(kernel_id)
468 except web.HTTPError:
469 except web.HTTPError:
469 # WebSockets don't response to traditional error codes so we
470 # WebSockets don't response to traditional error codes so we
470 # close the connection.
471 # close the connection.
471 if not self.stream.closed():
472 if not self.stream.closed():
472 self.stream.close()
473 self.stream.close()
473 self.close()
474 self.close()
474 else:
475 else:
475 self.iopub_stream.on_recv(self._on_zmq_reply)
476 self.iopub_stream.on_recv(self._on_zmq_reply)
476 self.start_hb(self.kernel_died)
477 self.start_hb(self.kernel_died)
477
478
478 def on_message(self, msg):
479 def on_message(self, msg):
479 pass
480 pass
480
481
481 def on_close(self):
482 def on_close(self):
482 # This method can be called twice, once by self.kernel_died and once
483 # This method can be called twice, once by self.kernel_died and once
483 # from the WebSocket close event. If the WebSocket connection is
484 # from the WebSocket close event. If the WebSocket connection is
484 # closed before the ZMQ streams are setup, they could be None.
485 # closed before the ZMQ streams are setup, they could be None.
485 self.stop_hb()
486 self.stop_hb()
486 if self.iopub_stream is not None and not self.iopub_stream.closed():
487 if self.iopub_stream is not None and not self.iopub_stream.closed():
487 self.iopub_stream.on_recv(None)
488 self.iopub_stream.on_recv(None)
488 self.iopub_stream.close()
489 self.iopub_stream.close()
489 if self.hb_stream is not None and not self.hb_stream.closed():
490 if self.hb_stream is not None and not self.hb_stream.closed():
490 self.hb_stream.close()
491 self.hb_stream.close()
491
492
492 def start_hb(self, callback):
493 def start_hb(self, callback):
493 """Start the heartbeating and call the callback if the kernel dies."""
494 """Start the heartbeating and call the callback if the kernel dies."""
494 if not self._beating:
495 if not self._beating:
495 self._kernel_alive = True
496 self._kernel_alive = True
496
497
497 def ping_or_dead():
498 def ping_or_dead():
498 self.hb_stream.flush()
499 self.hb_stream.flush()
499 if self._kernel_alive:
500 if self._kernel_alive:
500 self._kernel_alive = False
501 self._kernel_alive = False
501 self.hb_stream.send(b'ping')
502 self.hb_stream.send(b'ping')
502 # flush stream to force immediate socket send
503 # flush stream to force immediate socket send
503 self.hb_stream.flush()
504 self.hb_stream.flush()
504 else:
505 else:
505 try:
506 try:
506 callback()
507 callback()
507 except:
508 except:
508 pass
509 pass
509 finally:
510 finally:
510 self.stop_hb()
511 self.stop_hb()
511
512
512 def beat_received(msg):
513 def beat_received(msg):
513 self._kernel_alive = True
514 self._kernel_alive = True
514
515
515 self.hb_stream.on_recv(beat_received)
516 self.hb_stream.on_recv(beat_received)
516 loop = ioloop.IOLoop.instance()
517 loop = ioloop.IOLoop.instance()
517 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
518 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
518 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
519 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
519 self._beating= True
520 self._beating= True
520
521
521 def _really_start_hb(self):
522 def _really_start_hb(self):
522 """callback for delayed heartbeat start
523 """callback for delayed heartbeat start
523
524
524 Only start the hb loop if we haven't been closed during the wait.
525 Only start the hb loop if we haven't been closed during the wait.
525 """
526 """
526 if self._beating and not self.hb_stream.closed():
527 if self._beating and not self.hb_stream.closed():
527 self._hb_periodic_callback.start()
528 self._hb_periodic_callback.start()
528
529
529 def stop_hb(self):
530 def stop_hb(self):
530 """Stop the heartbeating and cancel all related callbacks."""
531 """Stop the heartbeating and cancel all related callbacks."""
531 if self._beating:
532 if self._beating:
532 self._beating = False
533 self._beating = False
533 self._hb_periodic_callback.stop()
534 self._hb_periodic_callback.stop()
534 if not self.hb_stream.closed():
535 if not self.hb_stream.closed():
535 self.hb_stream.on_recv(None)
536 self.hb_stream.on_recv(None)
536
537
537 def kernel_died(self):
538 def kernel_died(self):
538 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
539 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
539 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
540 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
540 self.write_message(
541 self.write_message(
541 {'header': {'msg_type': 'status'},
542 {'header': {'msg_type': 'status'},
542 'parent_header': {},
543 'parent_header': {},
543 'content': {'execution_state':'dead'}
544 'content': {'execution_state':'dead'}
544 }
545 }
545 )
546 )
546 self.on_close()
547 self.on_close()
547
548
548
549
549 class ShellHandler(AuthenticatedZMQStreamHandler):
550 class ShellHandler(AuthenticatedZMQStreamHandler):
550
551
551 def initialize(self, *args, **kwargs):
552 def initialize(self, *args, **kwargs):
552 self.shell_stream = None
553 self.shell_stream = None
553
554
554 def on_first_message(self, msg):
555 def on_first_message(self, msg):
555 try:
556 try:
556 super(ShellHandler, self).on_first_message(msg)
557 super(ShellHandler, self).on_first_message(msg)
557 except web.HTTPError:
558 except web.HTTPError:
558 self.close()
559 self.close()
559 return
560 return
560 km = self.application.kernel_manager
561 km = self.application.kernel_manager
561 self.max_msg_size = km.max_msg_size
562 self.max_msg_size = km.max_msg_size
562 kernel_id = self.kernel_id
563 kernel_id = self.kernel_id
563 try:
564 try:
564 self.shell_stream = km.create_shell_stream(kernel_id)
565 self.shell_stream = km.create_shell_stream(kernel_id)
565 except web.HTTPError:
566 except web.HTTPError:
566 # WebSockets don't response to traditional error codes so we
567 # WebSockets don't response to traditional error codes so we
567 # close the connection.
568 # close the connection.
568 if not self.stream.closed():
569 if not self.stream.closed():
569 self.stream.close()
570 self.stream.close()
570 self.close()
571 self.close()
571 else:
572 else:
572 self.shell_stream.on_recv(self._on_zmq_reply)
573 self.shell_stream.on_recv(self._on_zmq_reply)
573
574
574 def on_message(self, msg):
575 def on_message(self, msg):
575 if len(msg) < self.max_msg_size:
576 if len(msg) < self.max_msg_size:
576 msg = jsonapi.loads(msg)
577 msg = jsonapi.loads(msg)
577 self.session.send(self.shell_stream, msg)
578 self.session.send(self.shell_stream, msg)
578
579
579 def on_close(self):
580 def on_close(self):
580 # Make sure the stream exists and is not already closed.
581 # Make sure the stream exists and is not already closed.
581 if self.shell_stream is not None and not self.shell_stream.closed():
582 if self.shell_stream is not None and not self.shell_stream.closed():
582 self.shell_stream.close()
583 self.shell_stream.close()
583
584
584
585
585 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
586 # Notebook web service handlers
587 # Notebook web service handlers
587 #-----------------------------------------------------------------------------
588 #-----------------------------------------------------------------------------
588
589
589 class NotebookRootHandler(AuthenticatedHandler):
590 class NotebookRootHandler(AuthenticatedHandler):
590
591
591 @authenticate_unless_readonly
592 @authenticate_unless_readonly
592 def get(self):
593 def get(self):
593 nbm = self.application.notebook_manager
594 nbm = self.application.notebook_manager
594 km = self.application.kernel_manager
595 km = self.application.kernel_manager
595 files = nbm.list_notebooks()
596 files = nbm.list_notebooks()
596 for f in files :
597 for f in files :
597 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
598 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
598 self.finish(jsonapi.dumps(files))
599 self.finish(jsonapi.dumps(files))
599
600
600 @web.authenticated
601 @web.authenticated
601 def post(self):
602 def post(self):
602 nbm = self.application.notebook_manager
603 nbm = self.application.notebook_manager
603 body = self.request.body.strip()
604 body = self.request.body.strip()
604 format = self.get_argument('format', default='json')
605 format = self.get_argument('format', default='json')
605 name = self.get_argument('name', default=None)
606 name = self.get_argument('name', default=None)
606 if body:
607 if body:
607 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
608 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
608 else:
609 else:
609 notebook_id = nbm.new_notebook()
610 notebook_id = nbm.new_notebook()
610 self.set_header('Location', '/'+notebook_id)
611 self.set_header('Location', '/'+notebook_id)
611 self.finish(jsonapi.dumps(notebook_id))
612 self.finish(jsonapi.dumps(notebook_id))
612
613
613
614
614 class NotebookHandler(AuthenticatedHandler):
615 class NotebookHandler(AuthenticatedHandler):
615
616
616 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
617 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
617
618
618 @authenticate_unless_readonly
619 @authenticate_unless_readonly
619 def get(self, notebook_id):
620 def get(self, notebook_id):
620 nbm = self.application.notebook_manager
621 nbm = self.application.notebook_manager
621 format = self.get_argument('format', default='json')
622 format = self.get_argument('format', default='json')
622 last_mod, name, data = nbm.get_notebook(notebook_id, format)
623 last_mod, name, data = nbm.get_notebook(notebook_id, format)
623
624
624 if format == u'json':
625 if format == u'json':
625 self.set_header('Content-Type', 'application/json')
626 self.set_header('Content-Type', 'application/json')
626 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
627 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
627 elif format == u'py':
628 elif format == u'py':
628 self.set_header('Content-Type', 'application/x-python')
629 self.set_header('Content-Type', 'application/x-python')
629 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
630 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
630 self.set_header('Last-Modified', last_mod)
631 self.set_header('Last-Modified', last_mod)
631 self.finish(data)
632 self.finish(data)
632
633
633 @web.authenticated
634 @web.authenticated
634 def put(self, notebook_id):
635 def put(self, notebook_id):
635 nbm = self.application.notebook_manager
636 nbm = self.application.notebook_manager
636 format = self.get_argument('format', default='json')
637 format = self.get_argument('format', default='json')
637 name = self.get_argument('name', default=None)
638 name = self.get_argument('name', default=None)
638 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
639 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
639 self.set_status(204)
640 self.set_status(204)
640 self.finish()
641 self.finish()
641
642
642 @web.authenticated
643 @web.authenticated
643 def delete(self, notebook_id):
644 def delete(self, notebook_id):
644 nbm = self.application.notebook_manager
645 nbm = self.application.notebook_manager
645 nbm.delete_notebook(notebook_id)
646 nbm.delete_notebook(notebook_id)
646 self.set_status(204)
647 self.set_status(204)
647 self.finish()
648 self.finish()
648
649
649
650
650 class NotebookCopyHandler(AuthenticatedHandler):
651 class NotebookCopyHandler(AuthenticatedHandler):
651
652
652 @web.authenticated
653 @web.authenticated
653 def get(self, notebook_id):
654 def get(self, notebook_id):
654 nbm = self.application.notebook_manager
655 nbm = self.application.notebook_manager
655 project = nbm.notebook_dir
656 project = nbm.notebook_dir
656 notebook_id = nbm.copy_notebook(notebook_id)
657 notebook_id = nbm.copy_notebook(notebook_id)
657 self.render(
658 self.render(
658 'notebook.html', project=project,
659 'notebook.html', project=project,
659 notebook_id=notebook_id,
660 notebook_id=notebook_id,
660 base_project_url=self.application.ipython_app.base_project_url,
661 base_project_url=self.application.ipython_app.base_project_url,
661 base_kernel_url=self.application.ipython_app.base_kernel_url,
662 base_kernel_url=self.application.ipython_app.base_kernel_url,
662 kill_kernel=False,
663 kill_kernel=False,
663 read_only=False,
664 read_only=False,
664 logged_in=self.logged_in,
665 logged_in=self.logged_in,
665 login_available=self.login_available,
666 login_available=self.login_available,
666 mathjax_url=self.application.ipython_app.mathjax_url,
667 mathjax_url=self.application.ipython_app.mathjax_url,
667 )
668 )
668
669
669
670
670 #-----------------------------------------------------------------------------
671 #-----------------------------------------------------------------------------
671 # Cluster handlers
672 # Cluster handlers
672 #-----------------------------------------------------------------------------
673 #-----------------------------------------------------------------------------
673
674
674
675
675 class MainClusterHandler(AuthenticatedHandler):
676 class MainClusterHandler(AuthenticatedHandler):
676
677
677 @web.authenticated
678 @web.authenticated
678 def get(self):
679 def get(self):
679 cm = self.application.cluster_manager
680 cm = self.application.cluster_manager
680 self.finish(jsonapi.dumps(cm.list_profiles()))
681 self.finish(jsonapi.dumps(cm.list_profiles()))
681
682
682
683
683 class ClusterProfileHandler(AuthenticatedHandler):
684 class ClusterProfileHandler(AuthenticatedHandler):
684
685
685 @web.authenticated
686 @web.authenticated
686 def get(self, profile):
687 def get(self, profile):
687 cm = self.application.cluster_manager
688 cm = self.application.cluster_manager
688 self.finish(jsonapi.dumps(cm.profile_info(profile)))
689 self.finish(jsonapi.dumps(cm.profile_info(profile)))
689
690
690
691
691 class ClusterActionHandler(AuthenticatedHandler):
692 class ClusterActionHandler(AuthenticatedHandler):
692
693
693 @web.authenticated
694 @web.authenticated
694 def post(self, profile, action):
695 def post(self, profile, action):
695 cm = self.application.cluster_manager
696 cm = self.application.cluster_manager
696 if action == 'start':
697 if action == 'start':
697 n = self.get_argument('n',default=None)
698 n = self.get_argument('n',default=None)
698 if n is None:
699 if n is None:
699 data = cm.start_cluster(profile)
700 data = cm.start_cluster(profile)
700 else:
701 else:
701 data = cm.start_cluster(profile,int(n))
702 data = cm.start_cluster(profile,int(n))
702 if action == 'stop':
703 if action == 'stop':
703 data = cm.stop_cluster(profile)
704 data = cm.stop_cluster(profile)
704 self.finish(jsonapi.dumps(data))
705 self.finish(jsonapi.dumps(data))
705
706
706
707
707 #-----------------------------------------------------------------------------
708 #-----------------------------------------------------------------------------
708 # RST web service handlers
709 # RST web service handlers
709 #-----------------------------------------------------------------------------
710 #-----------------------------------------------------------------------------
710
711
711
712
712 class RSTHandler(AuthenticatedHandler):
713 class RSTHandler(AuthenticatedHandler):
713
714
714 @web.authenticated
715 @web.authenticated
715 def post(self):
716 def post(self):
716 if publish_string is None:
717 if publish_string is None:
717 raise web.HTTPError(503, u'docutils not available')
718 raise web.HTTPError(503, u'docutils not available')
718 body = self.request.body.strip()
719 body = self.request.body.strip()
719 source = body
720 source = body
720 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
721 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
721 defaults = {'file_insertion_enabled': 0,
722 defaults = {'file_insertion_enabled': 0,
722 'raw_enabled': 0,
723 'raw_enabled': 0,
723 '_disable_config': 1,
724 '_disable_config': 1,
724 'stylesheet_path': 0
725 'stylesheet_path': 0
725 # 'template': template_path
726 # 'template': template_path
726 }
727 }
727 try:
728 try:
728 html = publish_string(source, writer_name='html',
729 html = publish_string(source, writer_name='html',
729 settings_overrides=defaults
730 settings_overrides=defaults
730 )
731 )
731 except:
732 except:
732 raise web.HTTPError(400, u'Invalid RST')
733 raise web.HTTPError(400, u'Invalid RST')
733 print html
734 print html
734 self.set_header('Content-Type', 'text/html')
735 self.set_header('Content-Type', 'text/html')
735 self.finish(html)
736 self.finish(html)
736
737
737
738
@@ -1,324 +1,323 b''
1 """A kernel manager for multiple kernels.
1 """A kernel manager for multiple kernels.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
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 import os
19 import os
20 import signal
20 import signal
21 import sys
21 import sys
22 import uuid
22 import uuid
23
23
24 import zmq
24 import zmq
25 from zmq.eventloop.zmqstream import ZMQStream
25 from zmq.eventloop.zmqstream import ZMQStream
26
26
27 from tornado import web
27 from tornado import web
28
28
29 from IPython.config.configurable import LoggingConfigurable
29 from IPython.config.configurable import LoggingConfigurable
30 from IPython.utils.importstring import import_item
30 from IPython.utils.importstring import import_item
31 from IPython.utils.traitlets import (
31 from IPython.utils.traitlets import (
32 Instance, Dict, List, Unicode, Float, Integer, Any, DottedObjectName,
32 Instance, Dict, List, Unicode, Float, Integer, Any, DottedObjectName,
33 )
33 )
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class DuplicateKernelError(Exception):
38 class DuplicateKernelError(Exception):
39 pass
39 pass
40
40
41
41
42 class MultiKernelManager(LoggingConfigurable):
42 class MultiKernelManager(LoggingConfigurable):
43 """A class for managing multiple kernels."""
43 """A class for managing multiple kernels."""
44
44
45 kernel_manager_class = DottedObjectName(
45 kernel_manager_class = DottedObjectName(
46 "IPython.zmq.kernelmanager.KernelManager", config=True,
46 "IPython.zmq.kernelmanager.KernelManager", config=True,
47 help="""The kernel manager class. This is configurable to allow
47 help="""The kernel manager class. This is configurable to allow
48 subclassing of the KernelManager for customized behavior.
48 subclassing of the KernelManager for customized behavior.
49 """
49 """
50 )
50 )
51 def _kernel_manager_class_changed(self, name, old, new):
51 def _kernel_manager_class_changed(self, name, old, new):
52 self.kernel_manager_factory = import_item(new)
52 self.kernel_manager_factory = import_item(new)
53
53
54 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
54 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
55 def _kernel_manager_factory_default(self):
55 def _kernel_manager_factory_default(self):
56 return import_item(self.kernel_manager_class)
56 return import_item(self.kernel_manager_class)
57
57
58 context = Instance('zmq.Context')
58 context = Instance('zmq.Context')
59 def _context_default(self):
59 def _context_default(self):
60 return zmq.Context.instance()
60 return zmq.Context.instance()
61
61
62 connection_dir = Unicode('')
62 connection_dir = Unicode('')
63
63
64 _kernels = Dict()
64 _kernels = Dict()
65
65
66 @property
66 @property
67 def kernel_ids(self):
67 def kernel_ids(self):
68 """Return a list of the kernel ids of the active kernels."""
68 """Return a list of the kernel ids of the active kernels."""
69 return self._kernels.keys()
69 return self._kernels.keys()
70
70
71 def __len__(self):
71 def __len__(self):
72 """Return the number of running kernels."""
72 """Return the number of running kernels."""
73 return len(self.kernel_ids)
73 return len(self.kernel_ids)
74
74
75 def __contains__(self, kernel_id):
75 def __contains__(self, kernel_id):
76 if kernel_id in self.kernel_ids:
76 if kernel_id in self.kernel_ids:
77 return True
77 return True
78 else:
78 else:
79 return False
79 return False
80
80
81 def start_kernel(self, **kwargs):
81 def start_kernel(self, **kwargs):
82 """Start a new kernel."""
82 """Start a new kernel."""
83 kernel_id = unicode(uuid.uuid4())
83 kernel_id = unicode(uuid.uuid4())
84 # use base KernelManager for each Kernel
84 # use base KernelManager for each Kernel
85 km = self.kernel_manager_factory(connection_file=os.path.join(
85 km = self.kernel_manager_factory(connection_file=os.path.join(
86 self.connection_dir, "kernel-%s.json" % kernel_id),
86 self.connection_dir, "kernel-%s.json" % kernel_id),
87 config=self.config,
87 config=self.config,
88 )
88 )
89 km.start_kernel(**kwargs)
89 km.start_kernel(**kwargs)
90 self._kernels[kernel_id] = km
90 self._kernels[kernel_id] = km
91 return kernel_id
91 return kernel_id
92
92
93 def kill_kernel(self, kernel_id):
93 def kill_kernel(self, kernel_id):
94 """Kill a kernel by its kernel uuid.
94 """Kill a kernel by its kernel uuid.
95
95
96 Parameters
96 Parameters
97 ==========
97 ==========
98 kernel_id : uuid
98 kernel_id : uuid
99 The id of the kernel to kill.
99 The id of the kernel to kill.
100 """
100 """
101 self.get_kernel(kernel_id).kill_kernel()
101 self.get_kernel(kernel_id).kill_kernel()
102 del self._kernels[kernel_id]
102 del self._kernels[kernel_id]
103
103
104 def interrupt_kernel(self, kernel_id):
104 def interrupt_kernel(self, kernel_id):
105 """Interrupt (SIGINT) the kernel by its uuid.
105 """Interrupt (SIGINT) the kernel by its uuid.
106
106
107 Parameters
107 Parameters
108 ==========
108 ==========
109 kernel_id : uuid
109 kernel_id : uuid
110 The id of the kernel to interrupt.
110 The id of the kernel to interrupt.
111 """
111 """
112 return self.get_kernel(kernel_id).interrupt_kernel()
112 return self.get_kernel(kernel_id).interrupt_kernel()
113
113
114 def signal_kernel(self, kernel_id, signum):
114 def signal_kernel(self, kernel_id, signum):
115 """ Sends a signal to the kernel by its uuid.
115 """ Sends a signal to the kernel by its uuid.
116
116
117 Note that since only SIGTERM is supported on Windows, this function
117 Note that since only SIGTERM is supported on Windows, this function
118 is only useful on Unix systems.
118 is only useful on Unix systems.
119
119
120 Parameters
120 Parameters
121 ==========
121 ==========
122 kernel_id : uuid
122 kernel_id : uuid
123 The id of the kernel to signal.
123 The id of the kernel to signal.
124 """
124 """
125 return self.get_kernel(kernel_id).signal_kernel(signum)
125 return self.get_kernel(kernel_id).signal_kernel(signum)
126
126
127 def get_kernel(self, kernel_id):
127 def get_kernel(self, kernel_id):
128 """Get the single KernelManager object for a kernel by its uuid.
128 """Get the single KernelManager object for a kernel by its uuid.
129
129
130 Parameters
130 Parameters
131 ==========
131 ==========
132 kernel_id : uuid
132 kernel_id : uuid
133 The id of the kernel.
133 The id of the kernel.
134 """
134 """
135 km = self._kernels.get(kernel_id)
135 km = self._kernels.get(kernel_id)
136 if km is not None:
136 if km is not None:
137 return km
137 return km
138 else:
138 else:
139 raise KeyError("Kernel with id not found: %s" % kernel_id)
139 raise KeyError("Kernel with id not found: %s" % kernel_id)
140
140
141 def get_kernel_ports(self, kernel_id):
141 def get_kernel_ports(self, kernel_id):
142 """Return a dictionary of ports for a kernel.
142 """Return a dictionary of ports for a kernel.
143
143
144 Parameters
144 Parameters
145 ==========
145 ==========
146 kernel_id : uuid
146 kernel_id : uuid
147 The id of the kernel.
147 The id of the kernel.
148
148
149 Returns
149 Returns
150 =======
150 =======
151 port_dict : dict
151 port_dict : dict
152 A dict of key, value pairs where the keys are the names
152 A dict of key, value pairs where the keys are the names
153 (stdin_port,iopub_port,shell_port) and the values are the
153 (stdin_port,iopub_port,shell_port) and the values are the
154 integer port numbers for those channels.
154 integer port numbers for those channels.
155 """
155 """
156 # this will raise a KeyError if not found:
156 # this will raise a KeyError if not found:
157 km = self.get_kernel(kernel_id)
157 km = self.get_kernel(kernel_id)
158 return dict(shell_port=km.shell_port,
158 return dict(shell_port=km.shell_port,
159 iopub_port=km.iopub_port,
159 iopub_port=km.iopub_port,
160 stdin_port=km.stdin_port,
160 stdin_port=km.stdin_port,
161 hb_port=km.hb_port,
161 hb_port=km.hb_port,
162 )
162 )
163
163
164 def get_kernel_ip(self, kernel_id):
164 def get_kernel_ip(self, kernel_id):
165 """Return ip address for a kernel.
165 """Return ip address for a kernel.
166
166
167 Parameters
167 Parameters
168 ==========
168 ==========
169 kernel_id : uuid
169 kernel_id : uuid
170 The id of the kernel.
170 The id of the kernel.
171
171
172 Returns
172 Returns
173 =======
173 =======
174 ip : str
174 ip : str
175 The ip address of the kernel.
175 The ip address of the kernel.
176 """
176 """
177 return self.get_kernel(kernel_id).ip
177 return self.get_kernel(kernel_id).ip
178
178
179 def create_connected_stream(self, ip, port, socket_type):
179 def create_connected_stream(self, ip, port, socket_type):
180 sock = self.context.socket(socket_type)
180 sock = self.context.socket(socket_type)
181 addr = "tcp://%s:%i" % (ip, port)
181 addr = "tcp://%s:%i" % (ip, port)
182 self.log.info("Connecting to: %s" % addr)
182 self.log.info("Connecting to: %s" % addr)
183 sock.connect(addr)
183 sock.connect(addr)
184 return ZMQStream(sock)
184 return ZMQStream(sock)
185
185
186 def create_iopub_stream(self, kernel_id):
186 def create_iopub_stream(self, kernel_id):
187 ip = self.get_kernel_ip(kernel_id)
187 ip = self.get_kernel_ip(kernel_id)
188 ports = self.get_kernel_ports(kernel_id)
188 ports = self.get_kernel_ports(kernel_id)
189 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
189 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
190 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
190 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
191 return iopub_stream
191 return iopub_stream
192
192
193 def create_shell_stream(self, kernel_id):
193 def create_shell_stream(self, kernel_id):
194 ip = self.get_kernel_ip(kernel_id)
194 ip = self.get_kernel_ip(kernel_id)
195 ports = self.get_kernel_ports(kernel_id)
195 ports = self.get_kernel_ports(kernel_id)
196 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ)
196 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ)
197 return shell_stream
197 return shell_stream
198
198
199 def create_hb_stream(self, kernel_id):
199 def create_hb_stream(self, kernel_id):
200 ip = self.get_kernel_ip(kernel_id)
200 ip = self.get_kernel_ip(kernel_id)
201 ports = self.get_kernel_ports(kernel_id)
201 ports = self.get_kernel_ports(kernel_id)
202 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
202 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
203 return hb_stream
203 return hb_stream
204
204
205
205
206 class MappingKernelManager(MultiKernelManager):
206 class MappingKernelManager(MultiKernelManager):
207 """A KernelManager that handles notebok mapping and HTTP error handling"""
207 """A KernelManager that handles notebok mapping and HTTP error handling"""
208
208
209 kernel_argv = List(Unicode)
209 kernel_argv = List(Unicode)
210
210
211 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
211 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
212 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
212 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
213
213
214 max_msg_size = Integer(65536, config=True, help="""
214 max_msg_size = Integer(65536, config=True, help="""
215 The max raw message size accepted from the browser
215 The max raw message size accepted from the browser
216 over a WebSocket connection.
216 over a WebSocket connection.
217 """)
217 """)
218
218
219 _notebook_mapping = Dict()
219 _notebook_mapping = Dict()
220
220
221 #-------------------------------------------------------------------------
221 #-------------------------------------------------------------------------
222 # Methods for managing kernels and sessions
222 # Methods for managing kernels and sessions
223 #-------------------------------------------------------------------------
223 #-------------------------------------------------------------------------
224
224
225 def kernel_for_notebook(self, notebook_id):
225 def kernel_for_notebook(self, notebook_id):
226 """Return the kernel_id for a notebook_id or None."""
226 """Return the kernel_id for a notebook_id or None."""
227 return self._notebook_mapping.get(notebook_id)
227 return self._notebook_mapping.get(notebook_id)
228
228
229 def set_kernel_for_notebook(self, notebook_id, kernel_id):
229 def set_kernel_for_notebook(self, notebook_id, kernel_id):
230 """Associate a notebook with a kernel."""
230 """Associate a notebook with a kernel."""
231 if notebook_id is not None:
231 if notebook_id is not None:
232 self._notebook_mapping[notebook_id] = kernel_id
232 self._notebook_mapping[notebook_id] = kernel_id
233
233
234 def notebook_for_kernel(self, kernel_id):
234 def notebook_for_kernel(self, kernel_id):
235 """Return the notebook_id for a kernel_id or None."""
235 """Return the notebook_id for a kernel_id or None."""
236 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
236 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
237 if len(notebook_ids) == 1:
237 if len(notebook_ids) == 1:
238 return notebook_ids[0]
238 return notebook_ids[0]
239 else:
239 else:
240 return None
240 return None
241
241
242 def delete_mapping_for_kernel(self, kernel_id):
242 def delete_mapping_for_kernel(self, kernel_id):
243 """Remove the kernel/notebook mapping for kernel_id."""
243 """Remove the kernel/notebook mapping for kernel_id."""
244 notebook_id = self.notebook_for_kernel(kernel_id)
244 notebook_id = self.notebook_for_kernel(kernel_id)
245 if notebook_id is not None:
245 if notebook_id is not None:
246 del self._notebook_mapping[notebook_id]
246 del self._notebook_mapping[notebook_id]
247
247
248 def start_kernel(self, notebook_id=None):
248 def start_kernel(self, notebook_id=None, **kwargs):
249 """Start a kernel for a notebok an return its kernel_id.
249 """Start a kernel for a notebok an return its kernel_id.
250
250
251 Parameters
251 Parameters
252 ----------
252 ----------
253 notebook_id : uuid
253 notebook_id : uuid
254 The uuid of the notebook to associate the new kernel with. If this
254 The uuid of the notebook to associate the new kernel with. If this
255 is not None, this kernel will be persistent whenever the notebook
255 is not None, this kernel will be persistent whenever the notebook
256 requests a kernel.
256 requests a kernel.
257 """
257 """
258 kernel_id = self.kernel_for_notebook(notebook_id)
258 kernel_id = self.kernel_for_notebook(notebook_id)
259 if kernel_id is None:
259 if kernel_id is None:
260 kwargs = dict()
261 kwargs['extra_arguments'] = self.kernel_argv
260 kwargs['extra_arguments'] = self.kernel_argv
262 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
261 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
263 self.set_kernel_for_notebook(notebook_id, kernel_id)
262 self.set_kernel_for_notebook(notebook_id, kernel_id)
264 self.log.info("Kernel started: %s" % kernel_id)
263 self.log.info("Kernel started: %s" % kernel_id)
265 self.log.debug("Kernel args: %r" % kwargs)
264 self.log.debug("Kernel args: %r" % kwargs)
266 else:
265 else:
267 self.log.info("Using existing kernel: %s" % kernel_id)
266 self.log.info("Using existing kernel: %s" % kernel_id)
268 return kernel_id
267 return kernel_id
269
268
270 def kill_kernel(self, kernel_id):
269 def kill_kernel(self, kernel_id):
271 """Kill a kernel and remove its notebook association."""
270 """Kill a kernel and remove its notebook association."""
272 self._check_kernel_id(kernel_id)
271 self._check_kernel_id(kernel_id)
273 super(MappingKernelManager, self).kill_kernel(kernel_id)
272 super(MappingKernelManager, self).kill_kernel(kernel_id)
274 self.delete_mapping_for_kernel(kernel_id)
273 self.delete_mapping_for_kernel(kernel_id)
275 self.log.info("Kernel killed: %s" % kernel_id)
274 self.log.info("Kernel killed: %s" % kernel_id)
276
275
277 def interrupt_kernel(self, kernel_id):
276 def interrupt_kernel(self, kernel_id):
278 """Interrupt a kernel."""
277 """Interrupt a kernel."""
279 self._check_kernel_id(kernel_id)
278 self._check_kernel_id(kernel_id)
280 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
279 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
281 self.log.info("Kernel interrupted: %s" % kernel_id)
280 self.log.info("Kernel interrupted: %s" % kernel_id)
282
281
283 def restart_kernel(self, kernel_id):
282 def restart_kernel(self, kernel_id):
284 """Restart a kernel while keeping clients connected."""
283 """Restart a kernel while keeping clients connected."""
285 self._check_kernel_id(kernel_id)
284 self._check_kernel_id(kernel_id)
286 km = self.get_kernel(kernel_id)
285 km = self.get_kernel(kernel_id)
287 km.restart_kernel(now=True)
286 km.restart_kernel(now=True)
288 self.log.info("Kernel restarted: %s" % kernel_id)
287 self.log.info("Kernel restarted: %s" % kernel_id)
289 return kernel_id
288 return kernel_id
290
289
291 # the following remains, in case the KM restart machinery is
290 # the following remains, in case the KM restart machinery is
292 # somehow unacceptable
291 # somehow unacceptable
293 # Get the notebook_id to preserve the kernel/notebook association.
292 # Get the notebook_id to preserve the kernel/notebook association.
294 notebook_id = self.notebook_for_kernel(kernel_id)
293 notebook_id = self.notebook_for_kernel(kernel_id)
295 # Create the new kernel first so we can move the clients over.
294 # Create the new kernel first so we can move the clients over.
296 new_kernel_id = self.start_kernel()
295 new_kernel_id = self.start_kernel()
297 # Now kill the old kernel.
296 # Now kill the old kernel.
298 self.kill_kernel(kernel_id)
297 self.kill_kernel(kernel_id)
299 # Now save the new kernel/notebook association. We have to save it
298 # Now save the new kernel/notebook association. We have to save it
300 # after the old kernel is killed as that will delete the mapping.
299 # after the old kernel is killed as that will delete the mapping.
301 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
300 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
302 self.log.info("Kernel restarted: %s" % new_kernel_id)
301 self.log.info("Kernel restarted: %s" % new_kernel_id)
303 return new_kernel_id
302 return new_kernel_id
304
303
305 def create_iopub_stream(self, kernel_id):
304 def create_iopub_stream(self, kernel_id):
306 """Create a new iopub stream."""
305 """Create a new iopub stream."""
307 self._check_kernel_id(kernel_id)
306 self._check_kernel_id(kernel_id)
308 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
307 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
309
308
310 def create_shell_stream(self, kernel_id):
309 def create_shell_stream(self, kernel_id):
311 """Create a new shell stream."""
310 """Create a new shell stream."""
312 self._check_kernel_id(kernel_id)
311 self._check_kernel_id(kernel_id)
313 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
312 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
314
313
315 def create_hb_stream(self, kernel_id):
314 def create_hb_stream(self, kernel_id):
316 """Create a new hb stream."""
315 """Create a new hb stream."""
317 self._check_kernel_id(kernel_id)
316 self._check_kernel_id(kernel_id)
318 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
317 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
319
318
320 def _check_kernel_id(self, kernel_id):
319 def _check_kernel_id(self, kernel_id):
321 """Check a that a kernel_id exists and raise 404 if not."""
320 """Check a that a kernel_id exists and raise 404 if not."""
322 if kernel_id not in self:
321 if kernel_id not in self:
323 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
322 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
324
323
@@ -1,590 +1,589 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 #-----------------------------------------------------------------------------
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 webbrowser
31 import webbrowser
32
32
33 # Third party
33 # Third party
34 import zmq
34 import zmq
35
35
36 # Install the pyzmq ioloop. This has to be done before anything else from
36 # Install the pyzmq ioloop. This has to be done before anything else from
37 # tornado is imported.
37 # tornado is imported.
38 from zmq.eventloop import ioloop
38 from zmq.eventloop import ioloop
39 ioloop.install()
39 ioloop.install()
40
40
41 from tornado import httpserver
41 from tornado import httpserver
42 from tornado import web
42 from tornado import web
43
43
44 # Our own libraries
44 # Our own libraries
45 from .kernelmanager import MappingKernelManager
45 from .kernelmanager import MappingKernelManager
46 from .handlers import (LoginHandler, LogoutHandler,
46 from .handlers import (LoginHandler, LogoutHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
52 )
52 )
53 from .notebookmanager import NotebookManager
53 from .notebookmanager import NotebookManager
54 from .clustermanager import ClusterManager
54 from .clustermanager import ClusterManager
55
55
56 from IPython.config.application import catch_config_error, boolean_flag
56 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.core.application import BaseIPythonApplication
57 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.profiledir import ProfileDir
58 from IPython.core.profiledir import ProfileDir
59 from IPython.frontend.consoleapp import IPythonConsoleApp
59 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.lib.kernel import swallow_argv
60 from IPython.lib.kernel import swallow_argv
61 from IPython.zmq.session import Session, default_secure
61 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.ipkernel import (
63 from IPython.zmq.ipkernel import (
64 flags as ipkernel_flags,
64 flags as ipkernel_flags,
65 aliases as ipkernel_aliases,
65 aliases as ipkernel_aliases,
66 IPKernelApp
66 IPKernelApp
67 )
67 )
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils import py3compat
69 from IPython.utils import py3compat
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Module globals
72 # Module globals
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
78 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
79 _cluster_action_regex = r"(?P<action>start|stop)"
79 _cluster_action_regex = r"(?P<action>start|stop)"
80
80
81
81
82 LOCALHOST = '127.0.0.1'
82 LOCALHOST = '127.0.0.1'
83
83
84 _examples = """
84 _examples = """
85 ipython notebook # start the notebook
85 ipython notebook # start the notebook
86 ipython notebook --profile=sympy # use the sympy profile
86 ipython notebook --profile=sympy # use the sympy profile
87 ipython notebook --pylab=inline # pylab in inline plotting mode
87 ipython notebook --pylab=inline # pylab in inline plotting mode
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 """
90 """
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Helper functions
93 # Helper functions
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95
95
96 def url_path_join(a,b):
96 def url_path_join(a,b):
97 if a.endswith('/') and b.startswith('/'):
97 if a.endswith('/') and b.startswith('/'):
98 return a[:-1]+b
98 return a[:-1]+b
99 else:
99 else:
100 return a+b
100 return a+b
101
101
102 def random_ports(port, n):
102 def random_ports(port, n):
103 """Generate a list of n random ports near the given port.
103 """Generate a list of n random ports near the given port.
104
104
105 The first 5 ports will be sequential, and the remaining n-5 will be
105 The first 5 ports will be sequential, and the remaining n-5 will be
106 randomly selected in the range [port-2*n, port+2*n].
106 randomly selected in the range [port-2*n, port+2*n].
107 """
107 """
108 for i in range(min(5, n)):
108 for i in range(min(5, n)):
109 yield port + i
109 yield port + i
110 for i in range(n-5):
110 for i in range(n-5):
111 yield port + random.randint(-2*n, 2*n)
111 yield port + random.randint(-2*n, 2*n)
112
112
113 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
114 # The Tornado web application
114 # The Tornado web application
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116
116
117 class NotebookWebApplication(web.Application):
117 class NotebookWebApplication(web.Application):
118
118
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
120 cluster_manager, log,
120 cluster_manager, log,
121 base_project_url, settings_overrides):
121 base_project_url, settings_overrides):
122 handlers = [
122 handlers = [
123 (r"/", ProjectDashboardHandler),
123 (r"/", ProjectDashboardHandler),
124 (r"/login", LoginHandler),
124 (r"/login", LoginHandler),
125 (r"/logout", LogoutHandler),
125 (r"/logout", LogoutHandler),
126 (r"/new", NewHandler),
126 (r"/new", NewHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
130 (r"/kernels", MainKernelHandler),
130 (r"/kernels", MainKernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
135 (r"/notebooks", NotebookRootHandler),
135 (r"/notebooks", NotebookRootHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
137 (r"/rstservice/render", RSTHandler),
137 (r"/rstservice/render", RSTHandler),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
139 (r"/clusters", MainClusterHandler),
139 (r"/clusters", MainClusterHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
142 ]
142 ]
143 settings = dict(
143 settings = dict(
144 template_path=os.path.join(os.path.dirname(__file__), "templates"),
144 template_path=os.path.join(os.path.dirname(__file__), "templates"),
145 static_path=os.path.join(os.path.dirname(__file__), "static"),
145 static_path=os.path.join(os.path.dirname(__file__), "static"),
146 cookie_secret=os.urandom(1024),
146 cookie_secret=os.urandom(1024),
147 login_url="/login",
147 login_url="/login",
148 )
148 )
149
149
150 # allow custom overrides for the tornado web app.
150 # allow custom overrides for the tornado web app.
151 settings.update(settings_overrides)
151 settings.update(settings_overrides)
152
152
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
154 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
155 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
157 # This enforces that base_project_url be ascii in that situation.
158 #
158 #
159 # Note that the URLs these patterns check against are escaped,
159 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
162
163 # prepend base_project_url onto the patterns that we match
163 # prepend base_project_url onto the patterns that we match
164 new_handlers = []
164 new_handlers = []
165 for handler in handlers:
165 for handler in handlers:
166 pattern = url_path_join(base_project_url, handler[0])
166 pattern = url_path_join(base_project_url, handler[0])
167 new_handler = tuple([pattern]+list(handler[1:]))
167 new_handler = tuple([pattern]+list(handler[1:]))
168 new_handlers.append( new_handler )
168 new_handlers.append( new_handler )
169
169
170 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
170 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
171
171
172 self.kernel_manager = kernel_manager
172 self.kernel_manager = kernel_manager
173 self.notebook_manager = notebook_manager
173 self.notebook_manager = notebook_manager
174 self.cluster_manager = cluster_manager
174 self.cluster_manager = cluster_manager
175 self.ipython_app = ipython_app
175 self.ipython_app = ipython_app
176 self.read_only = self.ipython_app.read_only
176 self.read_only = self.ipython_app.read_only
177 self.log = log
177 self.log = log
178
178
179
179
180 #-----------------------------------------------------------------------------
180 #-----------------------------------------------------------------------------
181 # Aliases and Flags
181 # Aliases and Flags
182 #-----------------------------------------------------------------------------
182 #-----------------------------------------------------------------------------
183
183
184 flags = dict(ipkernel_flags)
184 flags = dict(ipkernel_flags)
185 flags['no-browser']=(
185 flags['no-browser']=(
186 {'NotebookApp' : {'open_browser' : False}},
186 {'NotebookApp' : {'open_browser' : False}},
187 "Don't open the notebook in a browser after startup."
187 "Don't open the notebook in a browser after startup."
188 )
188 )
189 flags['no-mathjax']=(
189 flags['no-mathjax']=(
190 {'NotebookApp' : {'enable_mathjax' : False}},
190 {'NotebookApp' : {'enable_mathjax' : False}},
191 """Disable MathJax
191 """Disable MathJax
192
192
193 MathJax is the javascript library IPython uses to render math/LaTeX. It is
193 MathJax is the javascript library IPython uses to render math/LaTeX. It is
194 very large, so you may want to disable it if you have a slow internet
194 very large, so you may want to disable it if you have a slow internet
195 connection, or for offline use of the notebook.
195 connection, or for offline use of the notebook.
196
196
197 When disabled, equations etc. will appear as their untransformed TeX source.
197 When disabled, equations etc. will appear as their untransformed TeX source.
198 """
198 """
199 )
199 )
200 flags['read-only'] = (
200 flags['read-only'] = (
201 {'NotebookApp' : {'read_only' : True}},
201 {'NotebookApp' : {'read_only' : True}},
202 """Allow read-only access to notebooks.
202 """Allow read-only access to notebooks.
203
203
204 When using a password to protect the notebook server, this flag
204 When using a password to protect the notebook server, this flag
205 allows unauthenticated clients to view the notebook list, and
205 allows unauthenticated clients to view the notebook list, and
206 individual notebooks, but not edit them, start kernels, or run
206 individual notebooks, but not edit them, start kernels, or run
207 code.
207 code.
208
208
209 If no password is set, the server will be entirely read-only.
209 If no password is set, the server will be entirely read-only.
210 """
210 """
211 )
211 )
212
212
213 # Add notebook manager flags
213 # Add notebook manager flags
214 flags.update(boolean_flag('script', 'NotebookManager.save_script',
214 flags.update(boolean_flag('script', 'NotebookManager.save_script',
215 'Auto-save a .py script everytime the .ipynb notebook is saved',
215 'Auto-save a .py script everytime the .ipynb notebook is saved',
216 'Do not auto-save .py scripts for every notebook'))
216 'Do not auto-save .py scripts for every notebook'))
217
217
218 # the flags that are specific to the frontend
218 # the flags that are specific to the frontend
219 # these must be scrubbed before being passed to the kernel,
219 # these must be scrubbed before being passed to the kernel,
220 # or it will raise an error on unrecognized flags
220 # or it will raise an error on unrecognized flags
221 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
221 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
222
222
223 aliases = dict(ipkernel_aliases)
223 aliases = dict(ipkernel_aliases)
224
224
225 aliases.update({
225 aliases.update({
226 'ip': 'NotebookApp.ip',
226 'ip': 'NotebookApp.ip',
227 'port': 'NotebookApp.port',
227 'port': 'NotebookApp.port',
228 'port-retries': 'NotebookApp.port_retries',
228 'port-retries': 'NotebookApp.port_retries',
229 'keyfile': 'NotebookApp.keyfile',
229 'keyfile': 'NotebookApp.keyfile',
230 'certfile': 'NotebookApp.certfile',
230 'certfile': 'NotebookApp.certfile',
231 'notebook-dir': 'NotebookManager.notebook_dir',
231 'notebook-dir': 'NotebookManager.notebook_dir',
232 'browser': 'NotebookApp.browser',
232 'browser': 'NotebookApp.browser',
233 })
233 })
234
234
235 # remove ipkernel flags that are singletons, and don't make sense in
235 # remove ipkernel flags that are singletons, and don't make sense in
236 # multi-kernel evironment:
236 # multi-kernel evironment:
237 aliases.pop('f', None)
237 aliases.pop('f', None)
238
238
239 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
239 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
240 u'notebook-dir']
240 u'notebook-dir']
241
241
242 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
243 # NotebookApp
243 # NotebookApp
244 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
245
245
246 class NotebookApp(BaseIPythonApplication):
246 class NotebookApp(BaseIPythonApplication):
247
247
248 name = 'ipython-notebook'
248 name = 'ipython-notebook'
249 default_config_file_name='ipython_notebook_config.py'
249 default_config_file_name='ipython_notebook_config.py'
250
250
251 description = """
251 description = """
252 The IPython HTML Notebook.
252 The IPython HTML Notebook.
253
253
254 This launches a Tornado based HTML Notebook Server that serves up an
254 This launches a Tornado based HTML Notebook Server that serves up an
255 HTML5/Javascript Notebook client.
255 HTML5/Javascript Notebook client.
256 """
256 """
257 examples = _examples
257 examples = _examples
258
258
259 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
259 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
260 flags = Dict(flags)
260 flags = Dict(flags)
261 aliases = Dict(aliases)
261 aliases = Dict(aliases)
262
262
263 kernel_argv = List(Unicode)
263 kernel_argv = List(Unicode)
264
264
265 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
265 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
266 default_value=logging.INFO,
266 default_value=logging.INFO,
267 config=True,
267 config=True,
268 help="Set the log level by value or name.")
268 help="Set the log level by value or name.")
269
269
270 # create requested profiles by default, if they don't exist:
270 # create requested profiles by default, if they don't exist:
271 auto_create = Bool(True)
271 auto_create = Bool(True)
272
272
273 # file to be opened in the notebook server
273 # file to be opened in the notebook server
274 file_to_run = Unicode('')
274 file_to_run = Unicode('')
275
275
276 # Network related information.
276 # Network related information.
277
277
278 ip = Unicode(LOCALHOST, config=True,
278 ip = Unicode(LOCALHOST, config=True,
279 help="The IP address the notebook server will listen on."
279 help="The IP address the notebook server will listen on."
280 )
280 )
281
281
282 def _ip_changed(self, name, old, new):
282 def _ip_changed(self, name, old, new):
283 if new == u'*': self.ip = u''
283 if new == u'*': self.ip = u''
284
284
285 port = Integer(8888, config=True,
285 port = Integer(8888, config=True,
286 help="The port the notebook server will listen on."
286 help="The port the notebook server will listen on."
287 )
287 )
288 port_retries = Integer(50, config=True,
288 port_retries = Integer(50, config=True,
289 help="The number of additional ports to try if the specified port is not available."
289 help="The number of additional ports to try if the specified port is not available."
290 )
290 )
291
291
292 certfile = Unicode(u'', config=True,
292 certfile = Unicode(u'', config=True,
293 help="""The full path to an SSL/TLS certificate file."""
293 help="""The full path to an SSL/TLS certificate file."""
294 )
294 )
295
295
296 keyfile = Unicode(u'', config=True,
296 keyfile = Unicode(u'', config=True,
297 help="""The full path to a private key file for usage with SSL/TLS."""
297 help="""The full path to a private key file for usage with SSL/TLS."""
298 )
298 )
299
299
300 password = Unicode(u'', config=True,
300 password = Unicode(u'', config=True,
301 help="""Hashed password to use for web authentication.
301 help="""Hashed password to use for web authentication.
302
302
303 To generate, type in a python/IPython shell:
303 To generate, type in a python/IPython shell:
304
304
305 from IPython.lib import passwd; passwd()
305 from IPython.lib import passwd; passwd()
306
306
307 The string should be of the form type:salt:hashed-password.
307 The string should be of the form type:salt:hashed-password.
308 """
308 """
309 )
309 )
310
310
311 open_browser = Bool(True, config=True,
311 open_browser = Bool(True, config=True,
312 help="""Whether to open in a browser after starting.
312 help="""Whether to open in a browser after starting.
313 The specific browser used is platform dependent and
313 The specific browser used is platform dependent and
314 determined by the python standard library `webbrowser`
314 determined by the python standard library `webbrowser`
315 module, unless it is overridden using the --browser
315 module, unless it is overridden using the --browser
316 (NotebookApp.browser) configuration option.
316 (NotebookApp.browser) configuration option.
317 """)
317 """)
318
318
319 browser = Unicode(u'', config=True,
319 browser = Unicode(u'', config=True,
320 help="""Specify what command to use to invoke a web
320 help="""Specify what command to use to invoke a web
321 browser when opening the notebook. If not specified, the
321 browser when opening the notebook. If not specified, the
322 default browser will be determined by the `webbrowser`
322 default browser will be determined by the `webbrowser`
323 standard library module, which allows setting of the
323 standard library module, which allows setting of the
324 BROWSER environment variable to override it.
324 BROWSER environment variable to override it.
325 """)
325 """)
326
326
327 read_only = Bool(False, config=True,
327 read_only = Bool(False, config=True,
328 help="Whether to prevent editing/execution of notebooks."
328 help="Whether to prevent editing/execution of notebooks."
329 )
329 )
330
330
331 webapp_settings = Dict(config=True,
331 webapp_settings = Dict(config=True,
332 help="Supply overrides for the tornado.web.Application that the "
332 help="Supply overrides for the tornado.web.Application that the "
333 "IPython notebook uses.")
333 "IPython notebook uses.")
334
334
335 enable_mathjax = Bool(True, config=True,
335 enable_mathjax = Bool(True, config=True,
336 help="""Whether to enable MathJax for typesetting math/TeX
336 help="""Whether to enable MathJax for typesetting math/TeX
337
337
338 MathJax is the javascript library IPython uses to render math/LaTeX. It is
338 MathJax is the javascript library IPython uses to render math/LaTeX. It is
339 very large, so you may want to disable it if you have a slow internet
339 very large, so you may want to disable it if you have a slow internet
340 connection, or for offline use of the notebook.
340 connection, or for offline use of the notebook.
341
341
342 When disabled, equations etc. will appear as their untransformed TeX source.
342 When disabled, equations etc. will appear as their untransformed TeX source.
343 """
343 """
344 )
344 )
345 def _enable_mathjax_changed(self, name, old, new):
345 def _enable_mathjax_changed(self, name, old, new):
346 """set mathjax url to empty if mathjax is disabled"""
346 """set mathjax url to empty if mathjax is disabled"""
347 if not new:
347 if not new:
348 self.mathjax_url = u''
348 self.mathjax_url = u''
349
349
350 base_project_url = Unicode('/', config=True,
350 base_project_url = Unicode('/', config=True,
351 help='''The base URL for the notebook server''')
351 help='''The base URL for the notebook server''')
352 base_kernel_url = Unicode('/', config=True,
352 base_kernel_url = Unicode('/', config=True,
353 help='''The base URL for the kernel server''')
353 help='''The base URL for the kernel server''')
354 websocket_host = Unicode("", config=True,
354 websocket_host = Unicode("", config=True,
355 help="""The hostname for the websocket server."""
355 help="""The hostname for the websocket server."""
356 )
356 )
357
357
358 mathjax_url = Unicode("", config=True,
358 mathjax_url = Unicode("", config=True,
359 help="""The url for MathJax.js."""
359 help="""The url for MathJax.js."""
360 )
360 )
361 def _mathjax_url_default(self):
361 def _mathjax_url_default(self):
362 if not self.enable_mathjax:
362 if not self.enable_mathjax:
363 return u''
363 return u''
364 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
364 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
365 static_url_prefix = self.webapp_settings.get("static_url_prefix",
365 static_url_prefix = self.webapp_settings.get("static_url_prefix",
366 "/static/")
366 "/static/")
367 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
367 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
368 self.log.info("Using local MathJax")
368 self.log.info("Using local MathJax")
369 return static_url_prefix+u"mathjax/MathJax.js"
369 return static_url_prefix+u"mathjax/MathJax.js"
370 else:
370 else:
371 if self.certfile:
371 if self.certfile:
372 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
372 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
373 base = u"https://c328740.ssl.cf1.rackcdn.com"
373 base = u"https://c328740.ssl.cf1.rackcdn.com"
374 else:
374 else:
375 base = u"http://cdn.mathjax.org"
375 base = u"http://cdn.mathjax.org"
376
376
377 url = base + u"/mathjax/latest/MathJax.js"
377 url = base + u"/mathjax/latest/MathJax.js"
378 self.log.info("Using MathJax from CDN: %s", url)
378 self.log.info("Using MathJax from CDN: %s", url)
379 return url
379 return url
380
380
381 def _mathjax_url_changed(self, name, old, new):
381 def _mathjax_url_changed(self, name, old, new):
382 if new and not self.enable_mathjax:
382 if new and not self.enable_mathjax:
383 # enable_mathjax=False overrides mathjax_url
383 # enable_mathjax=False overrides mathjax_url
384 self.mathjax_url = u''
384 self.mathjax_url = u''
385 else:
385 else:
386 self.log.info("Using MathJax: %s", new)
386 self.log.info("Using MathJax: %s", new)
387
387
388 def parse_command_line(self, argv=None):
388 def parse_command_line(self, argv=None):
389 super(NotebookApp, self).parse_command_line(argv)
389 super(NotebookApp, self).parse_command_line(argv)
390 if argv is None:
390 if argv is None:
391 argv = sys.argv[1:]
391 argv = sys.argv[1:]
392
392
393 # Scrub frontend-specific flags
393 # Scrub frontend-specific flags
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
395 # Kernel should inherit default config file from frontend
395 # Kernel should inherit default config file from frontend
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
397
397
398 if self.extra_args:
398 if self.extra_args:
399 f = os.path.abspath(self.extra_args[0])
399 f = os.path.abspath(self.extra_args[0])
400 if os.path.isdir(f):
400 if os.path.isdir(f):
401 nbdir = f
401 nbdir = f
402 else:
402 else:
403 self.file_to_run = f
403 self.file_to_run = f
404 nbdir = os.path.dirname(f)
404 nbdir = os.path.dirname(f)
405 self.config.NotebookManager.notebook_dir = nbdir
405 self.config.NotebookManager.notebook_dir = nbdir
406
406
407 def init_configurables(self):
407 def init_configurables(self):
408 # force Session default to be secure
408 # force Session default to be secure
409 default_secure(self.config)
409 default_secure(self.config)
410 # Create a KernelManager and start a kernel.
411 self.kernel_manager = MappingKernelManager(
410 self.kernel_manager = MappingKernelManager(
412 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
411 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
413 connection_dir = self.profile_dir.security_dir,
412 connection_dir = self.profile_dir.security_dir,
414 )
413 )
415 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
414 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
416 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
415 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
417 self.notebook_manager.list_notebooks()
416 self.notebook_manager.list_notebooks()
418 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
417 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
419 self.cluster_manager.update_profiles()
418 self.cluster_manager.update_profiles()
420
419
421 def init_logging(self):
420 def init_logging(self):
422 # This prevents double log messages because tornado use a root logger that
421 # This prevents double log messages because tornado use a root logger that
423 # self.log is a child of. The logging module dipatches log messages to a log
422 # self.log is a child of. The logging module dipatches log messages to a log
424 # and all of its ancenstors until propagate is set to False.
423 # and all of its ancenstors until propagate is set to False.
425 self.log.propagate = False
424 self.log.propagate = False
426
425
427 def init_webapp(self):
426 def init_webapp(self):
428 """initialize tornado webapp and httpserver"""
427 """initialize tornado webapp and httpserver"""
429 self.web_app = NotebookWebApplication(
428 self.web_app = NotebookWebApplication(
430 self, self.kernel_manager, self.notebook_manager,
429 self, self.kernel_manager, self.notebook_manager,
431 self.cluster_manager, self.log,
430 self.cluster_manager, self.log,
432 self.base_project_url, self.webapp_settings
431 self.base_project_url, self.webapp_settings
433 )
432 )
434 if self.certfile:
433 if self.certfile:
435 ssl_options = dict(certfile=self.certfile)
434 ssl_options = dict(certfile=self.certfile)
436 if self.keyfile:
435 if self.keyfile:
437 ssl_options['keyfile'] = self.keyfile
436 ssl_options['keyfile'] = self.keyfile
438 else:
437 else:
439 ssl_options = None
438 ssl_options = None
440 self.web_app.password = self.password
439 self.web_app.password = self.password
441 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
440 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
442 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
441 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
443 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
442 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
444 'but not using any encryption or authentication. This is highly '
443 'but not using any encryption or authentication. This is highly '
445 'insecure and not recommended.')
444 'insecure and not recommended.')
446
445
447 success = None
446 success = None
448 for port in random_ports(self.port, self.port_retries+1):
447 for port in random_ports(self.port, self.port_retries+1):
449 try:
448 try:
450 self.http_server.listen(port, self.ip)
449 self.http_server.listen(port, self.ip)
451 except socket.error, e:
450 except socket.error, e:
452 if e.errno != errno.EADDRINUSE:
451 if e.errno != errno.EADDRINUSE:
453 raise
452 raise
454 self.log.info('The port %i is already in use, trying another random port.' % port)
453 self.log.info('The port %i is already in use, trying another random port.' % port)
455 else:
454 else:
456 self.port = port
455 self.port = port
457 success = True
456 success = True
458 break
457 break
459 if not success:
458 if not success:
460 self.log.critical('ERROR: the notebook server could not be started because '
459 self.log.critical('ERROR: the notebook server could not be started because '
461 'no available port could be found.')
460 'no available port could be found.')
462 self.exit(1)
461 self.exit(1)
463
462
464 def init_signal(self):
463 def init_signal(self):
465 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
464 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
466 # safely extract zmq version info:
465 # safely extract zmq version info:
467 try:
466 try:
468 zmq_v = zmq.pyzmq_version_info()
467 zmq_v = zmq.pyzmq_version_info()
469 except AttributeError:
468 except AttributeError:
470 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
469 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
471 if 'dev' in zmq.__version__:
470 if 'dev' in zmq.__version__:
472 zmq_v.append(999)
471 zmq_v.append(999)
473 zmq_v = tuple(zmq_v)
472 zmq_v = tuple(zmq_v)
474 if zmq_v >= (2,1,9):
473 if zmq_v >= (2,1,9):
475 # This won't work with 2.1.7 and
474 # This won't work with 2.1.7 and
476 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
475 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
477 # but it will work
476 # but it will work
478 signal.signal(signal.SIGINT, self._handle_sigint)
477 signal.signal(signal.SIGINT, self._handle_sigint)
479 signal.signal(signal.SIGTERM, self._signal_stop)
478 signal.signal(signal.SIGTERM, self._signal_stop)
480
479
481 def _handle_sigint(self, sig, frame):
480 def _handle_sigint(self, sig, frame):
482 """SIGINT handler spawns confirmation dialog"""
481 """SIGINT handler spawns confirmation dialog"""
483 # register more forceful signal handler for ^C^C case
482 # register more forceful signal handler for ^C^C case
484 signal.signal(signal.SIGINT, self._signal_stop)
483 signal.signal(signal.SIGINT, self._signal_stop)
485 # request confirmation dialog in bg thread, to avoid
484 # request confirmation dialog in bg thread, to avoid
486 # blocking the App
485 # blocking the App
487 thread = threading.Thread(target=self._confirm_exit)
486 thread = threading.Thread(target=self._confirm_exit)
488 thread.daemon = True
487 thread.daemon = True
489 thread.start()
488 thread.start()
490
489
491 def _restore_sigint_handler(self):
490 def _restore_sigint_handler(self):
492 """callback for restoring original SIGINT handler"""
491 """callback for restoring original SIGINT handler"""
493 signal.signal(signal.SIGINT, self._handle_sigint)
492 signal.signal(signal.SIGINT, self._handle_sigint)
494
493
495 def _confirm_exit(self):
494 def _confirm_exit(self):
496 """confirm shutdown on ^C
495 """confirm shutdown on ^C
497
496
498 A second ^C, or answering 'y' within 5s will cause shutdown,
497 A second ^C, or answering 'y' within 5s will cause shutdown,
499 otherwise original SIGINT handler will be restored.
498 otherwise original SIGINT handler will be restored.
500 """
499 """
501 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
500 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
502 time.sleep(0.1)
501 time.sleep(0.1)
503 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
502 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
504 sys.stdout.flush()
503 sys.stdout.flush()
505 r,w,x = select.select([sys.stdin], [], [], 5)
504 r,w,x = select.select([sys.stdin], [], [], 5)
506 if r:
505 if r:
507 line = sys.stdin.readline()
506 line = sys.stdin.readline()
508 if line.lower().startswith('y'):
507 if line.lower().startswith('y'):
509 self.log.critical("Shutdown confirmed")
508 self.log.critical("Shutdown confirmed")
510 ioloop.IOLoop.instance().stop()
509 ioloop.IOLoop.instance().stop()
511 return
510 return
512 else:
511 else:
513 print "No answer for 5s:",
512 print "No answer for 5s:",
514 print "resuming operation..."
513 print "resuming operation..."
515 # no answer, or answer is no:
514 # no answer, or answer is no:
516 # set it back to original SIGINT handler
515 # set it back to original SIGINT handler
517 # use IOLoop.add_callback because signal.signal must be called
516 # use IOLoop.add_callback because signal.signal must be called
518 # from main thread
517 # from main thread
519 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
518 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
520
519
521 def _signal_stop(self, sig, frame):
520 def _signal_stop(self, sig, frame):
522 self.log.critical("received signal %s, stopping", sig)
521 self.log.critical("received signal %s, stopping", sig)
523 ioloop.IOLoop.instance().stop()
522 ioloop.IOLoop.instance().stop()
524
523
525 @catch_config_error
524 @catch_config_error
526 def initialize(self, argv=None):
525 def initialize(self, argv=None):
527 self.init_logging()
526 self.init_logging()
528 super(NotebookApp, self).initialize(argv)
527 super(NotebookApp, self).initialize(argv)
529 self.init_configurables()
528 self.init_configurables()
530 self.init_webapp()
529 self.init_webapp()
531 self.init_signal()
530 self.init_signal()
532
531
533 def cleanup_kernels(self):
532 def cleanup_kernels(self):
534 """shutdown all kernels
533 """shutdown all kernels
535
534
536 The kernels will shutdown themselves when this process no longer exists,
535 The kernels will shutdown themselves when this process no longer exists,
537 but explicit shutdown allows the KernelManagers to cleanup the connection files.
536 but explicit shutdown allows the KernelManagers to cleanup the connection files.
538 """
537 """
539 self.log.info('Shutting down kernels')
538 self.log.info('Shutting down kernels')
540 km = self.kernel_manager
539 km = self.kernel_manager
541 # copy list, since kill_kernel deletes keys
540 # copy list, since kill_kernel deletes keys
542 for kid in list(km.kernel_ids):
541 for kid in list(km.kernel_ids):
543 km.kill_kernel(kid)
542 km.kill_kernel(kid)
544
543
545 def start(self):
544 def start(self):
546 ip = self.ip if self.ip else '[all ip addresses on your system]'
545 ip = self.ip if self.ip else '[all ip addresses on your system]'
547 proto = 'https' if self.certfile else 'http'
546 proto = 'https' if self.certfile else 'http'
548 info = self.log.info
547 info = self.log.info
549 info("The IPython Notebook is running at: %s://%s:%i%s" %
548 info("The IPython Notebook is running at: %s://%s:%i%s" %
550 (proto, ip, self.port,self.base_project_url) )
549 (proto, ip, self.port,self.base_project_url) )
551 info("Use Control-C to stop this server and shut down all kernels.")
550 info("Use Control-C to stop this server and shut down all kernels.")
552
551
553 if self.open_browser or self.file_to_run:
552 if self.open_browser or self.file_to_run:
554 ip = self.ip or '127.0.0.1'
553 ip = self.ip or '127.0.0.1'
555 if self.browser:
554 if self.browser:
556 browser = webbrowser.get(self.browser)
555 browser = webbrowser.get(self.browser)
557 else:
556 else:
558 browser = webbrowser.get()
557 browser = webbrowser.get()
559
558
560 if self.file_to_run:
559 if self.file_to_run:
561 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
560 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
562 for nb in self.notebook_manager.list_notebooks():
561 for nb in self.notebook_manager.list_notebooks():
563 if filename == nb['name']:
562 if filename == nb['name']:
564 url = nb['notebook_id']
563 url = nb['notebook_id']
565 break
564 break
566 else:
565 else:
567 url = ''
566 url = ''
568 else:
567 else:
569 url = ''
568 url = ''
570 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
569 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
571 self.port, self.base_project_url, url),
570 self.port, self.base_project_url, url),
572 new=2)
571 new=2)
573 threading.Thread(target=b).start()
572 threading.Thread(target=b).start()
574 try:
573 try:
575 ioloop.IOLoop.instance().start()
574 ioloop.IOLoop.instance().start()
576 except KeyboardInterrupt:
575 except KeyboardInterrupt:
577 info("Interrupted...")
576 info("Interrupted...")
578 finally:
577 finally:
579 self.cleanup_kernels()
578 self.cleanup_kernels()
580
579
581
580
582 #-----------------------------------------------------------------------------
581 #-----------------------------------------------------------------------------
583 # Main entry point
582 # Main entry point
584 #-----------------------------------------------------------------------------
583 #-----------------------------------------------------------------------------
585
584
586 def launch_new_instance():
585 def launch_new_instance():
587 app = NotebookApp.instance()
586 app = NotebookApp.instance()
588 app.initialize()
587 app.initialize()
589 app.start()
588 app.start()
590
589
General Comments 0
You need to be logged in to leave comments. Login now