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