From a9d6f2eb2d753efbf4203f3d572fce8495643389 Mon Sep 17 00:00:00 2001 From: "Brian E. Granger" Date: Sun, 14 Aug 2011 17:58:25 -0700 Subject: [PATCH] Major refactor of kernel connection management in the notebook. * Full kernel heartbeating is working. * Connections between the notebook and server and now created a new each time there is a WebSocket connection. Each channel is also handled separately. This dramatically simplifies the server code and makes for a more scalable system. --- IPython/frontend/html/notebook/handlers.py | 146 ++++++++++++++---- .../frontend/html/notebook/kernelmanager.py | 134 ++++++---------- IPython/frontend/html/notebook/notebookapp.py | 28 ++-- .../html/notebook/static/js/kernel.js | 85 ++++++---- .../html/notebook/static/js/notebook.js | 52 +++++-- 5 files changed, 275 insertions(+), 170 deletions(-) diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index aa272a45c..45608319e 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -4,20 +4,22 @@ # Imports #----------------------------------------------------------------------------- -import json -import logging -import os -import urllib from tornado import web from tornado import websocket +from zmq.eventloop import ioloop +from zmq.utils import jsonapi + +from IPython.zmq.session import Session + try: from docutils.core import publish_string except ImportError: publish_string = None + #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -52,15 +54,15 @@ class NamedNotebookHandler(web.RequestHandler): class MainKernelHandler(web.RequestHandler): def get(self): - rkm = self.application.routing_kernel_manager - self.finish(json.dumps(rkm.kernel_ids)) + km = self.application.kernel_manager + self.finish(jsonapi.dumps(km.kernel_ids)) def post(self): - rkm = self.application.routing_kernel_manager + km = self.application.kernel_manager notebook_id = self.get_argument('notebook', default=None) - kernel_id = rkm.start_kernel(notebook_id) + kernel_id = km.start_kernel(notebook_id) self.set_header('Location', '/'+kernel_id) - self.finish(json.dumps(kernel_id)) + self.finish(jsonapi.dumps(kernel_id)) class KernelHandler(web.RequestHandler): @@ -68,8 +70,8 @@ class KernelHandler(web.RequestHandler): SUPPORTED_METHODS = ('DELETE') def delete(self, kernel_id): - rkm = self.application.routing_kernel_manager - rkm.kill_kernel(kernel_id) + km = self.application.kernel_manager + km.kill_kernel(kernel_id) self.set_status(204) self.finish() @@ -77,31 +79,124 @@ class KernelHandler(web.RequestHandler): class KernelActionHandler(web.RequestHandler): def post(self, kernel_id, action): - rkm = self.application.routing_kernel_manager + km = self.application.kernel_manager if action == 'interrupt': - rkm.interrupt_kernel(kernel_id) + km.interrupt_kernel(kernel_id) self.set_status(204) if action == 'restart': - new_kernel_id = rkm.restart_kernel(kernel_id) - self.write(json.dumps(new_kernel_id)) + new_kernel_id = km.restart_kernel(kernel_id) + self.write(jsonapi.dumps(new_kernel_id)) self.finish() class ZMQStreamHandler(websocket.WebSocketHandler): - def initialize(self, stream_name): - self.stream_name = stream_name + def _reserialize_reply(self, msg_list): + """Reserialize a reply message using JSON. + + This takes the msg list from the ZMQ socket, unserializes it using + self.session and then serializes the result using JSON. This method + should be used by self._on_zmq_reply to build messages that can + be sent back to the browser. + """ + idents, msg_list = self.session.feed_identities(msg_list) + msg = self.session.unserialize(msg_list) + msg['header'].pop('date') + msg.pop('buffers') + return jsonapi.dumps(msg) + + +class IOPubHandler(ZMQStreamHandler): + + def initialize(self, *args, **kwargs): + self._kernel_alive = True + self._beating = False + + def open(self, kernel_id): + km = self.application.kernel_manager + self.kernel_id = kernel_id + self.session = Session() + self.time_to_dead = km.time_to_dead + self.iopub_stream = km.create_iopub_stream(kernel_id) + self.hb_stream = km.create_hb_stream(kernel_id) + self.iopub_stream.on_recv(self._on_zmq_reply) + self.start_hb(self.kernel_died) + + def _on_zmq_reply(self, msg_list): + msg = self._reserialize_reply(msg_list) + self.write_message(msg) + + def on_close(self): + self.stop_hb() + self.iopub_stream.close() + self.hb_stream.close() + + def start_hb(self, callback): + """Start the heartbeating and call the callback if the kernel dies.""" + if not self._beating: + self._kernel_alive = True + + def ping_or_dead(): + if self._kernel_alive: + self._kernel_alive = False + self.hb_stream.send(b'ping') + else: + try: + callback() + except: + pass + finally: + self._hb_periodic_callback.stop() + + def beat_received(msg): + self._kernel_alive = True + + self.hb_stream.on_recv(beat_received) + self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000) + self._hb_periodic_callback.start() + self._beating= True + + def stop_hb(self): + """Stop the heartbeating and cancel all related callbacks.""" + if self._beating: + self._hb_periodic_callback.stop() + if not self.hb_stream.closed(): + self.hb_stream.on_recv(None) + + def kernel_died(self): + self.write_message( + {'header': {'msg_type': 'status'}, + 'parent_header': {}, + 'content': {'execution_state':'dead'} + } + ) + self.on_close() + + +class ShellHandler(ZMQStreamHandler): + + def initialize(self, *args, **kwargs): + pass def open(self, kernel_id): - rkm = self.application.routing_kernel_manager - self.router = rkm.get_router(kernel_id, self.stream_name) - self.client_id = self.router.register_client(self) + km = self.application.kernel_manager + self.max_msg_size = km.max_msg_size + self.kernel_id = kernel_id + self.session = Session() + self.shell_stream = self.application.kernel_manager.create_shell_stream(kernel_id) + self.shell_stream.on_recv(self._on_zmq_reply) + + def _on_zmq_reply(self, msg_list): + msg = self._reserialize_reply(msg_list) + self.write_message(msg) def on_message(self, msg): - self.router.forward_msg(self.client_id, msg) + if len(msg) < self.max_msg_size: + msg = jsonapi.loads(msg) + self.session.send(self.shell_stream, msg) def on_close(self): - self.router.unregister_client(self.client_id) + self.shell_stream.close() #----------------------------------------------------------------------------- @@ -113,7 +208,7 @@ class NotebookRootHandler(web.RequestHandler): def get(self): nbm = self.application.notebook_manager files = nbm.list_notebooks() - self.finish(json.dumps(files)) + self.finish(jsonapi.dumps(files)) def post(self): nbm = self.application.notebook_manager @@ -125,7 +220,7 @@ class NotebookRootHandler(web.RequestHandler): else: notebook_id = nbm.new_notebook() self.set_header('Location', '/'+notebook_id) - self.finish(json.dumps(notebook_id)) + self.finish(jsonapi.dumps(notebook_id)) class NotebookHandler(web.RequestHandler): @@ -175,11 +270,10 @@ class RSTHandler(web.RequestHandler): body = self.request.body.strip() source = body # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html') - print template_path defaults = {'file_insertion_enabled': 0, 'raw_enabled': 0, '_disable_config': 1, - 'stylesheet_path': 0, + 'stylesheet_path': 0 # 'template': template_path } try: diff --git a/IPython/frontend/html/notebook/kernelmanager.py b/IPython/frontend/html/notebook/kernelmanager.py index e6735bb06..de6796fea 100644 --- a/IPython/frontend/html/notebook/kernelmanager.py +++ b/IPython/frontend/html/notebook/kernelmanager.py @@ -16,14 +16,13 @@ import sys import uuid import zmq +from zmq.eventloop.zmqstream import ZMQStream from tornado import web -from .routers import IOPubStreamRouter, ShellStreamRouter - from IPython.config.configurable import LoggingConfigurable from IPython.zmq.ipkernel import launch_kernel -from IPython.utils.traitlets import Instance, Dict, List, Unicode +from IPython.utils.traitlets import Instance, Dict, List, Unicode, Float, Int #----------------------------------------------------------------------------- # Classes @@ -110,6 +109,7 @@ class KernelManager(LoggingConfigurable): else: kernel_process.send_signal(signal.SIGINT) + def signal_kernel(self, kernel_id, signum): """ Sends a signal to the kernel by its uuid. @@ -182,34 +182,50 @@ class KernelManager(LoggingConfigurable): else: raise KeyError("Kernel with id not found: %s" % kernel_id) - def create_session_manager(self, kernel_id): - """Create a new session manager for a kernel by its uuid.""" - from sessionmanager import SessionManager - return SessionManager( - kernel_id=kernel_id, kernel_manager=self, - config=self.config, context=self.context, log=self.log - ) + def create_connected_stream(self, ip, port, socket_type): + sock = self.context.socket(socket_type) + addr = "tcp://%s:%i" % (ip, port) + self.log.info("Connecting to: %s" % addr) + sock.connect(addr) + return ZMQStream(sock) + + def create_iopub_stream(self, kernel_id): + ip = self.get_kernel_ip(kernel_id) + ports = self.get_kernel_ports(kernel_id) + iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB) + iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'') + return iopub_stream + def create_shell_stream(self, kernel_id): + ip = self.get_kernel_ip(kernel_id) + ports = self.get_kernel_ports(kernel_id) + shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ) + return shell_stream -class RoutingKernelManager(LoggingConfigurable): - """A KernelManager that handles WebSocket routing and HTTP error handling""" + def create_hb_stream(self, kernel_id): + ip = self.get_kernel_ip(kernel_id) + ports = self.get_kernel_ports(kernel_id) + hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ) + return hb_stream + + +class MappingKernelManager(KernelManager): + """A KernelManager that handles notebok mapping and HTTP error handling""" kernel_argv = List(Unicode) kernel_manager = Instance(KernelManager) + time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""") + max_msg_size = Int(65536, config=True, help=""" + The max raw message size accepted from the browser + over a WebSocket connection. + """) - _routers = Dict() - _session_dict = Dict() _notebook_mapping = Dict() #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- - @property - def kernel_ids(self): - """List the kernel ids.""" - return self.kernel_manager.kernel_ids - def kernel_for_notebook(self, notebook_id): """Return the kernel_id for a notebook_id or None.""" return self._notebook_mapping.get(notebook_id) @@ -234,7 +250,7 @@ class RoutingKernelManager(LoggingConfigurable): del self._notebook_mapping[notebook_id] def start_kernel(self, notebook_id=None): - """Start a kernel an return its kernel_id. + """Start a kernel for a notebok an return its kernel_id. Parameters ---------- @@ -243,108 +259,48 @@ class RoutingKernelManager(LoggingConfigurable): is not None, this kernel will be persistent whenever the notebook requests a kernel. """ - self.log.info kernel_id = self.kernel_for_notebook(notebook_id) if kernel_id is None: kwargs = dict() kwargs['extra_arguments'] = self.kernel_argv - kernel_id = self.kernel_manager.start_kernel(**kwargs) + kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs) self.set_kernel_for_notebook(notebook_id, kernel_id) self.log.info("Kernel started: %s" % kernel_id) self.log.debug("Kernel args: %r" % kwargs) - self.start_session_manager(kernel_id) else: self.log.info("Using existing kernel: %s" % kernel_id) return kernel_id - def start_session_manager(self, kernel_id): - """Start the ZMQ sockets (a "session") to connect to a kernel.""" - sm = self.kernel_manager.create_session_manager(kernel_id) - self._session_dict[kernel_id] = sm - iopub_stream = sm.get_iopub_stream() - shell_stream = sm.get_shell_stream() - iopub_router = IOPubStreamRouter( - zmq_stream=iopub_stream, session=sm.session, config=self.config - ) - shell_router = ShellStreamRouter( - zmq_stream=shell_stream, session=sm.session, config=self.config - ) - self.set_router(kernel_id, 'iopub', iopub_router) - self.set_router(kernel_id, 'shell', shell_router) - def kill_kernel(self, kernel_id): """Kill a kernel and remove its notebook association.""" - if kernel_id not in self.kernel_manager: + if kernel_id not in self: raise web.HTTPError(404) - try: - sm = self._session_dict.pop(kernel_id) - except KeyError: - raise web.HTTPError(404) - sm.stop() - self.kernel_manager.kill_kernel(kernel_id) + super(MappingKernelManager, self).kill_kernel(kernel_id) self.delete_mapping_for_kernel(kernel_id) self.log.info("Kernel killed: %s" % kernel_id) def interrupt_kernel(self, kernel_id): """Interrupt a kernel.""" - if kernel_id not in self.kernel_manager: + if kernel_id not in self: raise web.HTTPError(404) - self.kernel_manager.interrupt_kernel(kernel_id) - self.log.debug("Kernel interrupted: %s" % kernel_id) + super(MappingKernelManager, self).interrupt_kernel(kernel_id) + self.log.info("Kernel interrupted: %s" % kernel_id) def restart_kernel(self, kernel_id): """Restart a kernel while keeping clients connected.""" - if kernel_id not in self.kernel_manager: + if kernel_id not in self: raise web.HTTPError(404) - # Get the notebook_id to preserve the kernel/notebook association + # Get the notebook_id to preserve the kernel/notebook association. notebook_id = self.notebook_for_kernel(kernel_id) # Create the new kernel first so we can move the clients over. new_kernel_id = self.start_kernel() - - # Copy the clients over to the new routers. - old_iopub_router = self.get_router(kernel_id, 'iopub') - old_shell_router = self.get_router(kernel_id, 'shell') - new_iopub_router = self.get_router(new_kernel_id, 'iopub') - new_shell_router = self.get_router(new_kernel_id, 'shell') - new_iopub_router.copy_clients(old_iopub_router) - new_shell_router.copy_clients(old_shell_router) - - # Shut down the old routers - old_shell_router.close() - old_iopub_router.close() - self.delete_router(kernel_id, 'shell') - self.delete_router(kernel_id, 'iopub') - del old_shell_router - del old_iopub_router - - # Now shutdown the old session and the kernel. - # TODO: This causes a hard crash in ZMQStream.close, which sets - # self.socket to None to hastily. We will need to fix this in PyZMQ - # itself. For now, we just leave the old kernel running :( - # Maybe this is fixed now, but nothing was changed really. + # Now kill the old kernel. self.kill_kernel(kernel_id) - # Now save the new kernel/notebook association. We have to save it # after the old kernel is killed as that will delete the mapping. self.set_kernel_for_notebook(notebook_id, new_kernel_id) - self.log.debug("Kernel restarted: %s" % new_kernel_id) return new_kernel_id - def get_router(self, kernel_id, stream_name): - """Return the router for a given kernel_id and stream name.""" - router = self._routers[(kernel_id, stream_name)] - return router - - def set_router(self, kernel_id, stream_name, router): - """Set the router for a given kernel_id and stream_name.""" - self._routers[(kernel_id, stream_name)] = router - - def delete_router(self, kernel_id, stream_name): - """Delete a router for a kernel_id and stream_name.""" - try: - del self._routers[(kernel_id, stream_name)] - except KeyError: - pass diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 6ece1998e..72750d60a 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -27,12 +27,11 @@ tornado.ioloop = ioloop from tornado import httpserver from tornado import web -from .kernelmanager import KernelManager, RoutingKernelManager -from .sessionmanager import SessionManager +from .kernelmanager import MappingKernelManager from .handlers import ( NBBrowserHandler, NewHandler, NamedNotebookHandler, - MainKernelHandler, KernelHandler, KernelActionHandler, ZMQStreamHandler, - NotebookRootHandler, NotebookHandler, RSTHandler + MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler, + ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler ) from .notebookmanager import NotebookManager @@ -45,7 +44,7 @@ from IPython.zmq.ipkernel import ( aliases as ipkernel_aliases, IPKernelApp ) -from IPython.utils.traitlets import Dict, Unicode, Int, Any, List, Enum +from IPython.utils.traitlets import Dict, Unicode, Int, List, Enum #----------------------------------------------------------------------------- # Module globals @@ -71,7 +70,7 @@ ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces class NotebookWebApplication(web.Application): - def __init__(self, routing_kernel_manager, notebook_manager, log): + def __init__(self, kernel_manager, notebook_manager, log): handlers = [ (r"/", NBBrowserHandler), (r"/new", NewHandler), @@ -79,8 +78,8 @@ class NotebookWebApplication(web.Application): (r"/kernels", MainKernelHandler), (r"/kernels/%s" % _kernel_id_regex, KernelHandler), (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler), - (r"/kernels/%s/iopub" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='iopub')), - (r"/kernels/%s/shell" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='shell')), + (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler), + (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler), (r"/notebooks", NotebookRootHandler), (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler), (r"/rstservice/render", RSTHandler) @@ -91,7 +90,7 @@ class NotebookWebApplication(web.Application): ) web.Application.__init__(self, handlers, **settings) - self.routing_kernel_manager = routing_kernel_manager + self.kernel_manager = kernel_manager self.log = log self.notebook_manager = notebook_manager @@ -114,7 +113,6 @@ aliases.update({ 'port': 'IPythonNotebookApp.port', 'keyfile': 'IPythonNotebookApp.keyfile', 'certfile': 'IPythonNotebookApp.certfile', - 'colors': 'ZMQInteractiveShell.colors', 'notebook-dir': 'NotebookManager.notebook_dir' }) @@ -136,8 +134,7 @@ class IPythonNotebookApp(BaseIPythonApplication): examples = _examples classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session, - RoutingKernelManager, NotebookManager, - KernelManager, SessionManager] + MappingKernelManager, NotebookManager] flags = Dict(flags) aliases = Dict(aliases) @@ -187,9 +184,8 @@ class IPythonNotebookApp(BaseIPythonApplication): signal.signal(signal.SIGINT, signal.SIG_DFL) # Create a KernelManager and start a kernel. - self.kernel_manager = KernelManager(config=self.config, log=self.log) - self.routing_kernel_manager = RoutingKernelManager(config=self.config, log=self.log, - kernel_manager=self.kernel_manager, kernel_argv=self.kernel_argv + self.kernel_manager = MappingKernelManager( + config=self.config, log=self.log, kernel_argv=self.kernel_argv ) self.notebook_manager = NotebookManager(config=self.config, log=self.log) @@ -204,7 +200,7 @@ class IPythonNotebookApp(BaseIPythonApplication): super(IPythonNotebookApp, self).initialize(argv) self.init_configurables() self.web_app = NotebookWebApplication( - self.routing_kernel_manager, self.notebook_manager, self.log + self.kernel_manager, self.notebook_manager, self.log ) if self.certfile: ssl_options = dict(certfile=self.certfile) diff --git a/IPython/frontend/html/notebook/static/js/kernel.js b/IPython/frontend/html/notebook/static/js/kernel.js index a5ff2741d..f7a22d202 100644 --- a/IPython/frontend/html/notebook/static/js/kernel.js +++ b/IPython/frontend/html/notebook/static/js/kernel.js @@ -11,6 +11,9 @@ var IPython = (function (IPython) { this.kernel_id = null; this.base_url = "/kernels"; this.kernel_url = null; + this.shell_channel = null; + this.iopub_channel = null; + this.running = false; }; @@ -28,33 +31,65 @@ var IPython = (function (IPython) { return msg; } - Kernel.prototype.start_kernel = function (notebook_id, callback) { + Kernel.prototype.start = function (notebook_id, callback) { var that = this; - var qs = $.param({notebook:notebook_id}); - $.post(this.base_url + '?' + qs, - function (kernel_id) { - that._handle_start_kernel(kernel_id, callback); - }, - 'json' - ); + if (!this.running) { + var qs = $.param({notebook:notebook_id}); + $.post(this.base_url + '?' + qs, + function (kernel_id) { + that._handle_start_kernel(kernel_id, callback); + }, + 'json' + ); + }; + }; + + + Kernel.prototype.restart = function (callback) { + IPython.kernel_status_widget.status_restarting(); + var url = this.kernel_url + "/restart"; + var that = this; + if (this.running) { + this.stop_channels(); + $.post(url, + function (kernel_id) { + that._handle_start_kernel(kernel_id, callback); + }, + 'json' + ); + }; }; Kernel.prototype._handle_start_kernel = function (kernel_id, callback) { + this.running = true; this.kernel_id = kernel_id; this.kernel_url = this.base_url + "/" + this.kernel_id; - this._start_channels(); + this.start_channels(); callback(); + IPython.kernel_status_widget.status_idle(); }; - Kernel.prototype._start_channels = function () { + Kernel.prototype.start_channels = function () { + this.stop_channels(); var ws_url = "ws://127.0.0.1:8888" + this.kernel_url; this.shell_channel = new WebSocket(ws_url + "/shell"); this.iopub_channel = new WebSocket(ws_url + "/iopub"); - } + }; + Kernel.prototype.stop_channels = function () { + if (this.shell_channel !== null) { + this.shell_channel.close(); + this.shell_channel = null; + }; + if (this.iopub_channel !== null) { + this.iopub_channel.close(); + this.iopub_channel = null; + }; + }; + Kernel.prototype.execute = function (code) { var content = { code : code, @@ -81,29 +116,21 @@ var IPython = (function (IPython) { Kernel.prototype.interrupt = function () { - $.post(this.kernel_url + "/interrupt"); - }; - - - Kernel.prototype.restart = function () { - IPython.kernel_status_widget.status_restarting(); - var url = this.kernel_url + "/restart" - var that = this; - $.post(url, function (kernel_id) { - console.log("Kernel restarted: " + kernel_id); - that.kernel_id = kernel_id; - that.kernel_url = that.base_url + "/" + that.kernel_id; - IPython.kernel_status_widget.status_idle(); - }, 'json'); + if (this.running) { + $.post(this.kernel_url + "/interrupt"); + }; }; Kernel.prototype.kill = function () { - var settings = { - cache : false, - type : "DELETE", + if (this.running) { + this.running = false; + var settings = { + cache : false, + type : "DELETE", + }; + $.ajax(this.kernel_url, settings); }; - $.ajax(this.kernel_url, settings); }; IPython.Kernel = Kernel; diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js index a7937ce95..994a917c1 100644 --- a/IPython/frontend/html/notebook/static/js/notebook.js +++ b/IPython/frontend/html/notebook/static/js/notebook.js @@ -482,7 +482,20 @@ var IPython = (function (IPython) { Notebook.prototype.start_kernel = function () { this.kernel = new IPython.Kernel(); var notebook_id = IPython.save_widget.get_notebook_id(); - this.kernel.start_kernel(notebook_id, $.proxy(this.kernel_started, this)); + this.kernel.start(notebook_id, $.proxy(this.kernel_started, this)); + }; + + + Notebook.prototype.restart_kernel = function () { + var notebook_id = IPython.save_widget.get_notebook_id(); + this.kernel.restart($.proxy(this.kernel_started, this)); + }; + + + Notebook.prototype.kernel_started = function () { + console.log("Kernel started: ", this.kernel.kernel_id); + this.kernel.shell_channel.onmessage = $.proxy(this.handle_shell_reply,this); + this.kernel.iopub_channel.onmessage = $.proxy(this.handle_iopub_reply,this); }; @@ -528,16 +541,41 @@ var IPython = (function (IPython) { var output_types = ['stream','display_data','pyout','pyerr']; if (output_types.indexOf(msg_type) >= 0) { this.handle_output(cell, msg_type, content); - } else if (msg_type === "status") { - if (content.execution_state === "busy") { + } else if (msg_type === 'status') { + if (content.execution_state === 'busy') { IPython.kernel_status_widget.status_busy(); - } else if (content.execution_state === "idle") { + } else if (content.execution_state === 'idle') { IPython.kernel_status_widget.status_idle(); + } else if (content.execution_state === 'dead') { + this.handle_status_dead(); }; } }; + Notebook.prototype.handle_status_dead = function () { + var that = this; + this.kernel.stop_channels(); + var dialog = $('
'); + dialog.html('The kernel has died, would you like to restart it? If you do not restart the kernel, you will be able to save the notebook, but running code will not work until the notebook is reopened.'); + $(document).append(dialog); + dialog.dialog({ + resizable: false, + modal: true, + title: "Dead kernel", + buttons : { + "Yes": function () { + that.start_kernel(); + $(this).dialog('close'); + }, + "No": function () { + $(this).dialog('close'); + } + } + }); + }; + + Notebook.prototype.handle_output = function (cell, msg_type, content) { var json = {}; json.output_type = msg_type; @@ -589,12 +627,6 @@ var IPython = (function (IPython) { return json; }; - Notebook.prototype.kernel_started = function () { - console.log("Kernel started: ", this.kernel.kernel_id); - this.kernel.shell_channel.onmessage = $.proxy(this.handle_shell_reply,this); - this.kernel.iopub_channel.onmessage = $.proxy(this.handle_iopub_reply,this); - }; - Notebook.prototype.execute_selected_cell = function (options) { // add_new: should a new cell be added if we are at the end of the nb