##// END OF EJS Templates
add first_beat delay to notebook heartbeats...
MinRK -
Show More
@@ -1,614 +1,618 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 uuid
22 import uuid
22
23
23 from tornado import web
24 from tornado import web
24 from tornado import websocket
25 from tornado import websocket
25
26
26 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
27 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
28
29
29 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
30 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
31 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
32
33
33 try:
34 try:
34 from docutils.core import publish_string
35 from docutils.core import publish_string
35 except ImportError:
36 except ImportError:
36 publish_string = None
37 publish_string = None
37
38
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
41
42
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # parts tornado cares about haven't really changed, so it's OK to continue
44 # parts tornado cares about haven't really changed, so it's OK to continue
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # version as of Oct 30/2011) the version check fails, see the issue report:
46 # version as of Oct 30/2011) the version check fails, see the issue report:
46
47
47 # https://github.com/facebook/tornado/issues/385
48 # https://github.com/facebook/tornado/issues/385
48
49
49 # This issue has been fixed in Tornado post 2.1.1:
50 # This issue has been fixed in Tornado post 2.1.1:
50
51
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52
53
53 # Here we manually apply the same patch as above so that users of IPython can
54 # Here we manually apply the same patch as above so that users of IPython can
54 # continue to work with an officially released Tornado. We make the
55 # continue to work with an officially released Tornado. We make the
55 # monkeypatch version check as narrow as possible to limit its effects; once
56 # monkeypatch version check as narrow as possible to limit its effects; once
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57
58
58 import tornado
59 import tornado
59
60
60 if tornado.version_info <= (2,1,1):
61 if tornado.version_info <= (2,1,1):
61
62
62 def _execute(self, transforms, *args, **kwargs):
63 def _execute(self, transforms, *args, **kwargs):
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64
65
65 self.open_args = args
66 self.open_args = args
66 self.open_kwargs = kwargs
67 self.open_kwargs = kwargs
67
68
68 # The difference between version 8 and 13 is that in 8 the
69 # The difference between version 8 and 13 is that in 8 the
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # simply "Origin".
71 # simply "Origin".
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection.accept_connection()
74 self.ws_connection.accept_connection()
74
75
75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 elif self.request.headers.get("Sec-WebSocket-Version"):
76 self.stream.write(tornado.escape.utf8(
77 self.stream.write(tornado.escape.utf8(
77 "HTTP/1.1 426 Upgrade Required\r\n"
78 "HTTP/1.1 426 Upgrade Required\r\n"
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 self.stream.close()
80 self.stream.close()
80
81
81 else:
82 else:
82 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection.accept_connection()
84 self.ws_connection.accept_connection()
84
85
85 websocket.WebSocketHandler._execute = _execute
86 websocket.WebSocketHandler._execute = _execute
86 del _execute
87 del _execute
87
88
88 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
89 # Decorator for disabling read-only handlers
90 # Decorator for disabling read-only handlers
90 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
91
92
92 @decorator
93 @decorator
93 def not_if_readonly(f, self, *args, **kwargs):
94 def not_if_readonly(f, self, *args, **kwargs):
94 if self.application.read_only:
95 if self.application.read_only:
95 raise web.HTTPError(403, "Notebook server is read-only")
96 raise web.HTTPError(403, "Notebook server is read-only")
96 else:
97 else:
97 return f(self, *args, **kwargs)
98 return f(self, *args, **kwargs)
98
99
99 @decorator
100 @decorator
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 """authenticate this page *unless* readonly view is active.
102 """authenticate this page *unless* readonly view is active.
102
103
103 In read-only mode, the notebook list and print view should
104 In read-only mode, the notebook list and print view should
104 be accessible without authentication.
105 be accessible without authentication.
105 """
106 """
106
107
107 @web.authenticated
108 @web.authenticated
108 def auth_f(self, *args, **kwargs):
109 def auth_f(self, *args, **kwargs):
109 return f(self, *args, **kwargs)
110 return f(self, *args, **kwargs)
110 if self.application.read_only:
111 if self.application.read_only:
111 return f(self, *args, **kwargs)
112 return f(self, *args, **kwargs)
112 else:
113 else:
113 return auth_f(self, *args, **kwargs)
114 return auth_f(self, *args, **kwargs)
114
115
115 #-----------------------------------------------------------------------------
116 #-----------------------------------------------------------------------------
116 # Top-level handlers
117 # Top-level handlers
117 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
118
119
119 class RequestHandler(web.RequestHandler):
120 class RequestHandler(web.RequestHandler):
120 """RequestHandler with default variable setting."""
121 """RequestHandler with default variable setting."""
121
122
122 def render(*args, **kwargs):
123 def render(*args, **kwargs):
123 kwargs.setdefault('message', '')
124 kwargs.setdefault('message', '')
124 return web.RequestHandler.render(*args, **kwargs)
125 return web.RequestHandler.render(*args, **kwargs)
125
126
126 class AuthenticatedHandler(RequestHandler):
127 class AuthenticatedHandler(RequestHandler):
127 """A RequestHandler with an authenticated user."""
128 """A RequestHandler with an authenticated user."""
128
129
129 def get_current_user(self):
130 def get_current_user(self):
130 user_id = self.get_secure_cookie("username")
131 user_id = self.get_secure_cookie("username")
131 # For now the user_id should not return empty, but it could eventually
132 # For now the user_id should not return empty, but it could eventually
132 if user_id == '':
133 if user_id == '':
133 user_id = 'anonymous'
134 user_id = 'anonymous'
134 if user_id is None:
135 if user_id is None:
135 # prevent extra Invalid cookie sig warnings:
136 # prevent extra Invalid cookie sig warnings:
136 self.clear_cookie('username')
137 self.clear_cookie('username')
137 if not self.application.password and not self.application.read_only:
138 if not self.application.password and not self.application.read_only:
138 user_id = 'anonymous'
139 user_id = 'anonymous'
139 return user_id
140 return user_id
140
141
141 @property
142 @property
142 def logged_in(self):
143 def logged_in(self):
143 """Is a user currently logged in?
144 """Is a user currently logged in?
144
145
145 """
146 """
146 user = self.get_current_user()
147 user = self.get_current_user()
147 return (user and not user == 'anonymous')
148 return (user and not user == 'anonymous')
148
149
149 @property
150 @property
150 def login_available(self):
151 def login_available(self):
151 """May a user proceed to log in?
152 """May a user proceed to log in?
152
153
153 This returns True if login capability is available, irrespective of
154 This returns True if login capability is available, irrespective of
154 whether the user is already logged in or not.
155 whether the user is already logged in or not.
155
156
156 """
157 """
157 return bool(self.application.password)
158 return bool(self.application.password)
158
159
159 @property
160 @property
160 def read_only(self):
161 def read_only(self):
161 """Is the notebook read-only?
162 """Is the notebook read-only?
162
163
163 """
164 """
164 return self.application.read_only
165 return self.application.read_only
165
166
166 @property
167 @property
167 def ws_url(self):
168 def ws_url(self):
168 """websocket url matching the current request
169 """websocket url matching the current request
169
170
170 turns http[s]://host[:port] into
171 turns http[s]://host[:port] into
171 ws[s]://host[:port]
172 ws[s]://host[:port]
172 """
173 """
173 proto = self.request.protocol.replace('http', 'ws')
174 proto = self.request.protocol.replace('http', 'ws')
174 return "%s://%s" % (proto, self.request.host)
175 return "%s://%s" % (proto, self.request.host)
175
176
176
177
177 class ProjectDashboardHandler(AuthenticatedHandler):
178 class ProjectDashboardHandler(AuthenticatedHandler):
178
179
179 @authenticate_unless_readonly
180 @authenticate_unless_readonly
180 def get(self):
181 def get(self):
181 nbm = self.application.notebook_manager
182 nbm = self.application.notebook_manager
182 project = nbm.notebook_dir
183 project = nbm.notebook_dir
183 self.render(
184 self.render(
184 'projectdashboard.html', project=project,
185 'projectdashboard.html', project=project,
185 base_project_url=u'/', base_kernel_url=u'/',
186 base_project_url=u'/', base_kernel_url=u'/',
186 read_only=self.read_only,
187 read_only=self.read_only,
187 logged_in=self.logged_in,
188 logged_in=self.logged_in,
188 login_available=self.login_available
189 login_available=self.login_available
189 )
190 )
190
191
191
192
192 class LoginHandler(AuthenticatedHandler):
193 class LoginHandler(AuthenticatedHandler):
193
194
194 def _render(self, message=None):
195 def _render(self, message=None):
195 self.render('login.html',
196 self.render('login.html',
196 next=self.get_argument('next', default='/'),
197 next=self.get_argument('next', default='/'),
197 read_only=self.read_only,
198 read_only=self.read_only,
198 logged_in=self.logged_in,
199 logged_in=self.logged_in,
199 login_available=self.login_available,
200 login_available=self.login_available,
200 message=message
201 message=message
201 )
202 )
202
203
203 def get(self):
204 def get(self):
204 if self.current_user:
205 if self.current_user:
205 self.redirect(self.get_argument('next', default='/'))
206 self.redirect(self.get_argument('next', default='/'))
206 else:
207 else:
207 self._render()
208 self._render()
208
209
209 def post(self):
210 def post(self):
210 pwd = self.get_argument('password', default=u'')
211 pwd = self.get_argument('password', default=u'')
211 if self.application.password:
212 if self.application.password:
212 if passwd_check(self.application.password, pwd):
213 if passwd_check(self.application.password, pwd):
213 self.set_secure_cookie('username', str(uuid.uuid4()))
214 self.set_secure_cookie('username', str(uuid.uuid4()))
214 else:
215 else:
215 self._render(message={'error': 'Invalid password'})
216 self._render(message={'error': 'Invalid password'})
216 return
217 return
217
218
218 self.redirect(self.get_argument('next', default='/'))
219 self.redirect(self.get_argument('next', default='/'))
219
220
220
221
221 class LogoutHandler(AuthenticatedHandler):
222 class LogoutHandler(AuthenticatedHandler):
222
223
223 def get(self):
224 def get(self):
224 self.clear_cookie('username')
225 self.clear_cookie('username')
225 if self.login_available:
226 if self.login_available:
226 message = {'info': 'Successfully logged out.'}
227 message = {'info': 'Successfully logged out.'}
227 else:
228 else:
228 message = {'warning': 'Cannot log out. Notebook authentication '
229 message = {'warning': 'Cannot log out. Notebook authentication '
229 'is disabled.'}
230 'is disabled.'}
230
231
231 self.render('logout.html',
232 self.render('logout.html',
232 read_only=self.read_only,
233 read_only=self.read_only,
233 logged_in=self.logged_in,
234 logged_in=self.logged_in,
234 login_available=self.login_available,
235 login_available=self.login_available,
235 message=message)
236 message=message)
236
237
237
238
238 class NewHandler(AuthenticatedHandler):
239 class NewHandler(AuthenticatedHandler):
239
240
240 @web.authenticated
241 @web.authenticated
241 def get(self):
242 def get(self):
242 nbm = self.application.notebook_manager
243 nbm = self.application.notebook_manager
243 project = nbm.notebook_dir
244 project = nbm.notebook_dir
244 notebook_id = nbm.new_notebook()
245 notebook_id = nbm.new_notebook()
245 self.render(
246 self.render(
246 'notebook.html', project=project,
247 'notebook.html', project=project,
247 notebook_id=notebook_id,
248 notebook_id=notebook_id,
248 base_project_url=u'/', base_kernel_url=u'/',
249 base_project_url=u'/', base_kernel_url=u'/',
249 kill_kernel=False,
250 kill_kernel=False,
250 read_only=False,
251 read_only=False,
251 logged_in=self.logged_in,
252 logged_in=self.logged_in,
252 login_available=self.login_available,
253 login_available=self.login_available,
253 mathjax_url=self.application.ipython_app.mathjax_url,
254 mathjax_url=self.application.ipython_app.mathjax_url,
254 )
255 )
255
256
256
257
257 class NamedNotebookHandler(AuthenticatedHandler):
258 class NamedNotebookHandler(AuthenticatedHandler):
258
259
259 @authenticate_unless_readonly
260 @authenticate_unless_readonly
260 def get(self, notebook_id):
261 def get(self, notebook_id):
261 nbm = self.application.notebook_manager
262 nbm = self.application.notebook_manager
262 project = nbm.notebook_dir
263 project = nbm.notebook_dir
263 if not nbm.notebook_exists(notebook_id):
264 if not nbm.notebook_exists(notebook_id):
264 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
265 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
265
266
266 self.render(
267 self.render(
267 'notebook.html', project=project,
268 'notebook.html', project=project,
268 notebook_id=notebook_id,
269 notebook_id=notebook_id,
269 base_project_url=u'/', base_kernel_url=u'/',
270 base_project_url=u'/', base_kernel_url=u'/',
270 kill_kernel=False,
271 kill_kernel=False,
271 read_only=self.read_only,
272 read_only=self.read_only,
272 logged_in=self.logged_in,
273 logged_in=self.logged_in,
273 login_available=self.login_available,
274 login_available=self.login_available,
274 mathjax_url=self.application.ipython_app.mathjax_url,
275 mathjax_url=self.application.ipython_app.mathjax_url,
275 )
276 )
276
277
277
278
278 #-----------------------------------------------------------------------------
279 #-----------------------------------------------------------------------------
279 # Kernel handlers
280 # Kernel handlers
280 #-----------------------------------------------------------------------------
281 #-----------------------------------------------------------------------------
281
282
282
283
283 class MainKernelHandler(AuthenticatedHandler):
284 class MainKernelHandler(AuthenticatedHandler):
284
285
285 @web.authenticated
286 @web.authenticated
286 def get(self):
287 def get(self):
287 km = self.application.kernel_manager
288 km = self.application.kernel_manager
288 self.finish(jsonapi.dumps(km.kernel_ids))
289 self.finish(jsonapi.dumps(km.kernel_ids))
289
290
290 @web.authenticated
291 @web.authenticated
291 def post(self):
292 def post(self):
292 km = self.application.kernel_manager
293 km = self.application.kernel_manager
293 notebook_id = self.get_argument('notebook', default=None)
294 notebook_id = self.get_argument('notebook', default=None)
294 kernel_id = km.start_kernel(notebook_id)
295 kernel_id = km.start_kernel(notebook_id)
295 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
296 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
296 self.set_header('Location', '/'+kernel_id)
297 self.set_header('Location', '/'+kernel_id)
297 self.finish(jsonapi.dumps(data))
298 self.finish(jsonapi.dumps(data))
298
299
299
300
300 class KernelHandler(AuthenticatedHandler):
301 class KernelHandler(AuthenticatedHandler):
301
302
302 SUPPORTED_METHODS = ('DELETE')
303 SUPPORTED_METHODS = ('DELETE')
303
304
304 @web.authenticated
305 @web.authenticated
305 def delete(self, kernel_id):
306 def delete(self, kernel_id):
306 km = self.application.kernel_manager
307 km = self.application.kernel_manager
307 km.kill_kernel(kernel_id)
308 km.kill_kernel(kernel_id)
308 self.set_status(204)
309 self.set_status(204)
309 self.finish()
310 self.finish()
310
311
311
312
312 class KernelActionHandler(AuthenticatedHandler):
313 class KernelActionHandler(AuthenticatedHandler):
313
314
314 @web.authenticated
315 @web.authenticated
315 def post(self, kernel_id, action):
316 def post(self, kernel_id, action):
316 km = self.application.kernel_manager
317 km = self.application.kernel_manager
317 if action == 'interrupt':
318 if action == 'interrupt':
318 km.interrupt_kernel(kernel_id)
319 km.interrupt_kernel(kernel_id)
319 self.set_status(204)
320 self.set_status(204)
320 if action == 'restart':
321 if action == 'restart':
321 new_kernel_id = km.restart_kernel(kernel_id)
322 new_kernel_id = km.restart_kernel(kernel_id)
322 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
323 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
323 self.set_header('Location', '/'+new_kernel_id)
324 self.set_header('Location', '/'+new_kernel_id)
324 self.write(jsonapi.dumps(data))
325 self.write(jsonapi.dumps(data))
325 self.finish()
326 self.finish()
326
327
327
328
328 class ZMQStreamHandler(websocket.WebSocketHandler):
329 class ZMQStreamHandler(websocket.WebSocketHandler):
329
330
330 def _reserialize_reply(self, msg_list):
331 def _reserialize_reply(self, msg_list):
331 """Reserialize a reply message using JSON.
332 """Reserialize a reply message using JSON.
332
333
333 This takes the msg list from the ZMQ socket, unserializes it using
334 This takes the msg list from the ZMQ socket, unserializes it using
334 self.session and then serializes the result using JSON. This method
335 self.session and then serializes the result using JSON. This method
335 should be used by self._on_zmq_reply to build messages that can
336 should be used by self._on_zmq_reply to build messages that can
336 be sent back to the browser.
337 be sent back to the browser.
337 """
338 """
338 idents, msg_list = self.session.feed_identities(msg_list)
339 idents, msg_list = self.session.feed_identities(msg_list)
339 msg = self.session.unserialize(msg_list)
340 msg = self.session.unserialize(msg_list)
340 try:
341 try:
341 msg['header'].pop('date')
342 msg['header'].pop('date')
342 except KeyError:
343 except KeyError:
343 pass
344 pass
344 try:
345 try:
345 msg['parent_header'].pop('date')
346 msg['parent_header'].pop('date')
346 except KeyError:
347 except KeyError:
347 pass
348 pass
348 msg.pop('buffers')
349 msg.pop('buffers')
349 return jsonapi.dumps(msg)
350 return jsonapi.dumps(msg)
350
351
351 def _on_zmq_reply(self, msg_list):
352 def _on_zmq_reply(self, msg_list):
352 try:
353 try:
353 msg = self._reserialize_reply(msg_list)
354 msg = self._reserialize_reply(msg_list)
354 except:
355 except:
355 self.application.log.critical("Malformed message: %r" % msg_list)
356 self.application.log.critical("Malformed message: %r" % msg_list)
356 else:
357 else:
357 self.write_message(msg)
358 self.write_message(msg)
358
359
359
360
360 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
361 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
361
362
362 def open(self, kernel_id):
363 def open(self, kernel_id):
363 self.kernel_id = kernel_id.decode('ascii')
364 self.kernel_id = kernel_id.decode('ascii')
364 try:
365 try:
365 cfg = self.application.ipython_app.config
366 cfg = self.application.ipython_app.config
366 except AttributeError:
367 except AttributeError:
367 # protect from the case where this is run from something other than
368 # protect from the case where this is run from something other than
368 # the notebook app:
369 # the notebook app:
369 cfg = None
370 cfg = None
370 self.session = Session(config=cfg)
371 self.session = Session(config=cfg)
371 self.save_on_message = self.on_message
372 self.save_on_message = self.on_message
372 self.on_message = self.on_first_message
373 self.on_message = self.on_first_message
373
374
374 def get_current_user(self):
375 def get_current_user(self):
375 user_id = self.get_secure_cookie("username")
376 user_id = self.get_secure_cookie("username")
376 if user_id == '' or (user_id is None and not self.application.password):
377 if user_id == '' or (user_id is None and not self.application.password):
377 user_id = 'anonymous'
378 user_id = 'anonymous'
378 return user_id
379 return user_id
379
380
380 def _inject_cookie_message(self, msg):
381 def _inject_cookie_message(self, msg):
381 """Inject the first message, which is the document cookie,
382 """Inject the first message, which is the document cookie,
382 for authentication."""
383 for authentication."""
383 if isinstance(msg, unicode):
384 if isinstance(msg, unicode):
384 # Cookie can't constructor doesn't accept unicode strings for some reason
385 # Cookie can't constructor doesn't accept unicode strings for some reason
385 msg = msg.encode('utf8', 'replace')
386 msg = msg.encode('utf8', 'replace')
386 try:
387 try:
387 self.request._cookies = Cookie.SimpleCookie(msg)
388 self.request._cookies = Cookie.SimpleCookie(msg)
388 except:
389 except:
389 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
390 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
390
391
391 def on_first_message(self, msg):
392 def on_first_message(self, msg):
392 self._inject_cookie_message(msg)
393 self._inject_cookie_message(msg)
393 if self.get_current_user() is None:
394 if self.get_current_user() is None:
394 logging.warn("Couldn't authenticate WebSocket connection")
395 logging.warn("Couldn't authenticate WebSocket connection")
395 raise web.HTTPError(403)
396 raise web.HTTPError(403)
396 self.on_message = self.save_on_message
397 self.on_message = self.save_on_message
397
398
398
399
399 class IOPubHandler(AuthenticatedZMQStreamHandler):
400 class IOPubHandler(AuthenticatedZMQStreamHandler):
400
401
401 def initialize(self, *args, **kwargs):
402 def initialize(self, *args, **kwargs):
402 self._kernel_alive = True
403 self._kernel_alive = True
403 self._beating = False
404 self._beating = False
404 self.iopub_stream = None
405 self.iopub_stream = None
405 self.hb_stream = None
406 self.hb_stream = None
406
407
407 def on_first_message(self, msg):
408 def on_first_message(self, msg):
408 try:
409 try:
409 super(IOPubHandler, self).on_first_message(msg)
410 super(IOPubHandler, self).on_first_message(msg)
410 except web.HTTPError:
411 except web.HTTPError:
411 self.close()
412 self.close()
412 return
413 return
413 km = self.application.kernel_manager
414 km = self.application.kernel_manager
414 self.time_to_dead = km.time_to_dead
415 self.time_to_dead = km.time_to_dead
416 self.first_beat = km.first_beat
415 kernel_id = self.kernel_id
417 kernel_id = self.kernel_id
416 try:
418 try:
417 self.iopub_stream = km.create_iopub_stream(kernel_id)
419 self.iopub_stream = km.create_iopub_stream(kernel_id)
418 self.hb_stream = km.create_hb_stream(kernel_id)
420 self.hb_stream = km.create_hb_stream(kernel_id)
419 except web.HTTPError:
421 except web.HTTPError:
420 # WebSockets don't response to traditional error codes so we
422 # WebSockets don't response to traditional error codes so we
421 # close the connection.
423 # close the connection.
422 if not self.stream.closed():
424 if not self.stream.closed():
423 self.stream.close()
425 self.stream.close()
424 self.close()
426 self.close()
425 else:
427 else:
426 self.iopub_stream.on_recv(self._on_zmq_reply)
428 self.iopub_stream.on_recv(self._on_zmq_reply)
427 self.start_hb(self.kernel_died)
429 self.start_hb(self.kernel_died)
428
430
429 def on_message(self, msg):
431 def on_message(self, msg):
430 pass
432 pass
431
433
432 def on_close(self):
434 def on_close(self):
433 # This method can be called twice, once by self.kernel_died and once
435 # This method can be called twice, once by self.kernel_died and once
434 # from the WebSocket close event. If the WebSocket connection is
436 # from the WebSocket close event. If the WebSocket connection is
435 # closed before the ZMQ streams are setup, they could be None.
437 # closed before the ZMQ streams are setup, they could be None.
436 self.stop_hb()
438 self.stop_hb()
437 if self.iopub_stream is not None and not self.iopub_stream.closed():
439 if self.iopub_stream is not None and not self.iopub_stream.closed():
438 self.iopub_stream.on_recv(None)
440 self.iopub_stream.on_recv(None)
439 self.iopub_stream.close()
441 self.iopub_stream.close()
440 if self.hb_stream is not None and not self.hb_stream.closed():
442 if self.hb_stream is not None and not self.hb_stream.closed():
441 self.hb_stream.close()
443 self.hb_stream.close()
442
444
443 def start_hb(self, callback):
445 def start_hb(self, callback):
444 """Start the heartbeating and call the callback if the kernel dies."""
446 """Start the heartbeating and call the callback if the kernel dies."""
445 if not self._beating:
447 if not self._beating:
446 self._kernel_alive = True
448 self._kernel_alive = True
447
449
448 def ping_or_dead():
450 def ping_or_dead():
451 self.hb_stream.flush()
449 if self._kernel_alive:
452 if self._kernel_alive:
450 self._kernel_alive = False
453 self._kernel_alive = False
451 self.hb_stream.send(b'ping')
454 self.hb_stream.send(b'ping')
452 else:
455 else:
453 try:
456 try:
454 callback()
457 callback()
455 except:
458 except:
456 pass
459 pass
457 finally:
460 finally:
458 self._hb_periodic_callback.stop()
461 self._hb_periodic_callback.stop()
459
462
460 def beat_received(msg):
463 def beat_received(msg):
461 self._kernel_alive = True
464 self._kernel_alive = True
462
465
463 self.hb_stream.on_recv(beat_received)
466 self.hb_stream.on_recv(beat_received)
464 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
467 loop = ioloop.IOLoop.instance()
465 self._hb_periodic_callback.start()
468 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
469 loop.add_timeout(time.time()+self.first_beat, self._hb_periodic_callback.start)
466 self._beating= True
470 self._beating= True
467
471
468 def stop_hb(self):
472 def stop_hb(self):
469 """Stop the heartbeating and cancel all related callbacks."""
473 """Stop the heartbeating and cancel all related callbacks."""
470 if self._beating:
474 if self._beating:
471 self._hb_periodic_callback.stop()
475 self._hb_periodic_callback.stop()
472 if not self.hb_stream.closed():
476 if not self.hb_stream.closed():
473 self.hb_stream.on_recv(None)
477 self.hb_stream.on_recv(None)
474
478
475 def kernel_died(self):
479 def kernel_died(self):
476 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
480 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
477 self.write_message(
481 self.write_message(
478 {'header': {'msg_type': 'status'},
482 {'header': {'msg_type': 'status'},
479 'parent_header': {},
483 'parent_header': {},
480 'content': {'execution_state':'dead'}
484 'content': {'execution_state':'dead'}
481 }
485 }
482 )
486 )
483 self.on_close()
487 self.on_close()
484
488
485
489
486 class ShellHandler(AuthenticatedZMQStreamHandler):
490 class ShellHandler(AuthenticatedZMQStreamHandler):
487
491
488 def initialize(self, *args, **kwargs):
492 def initialize(self, *args, **kwargs):
489 self.shell_stream = None
493 self.shell_stream = None
490
494
491 def on_first_message(self, msg):
495 def on_first_message(self, msg):
492 try:
496 try:
493 super(ShellHandler, self).on_first_message(msg)
497 super(ShellHandler, self).on_first_message(msg)
494 except web.HTTPError:
498 except web.HTTPError:
495 self.close()
499 self.close()
496 return
500 return
497 km = self.application.kernel_manager
501 km = self.application.kernel_manager
498 self.max_msg_size = km.max_msg_size
502 self.max_msg_size = km.max_msg_size
499 kernel_id = self.kernel_id
503 kernel_id = self.kernel_id
500 try:
504 try:
501 self.shell_stream = km.create_shell_stream(kernel_id)
505 self.shell_stream = km.create_shell_stream(kernel_id)
502 except web.HTTPError:
506 except web.HTTPError:
503 # WebSockets don't response to traditional error codes so we
507 # WebSockets don't response to traditional error codes so we
504 # close the connection.
508 # close the connection.
505 if not self.stream.closed():
509 if not self.stream.closed():
506 self.stream.close()
510 self.stream.close()
507 self.close()
511 self.close()
508 else:
512 else:
509 self.shell_stream.on_recv(self._on_zmq_reply)
513 self.shell_stream.on_recv(self._on_zmq_reply)
510
514
511 def on_message(self, msg):
515 def on_message(self, msg):
512 if len(msg) < self.max_msg_size:
516 if len(msg) < self.max_msg_size:
513 msg = jsonapi.loads(msg)
517 msg = jsonapi.loads(msg)
514 self.session.send(self.shell_stream, msg)
518 self.session.send(self.shell_stream, msg)
515
519
516 def on_close(self):
520 def on_close(self):
517 # Make sure the stream exists and is not already closed.
521 # Make sure the stream exists and is not already closed.
518 if self.shell_stream is not None and not self.shell_stream.closed():
522 if self.shell_stream is not None and not self.shell_stream.closed():
519 self.shell_stream.close()
523 self.shell_stream.close()
520
524
521
525
522 #-----------------------------------------------------------------------------
526 #-----------------------------------------------------------------------------
523 # Notebook web service handlers
527 # Notebook web service handlers
524 #-----------------------------------------------------------------------------
528 #-----------------------------------------------------------------------------
525
529
526 class NotebookRootHandler(AuthenticatedHandler):
530 class NotebookRootHandler(AuthenticatedHandler):
527
531
528 @authenticate_unless_readonly
532 @authenticate_unless_readonly
529 def get(self):
533 def get(self):
530
534
531 nbm = self.application.notebook_manager
535 nbm = self.application.notebook_manager
532 files = nbm.list_notebooks()
536 files = nbm.list_notebooks()
533 self.finish(jsonapi.dumps(files))
537 self.finish(jsonapi.dumps(files))
534
538
535 @web.authenticated
539 @web.authenticated
536 def post(self):
540 def post(self):
537 nbm = self.application.notebook_manager
541 nbm = self.application.notebook_manager
538 body = self.request.body.strip()
542 body = self.request.body.strip()
539 format = self.get_argument('format', default='json')
543 format = self.get_argument('format', default='json')
540 name = self.get_argument('name', default=None)
544 name = self.get_argument('name', default=None)
541 if body:
545 if body:
542 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
546 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
543 else:
547 else:
544 notebook_id = nbm.new_notebook()
548 notebook_id = nbm.new_notebook()
545 self.set_header('Location', '/'+notebook_id)
549 self.set_header('Location', '/'+notebook_id)
546 self.finish(jsonapi.dumps(notebook_id))
550 self.finish(jsonapi.dumps(notebook_id))
547
551
548
552
549 class NotebookHandler(AuthenticatedHandler):
553 class NotebookHandler(AuthenticatedHandler):
550
554
551 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
555 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
552
556
553 @authenticate_unless_readonly
557 @authenticate_unless_readonly
554 def get(self, notebook_id):
558 def get(self, notebook_id):
555 nbm = self.application.notebook_manager
559 nbm = self.application.notebook_manager
556 format = self.get_argument('format', default='json')
560 format = self.get_argument('format', default='json')
557 last_mod, name, data = nbm.get_notebook(notebook_id, format)
561 last_mod, name, data = nbm.get_notebook(notebook_id, format)
558
562
559 if format == u'json':
563 if format == u'json':
560 self.set_header('Content-Type', 'application/json')
564 self.set_header('Content-Type', 'application/json')
561 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
565 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
562 elif format == u'py':
566 elif format == u'py':
563 self.set_header('Content-Type', 'application/x-python')
567 self.set_header('Content-Type', 'application/x-python')
564 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
568 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
565 self.set_header('Last-Modified', last_mod)
569 self.set_header('Last-Modified', last_mod)
566 self.finish(data)
570 self.finish(data)
567
571
568 @web.authenticated
572 @web.authenticated
569 def put(self, notebook_id):
573 def put(self, notebook_id):
570 nbm = self.application.notebook_manager
574 nbm = self.application.notebook_manager
571 format = self.get_argument('format', default='json')
575 format = self.get_argument('format', default='json')
572 name = self.get_argument('name', default=None)
576 name = self.get_argument('name', default=None)
573 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
577 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
574 self.set_status(204)
578 self.set_status(204)
575 self.finish()
579 self.finish()
576
580
577 @web.authenticated
581 @web.authenticated
578 def delete(self, notebook_id):
582 def delete(self, notebook_id):
579 nbm = self.application.notebook_manager
583 nbm = self.application.notebook_manager
580 nbm.delete_notebook(notebook_id)
584 nbm.delete_notebook(notebook_id)
581 self.set_status(204)
585 self.set_status(204)
582 self.finish()
586 self.finish()
583
587
584 #-----------------------------------------------------------------------------
588 #-----------------------------------------------------------------------------
585 # RST web service handlers
589 # RST web service handlers
586 #-----------------------------------------------------------------------------
590 #-----------------------------------------------------------------------------
587
591
588
592
589 class RSTHandler(AuthenticatedHandler):
593 class RSTHandler(AuthenticatedHandler):
590
594
591 @web.authenticated
595 @web.authenticated
592 def post(self):
596 def post(self):
593 if publish_string is None:
597 if publish_string is None:
594 raise web.HTTPError(503, u'docutils not available')
598 raise web.HTTPError(503, u'docutils not available')
595 body = self.request.body.strip()
599 body = self.request.body.strip()
596 source = body
600 source = body
597 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
601 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
598 defaults = {'file_insertion_enabled': 0,
602 defaults = {'file_insertion_enabled': 0,
599 'raw_enabled': 0,
603 'raw_enabled': 0,
600 '_disable_config': 1,
604 '_disable_config': 1,
601 'stylesheet_path': 0
605 'stylesheet_path': 0
602 # 'template': template_path
606 # 'template': template_path
603 }
607 }
604 try:
608 try:
605 html = publish_string(source, writer_name='html',
609 html = publish_string(source, writer_name='html',
606 settings_overrides=defaults
610 settings_overrides=defaults
607 )
611 )
608 except:
612 except:
609 raise web.HTTPError(400, u'Invalid RST')
613 raise web.HTTPError(400, u'Invalid RST')
610 print html
614 print html
611 self.set_header('Content-Type', 'text/html')
615 self.set_header('Content-Type', 'text/html')
612 self.finish(html)
616 self.finish(html)
613
617
614
618
@@ -1,309 +1,312 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.zmq.ipkernel import launch_kernel
30 from IPython.zmq.ipkernel import launch_kernel
31 from IPython.zmq.kernelmanager import KernelManager
31 from IPython.zmq.kernelmanager import KernelManager
32 from IPython.utils.traitlets import Instance, Dict, List, Unicode, Float, Integer
32 from IPython.utils.traitlets import Instance, Dict, List, Unicode, Float, Integer
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 context = Instance('zmq.Context')
45 context = Instance('zmq.Context')
46 def _context_default(self):
46 def _context_default(self):
47 return zmq.Context.instance()
47 return zmq.Context.instance()
48
48
49 connection_dir = Unicode('')
49 connection_dir = Unicode('')
50
50
51 _kernels = Dict()
51 _kernels = Dict()
52
52
53 @property
53 @property
54 def kernel_ids(self):
54 def kernel_ids(self):
55 """Return a list of the kernel ids of the active kernels."""
55 """Return a list of the kernel ids of the active kernels."""
56 return self._kernels.keys()
56 return self._kernels.keys()
57
57
58 def __len__(self):
58 def __len__(self):
59 """Return the number of running kernels."""
59 """Return the number of running kernels."""
60 return len(self.kernel_ids)
60 return len(self.kernel_ids)
61
61
62 def __contains__(self, kernel_id):
62 def __contains__(self, kernel_id):
63 if kernel_id in self.kernel_ids:
63 if kernel_id in self.kernel_ids:
64 return True
64 return True
65 else:
65 else:
66 return False
66 return False
67
67
68 def start_kernel(self, **kwargs):
68 def start_kernel(self, **kwargs):
69 """Start a new kernel."""
69 """Start a new kernel."""
70 kernel_id = unicode(uuid.uuid4())
70 kernel_id = unicode(uuid.uuid4())
71 # use base KernelManager for each Kernel
71 # use base KernelManager for each Kernel
72 km = KernelManager(connection_file=os.path.join(
72 km = KernelManager(connection_file=os.path.join(
73 self.connection_dir, "kernel-%s.json" % kernel_id),
73 self.connection_dir, "kernel-%s.json" % kernel_id),
74 config=self.config,
74 config=self.config,
75 )
75 )
76 km.start_kernel(**kwargs)
76 km.start_kernel(**kwargs)
77 self._kernels[kernel_id] = km
77 self._kernels[kernel_id] = km
78 return kernel_id
78 return kernel_id
79
79
80 def kill_kernel(self, kernel_id):
80 def kill_kernel(self, kernel_id):
81 """Kill a kernel by its kernel uuid.
81 """Kill a kernel by its kernel uuid.
82
82
83 Parameters
83 Parameters
84 ==========
84 ==========
85 kernel_id : uuid
85 kernel_id : uuid
86 The id of the kernel to kill.
86 The id of the kernel to kill.
87 """
87 """
88 self.get_kernel(kernel_id).kill_kernel()
88 self.get_kernel(kernel_id).kill_kernel()
89 del self._kernels[kernel_id]
89 del self._kernels[kernel_id]
90
90
91 def interrupt_kernel(self, kernel_id):
91 def interrupt_kernel(self, kernel_id):
92 """Interrupt (SIGINT) the kernel by its uuid.
92 """Interrupt (SIGINT) the kernel by its uuid.
93
93
94 Parameters
94 Parameters
95 ==========
95 ==========
96 kernel_id : uuid
96 kernel_id : uuid
97 The id of the kernel to interrupt.
97 The id of the kernel to interrupt.
98 """
98 """
99 return self.get_kernel(kernel_id).interrupt_kernel()
99 return self.get_kernel(kernel_id).interrupt_kernel()
100
100
101 def signal_kernel(self, kernel_id, signum):
101 def signal_kernel(self, kernel_id, signum):
102 """ Sends a signal to the kernel by its uuid.
102 """ Sends a signal to the kernel by its uuid.
103
103
104 Note that since only SIGTERM is supported on Windows, this function
104 Note that since only SIGTERM is supported on Windows, this function
105 is only useful on Unix systems.
105 is only useful on Unix systems.
106
106
107 Parameters
107 Parameters
108 ==========
108 ==========
109 kernel_id : uuid
109 kernel_id : uuid
110 The id of the kernel to signal.
110 The id of the kernel to signal.
111 """
111 """
112 return self.get_kernel(kernel_id).signal_kernel(signum)
112 return self.get_kernel(kernel_id).signal_kernel(signum)
113
113
114 def get_kernel(self, kernel_id):
114 def get_kernel(self, kernel_id):
115 """Get the single KernelManager object for a kernel by its uuid.
115 """Get the single KernelManager object for a kernel by its uuid.
116
116
117 Parameters
117 Parameters
118 ==========
118 ==========
119 kernel_id : uuid
119 kernel_id : uuid
120 The id of the kernel.
120 The id of the kernel.
121 """
121 """
122 km = self._kernels.get(kernel_id)
122 km = self._kernels.get(kernel_id)
123 if km is not None:
123 if km is not None:
124 return km
124 return km
125 else:
125 else:
126 raise KeyError("Kernel with id not found: %s" % kernel_id)
126 raise KeyError("Kernel with id not found: %s" % kernel_id)
127
127
128 def get_kernel_ports(self, kernel_id):
128 def get_kernel_ports(self, kernel_id):
129 """Return a dictionary of ports for a kernel.
129 """Return a dictionary of ports for a kernel.
130
130
131 Parameters
131 Parameters
132 ==========
132 ==========
133 kernel_id : uuid
133 kernel_id : uuid
134 The id of the kernel.
134 The id of the kernel.
135
135
136 Returns
136 Returns
137 =======
137 =======
138 port_dict : dict
138 port_dict : dict
139 A dict of key, value pairs where the keys are the names
139 A dict of key, value pairs where the keys are the names
140 (stdin_port,iopub_port,shell_port) and the values are the
140 (stdin_port,iopub_port,shell_port) and the values are the
141 integer port numbers for those channels.
141 integer port numbers for those channels.
142 """
142 """
143 # this will raise a KeyError if not found:
143 # this will raise a KeyError if not found:
144 km = self.get_kernel(kernel_id)
144 km = self.get_kernel(kernel_id)
145 return dict(shell_port=km.shell_port,
145 return dict(shell_port=km.shell_port,
146 iopub_port=km.iopub_port,
146 iopub_port=km.iopub_port,
147 stdin_port=km.stdin_port,
147 stdin_port=km.stdin_port,
148 hb_port=km.hb_port,
148 hb_port=km.hb_port,
149 )
149 )
150
150
151 def get_kernel_ip(self, kernel_id):
151 def get_kernel_ip(self, kernel_id):
152 """Return ip address for a kernel.
152 """Return ip address for a kernel.
153
153
154 Parameters
154 Parameters
155 ==========
155 ==========
156 kernel_id : uuid
156 kernel_id : uuid
157 The id of the kernel.
157 The id of the kernel.
158
158
159 Returns
159 Returns
160 =======
160 =======
161 ip : str
161 ip : str
162 The ip address of the kernel.
162 The ip address of the kernel.
163 """
163 """
164 return self.get_kernel(kernel_id).ip
164 return self.get_kernel(kernel_id).ip
165
165
166 def create_connected_stream(self, ip, port, socket_type):
166 def create_connected_stream(self, ip, port, socket_type):
167 sock = self.context.socket(socket_type)
167 sock = self.context.socket(socket_type)
168 addr = "tcp://%s:%i" % (ip, port)
168 addr = "tcp://%s:%i" % (ip, port)
169 self.log.info("Connecting to: %s" % addr)
169 self.log.info("Connecting to: %s" % addr)
170 sock.connect(addr)
170 sock.connect(addr)
171 return ZMQStream(sock)
171 return ZMQStream(sock)
172
172
173 def create_iopub_stream(self, kernel_id):
173 def create_iopub_stream(self, kernel_id):
174 ip = self.get_kernel_ip(kernel_id)
174 ip = self.get_kernel_ip(kernel_id)
175 ports = self.get_kernel_ports(kernel_id)
175 ports = self.get_kernel_ports(kernel_id)
176 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
176 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
177 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
177 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
178 return iopub_stream
178 return iopub_stream
179
179
180 def create_shell_stream(self, kernel_id):
180 def create_shell_stream(self, kernel_id):
181 ip = self.get_kernel_ip(kernel_id)
181 ip = self.get_kernel_ip(kernel_id)
182 ports = self.get_kernel_ports(kernel_id)
182 ports = self.get_kernel_ports(kernel_id)
183 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ)
183 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ)
184 return shell_stream
184 return shell_stream
185
185
186 def create_hb_stream(self, kernel_id):
186 def create_hb_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 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
189 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
190 return hb_stream
190 return hb_stream
191
191
192
192
193 class MappingKernelManager(MultiKernelManager):
193 class MappingKernelManager(MultiKernelManager):
194 """A KernelManager that handles notebok mapping and HTTP error handling"""
194 """A KernelManager that handles notebok mapping and HTTP error handling"""
195
195
196 kernel_argv = List(Unicode)
196 kernel_argv = List(Unicode)
197 kernel_manager = Instance(KernelManager)
197 kernel_manager = Instance(KernelManager)
198
198 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
199 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
200 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
201
199 max_msg_size = Integer(65536, config=True, help="""
202 max_msg_size = Integer(65536, config=True, help="""
200 The max raw message size accepted from the browser
203 The max raw message size accepted from the browser
201 over a WebSocket connection.
204 over a WebSocket connection.
202 """)
205 """)
203
206
204 _notebook_mapping = Dict()
207 _notebook_mapping = Dict()
205
208
206 #-------------------------------------------------------------------------
209 #-------------------------------------------------------------------------
207 # Methods for managing kernels and sessions
210 # Methods for managing kernels and sessions
208 #-------------------------------------------------------------------------
211 #-------------------------------------------------------------------------
209
212
210 def kernel_for_notebook(self, notebook_id):
213 def kernel_for_notebook(self, notebook_id):
211 """Return the kernel_id for a notebook_id or None."""
214 """Return the kernel_id for a notebook_id or None."""
212 return self._notebook_mapping.get(notebook_id)
215 return self._notebook_mapping.get(notebook_id)
213
216
214 def set_kernel_for_notebook(self, notebook_id, kernel_id):
217 def set_kernel_for_notebook(self, notebook_id, kernel_id):
215 """Associate a notebook with a kernel."""
218 """Associate a notebook with a kernel."""
216 if notebook_id is not None:
219 if notebook_id is not None:
217 self._notebook_mapping[notebook_id] = kernel_id
220 self._notebook_mapping[notebook_id] = kernel_id
218
221
219 def notebook_for_kernel(self, kernel_id):
222 def notebook_for_kernel(self, kernel_id):
220 """Return the notebook_id for a kernel_id or None."""
223 """Return the notebook_id for a kernel_id or None."""
221 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
224 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
222 if len(notebook_ids) == 1:
225 if len(notebook_ids) == 1:
223 return notebook_ids[0]
226 return notebook_ids[0]
224 else:
227 else:
225 return None
228 return None
226
229
227 def delete_mapping_for_kernel(self, kernel_id):
230 def delete_mapping_for_kernel(self, kernel_id):
228 """Remove the kernel/notebook mapping for kernel_id."""
231 """Remove the kernel/notebook mapping for kernel_id."""
229 notebook_id = self.notebook_for_kernel(kernel_id)
232 notebook_id = self.notebook_for_kernel(kernel_id)
230 if notebook_id is not None:
233 if notebook_id is not None:
231 del self._notebook_mapping[notebook_id]
234 del self._notebook_mapping[notebook_id]
232
235
233 def start_kernel(self, notebook_id=None):
236 def start_kernel(self, notebook_id=None):
234 """Start a kernel for a notebok an return its kernel_id.
237 """Start a kernel for a notebok an return its kernel_id.
235
238
236 Parameters
239 Parameters
237 ----------
240 ----------
238 notebook_id : uuid
241 notebook_id : uuid
239 The uuid of the notebook to associate the new kernel with. If this
242 The uuid of the notebook to associate the new kernel with. If this
240 is not None, this kernel will be persistent whenever the notebook
243 is not None, this kernel will be persistent whenever the notebook
241 requests a kernel.
244 requests a kernel.
242 """
245 """
243 kernel_id = self.kernel_for_notebook(notebook_id)
246 kernel_id = self.kernel_for_notebook(notebook_id)
244 if kernel_id is None:
247 if kernel_id is None:
245 kwargs = dict()
248 kwargs = dict()
246 kwargs['extra_arguments'] = self.kernel_argv
249 kwargs['extra_arguments'] = self.kernel_argv
247 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
250 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
248 self.set_kernel_for_notebook(notebook_id, kernel_id)
251 self.set_kernel_for_notebook(notebook_id, kernel_id)
249 self.log.info("Kernel started: %s" % kernel_id)
252 self.log.info("Kernel started: %s" % kernel_id)
250 self.log.debug("Kernel args: %r" % kwargs)
253 self.log.debug("Kernel args: %r" % kwargs)
251 else:
254 else:
252 self.log.info("Using existing kernel: %s" % kernel_id)
255 self.log.info("Using existing kernel: %s" % kernel_id)
253 return kernel_id
256 return kernel_id
254
257
255 def kill_kernel(self, kernel_id):
258 def kill_kernel(self, kernel_id):
256 """Kill a kernel and remove its notebook association."""
259 """Kill a kernel and remove its notebook association."""
257 self._check_kernel_id(kernel_id)
260 self._check_kernel_id(kernel_id)
258 super(MappingKernelManager, self).kill_kernel(kernel_id)
261 super(MappingKernelManager, self).kill_kernel(kernel_id)
259 self.delete_mapping_for_kernel(kernel_id)
262 self.delete_mapping_for_kernel(kernel_id)
260 self.log.info("Kernel killed: %s" % kernel_id)
263 self.log.info("Kernel killed: %s" % kernel_id)
261
264
262 def interrupt_kernel(self, kernel_id):
265 def interrupt_kernel(self, kernel_id):
263 """Interrupt a kernel."""
266 """Interrupt a kernel."""
264 self._check_kernel_id(kernel_id)
267 self._check_kernel_id(kernel_id)
265 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
268 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
266 self.log.info("Kernel interrupted: %s" % kernel_id)
269 self.log.info("Kernel interrupted: %s" % kernel_id)
267
270
268 def restart_kernel(self, kernel_id):
271 def restart_kernel(self, kernel_id):
269 """Restart a kernel while keeping clients connected."""
272 """Restart a kernel while keeping clients connected."""
270 self._check_kernel_id(kernel_id)
273 self._check_kernel_id(kernel_id)
271 km = self.get_kernel(kernel_id)
274 km = self.get_kernel(kernel_id)
272 km.restart_kernel(now=True)
275 km.restart_kernel(now=True)
273 self.log.info("Kernel restarted: %s" % kernel_id)
276 self.log.info("Kernel restarted: %s" % kernel_id)
274 return kernel_id
277 return kernel_id
275
278
276 # the following remains, in case the KM restart machinery is
279 # the following remains, in case the KM restart machinery is
277 # somehow unacceptable
280 # somehow unacceptable
278 # Get the notebook_id to preserve the kernel/notebook association.
281 # Get the notebook_id to preserve the kernel/notebook association.
279 notebook_id = self.notebook_for_kernel(kernel_id)
282 notebook_id = self.notebook_for_kernel(kernel_id)
280 # Create the new kernel first so we can move the clients over.
283 # Create the new kernel first so we can move the clients over.
281 new_kernel_id = self.start_kernel()
284 new_kernel_id = self.start_kernel()
282 # Now kill the old kernel.
285 # Now kill the old kernel.
283 self.kill_kernel(kernel_id)
286 self.kill_kernel(kernel_id)
284 # Now save the new kernel/notebook association. We have to save it
287 # Now save the new kernel/notebook association. We have to save it
285 # after the old kernel is killed as that will delete the mapping.
288 # after the old kernel is killed as that will delete the mapping.
286 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
289 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
287 self.log.info("Kernel restarted: %s" % new_kernel_id)
290 self.log.info("Kernel restarted: %s" % new_kernel_id)
288 return new_kernel_id
291 return new_kernel_id
289
292
290 def create_iopub_stream(self, kernel_id):
293 def create_iopub_stream(self, kernel_id):
291 """Create a new iopub stream."""
294 """Create a new iopub stream."""
292 self._check_kernel_id(kernel_id)
295 self._check_kernel_id(kernel_id)
293 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
296 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
294
297
295 def create_shell_stream(self, kernel_id):
298 def create_shell_stream(self, kernel_id):
296 """Create a new shell stream."""
299 """Create a new shell stream."""
297 self._check_kernel_id(kernel_id)
300 self._check_kernel_id(kernel_id)
298 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
301 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
299
302
300 def create_hb_stream(self, kernel_id):
303 def create_hb_stream(self, kernel_id):
301 """Create a new hb stream."""
304 """Create a new hb stream."""
302 self._check_kernel_id(kernel_id)
305 self._check_kernel_id(kernel_id)
303 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
306 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
304
307
305 def _check_kernel_id(self, kernel_id):
308 def _check_kernel_id(self, kernel_id):
306 """Check a that a kernel_id exists and raise 404 if not."""
309 """Check a that a kernel_id exists and raise 404 if not."""
307 if kernel_id not in self:
310 if kernel_id not in self:
308 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
311 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
309
312
General Comments 0
You need to be logged in to leave comments. Login now