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.
Brian E. Granger 15 years ago
parent b264e21a6d
commit a9d6f2eb2d

@ -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:

@ -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

@ -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)

@ -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;

@ -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 = $('<div/>');
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

Loading…
Cancel
Save