From 856b17fc21bbf7903b01984826c95060af475445 2011-08-16 06:35:01 From: Brian E. Granger Date: 2011-08-16 06:35:01 Subject: [PATCH] WebSocket url is now passed to browser when a kernel is started. --- diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index 85099fb..1d96cd9 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -61,8 +61,10 @@ class MainKernelHandler(web.RequestHandler): km = self.application.kernel_manager notebook_id = self.get_argument('notebook', default=None) kernel_id = km.start_kernel(notebook_id) + ws_url = self.application.ipython_app.get_ws_url() + data = {'ws_url':ws_url,'kernel_id':kernel_id} self.set_header('Location', '/'+kernel_id) - self.finish(jsonapi.dumps(kernel_id)) + self.finish(jsonapi.dumps(data)) class KernelHandler(web.RequestHandler): @@ -85,7 +87,10 @@ class KernelActionHandler(web.RequestHandler): self.set_status(204) if action == 'restart': new_kernel_id = km.restart_kernel(kernel_id) - self.write(jsonapi.dumps(new_kernel_id)) + ws_url = self.application.ipython_app.get_ws_url() + data = {'ws_url':ws_url,'kernel_id':new_kernel_id} + self.set_header('Location', '/'+new_kernel_id) + self.write(jsonapi.dumps(data)) self.finish() @@ -126,21 +131,36 @@ class IOPubHandler(ZMQStreamHandler): def initialize(self, *args, **kwargs): self._kernel_alive = True self._beating = False + self.iopub_stream = None + self.hb_stream = None 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) + try: + self.iopub_stream = km.create_iopub_stream(kernel_id) + self.hb_stream = km.create_hb_stream(kernel_id) + except web.HTTPError: + # WebSockets don't response to traditional error codes so we + # close the connection. + if not self.stream.closed(): + self.stream.close() + else: + self.iopub_stream.on_recv(self._on_zmq_reply) + self.start_hb(self.kernel_died) def on_close(self): + # This method can be called twice, once by self.kernel_died and once + # from the WebSocket close event. If the WebSocket connection is + # closed before the ZMQ streams are setup, they could be None. self.stop_hb() - self.iopub_stream.close() - self.hb_stream.close() + if self.iopub_stream is not None and not self.iopub_stream.closed(): + self.iopub_stream.on_recv(None) + self.iopub_stream.close() + if self.hb_stream is not None and not self.hb_stream.closed(): + self.hb_stream.close() def start_hb(self, callback): """Start the heartbeating and call the callback if the kernel dies.""" @@ -188,15 +208,22 @@ class IOPubHandler(ZMQStreamHandler): class ShellHandler(ZMQStreamHandler): def initialize(self, *args, **kwargs): - pass + self.shell_stream = None def open(self, kernel_id): 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) + try: + self.shell_stream = km.create_shell_stream(kernel_id) + except web.HTTPError: + # WebSockets don't response to traditional error codes so we + # close the connection. + if not self.stream.closed(): + self.stream.close() + else: + self.session = Session() + self.shell_stream.on_recv(self._on_zmq_reply) def on_message(self, msg): if len(msg) < self.max_msg_size: @@ -204,7 +231,9 @@ class ShellHandler(ZMQStreamHandler): self.session.send(self.shell_stream, msg) def on_close(self): - self.shell_stream.close() + # Make sure the stream exists and is not already closed. + if self.shell_stream is not None and not self.shell_stream.closed(): + self.shell_stream.close() #----------------------------------------------------------------------------- diff --git a/IPython/frontend/html/notebook/kernelmanager.py b/IPython/frontend/html/notebook/kernelmanager.py index de6796f..89bca69 100644 --- a/IPython/frontend/html/notebook/kernelmanager.py +++ b/IPython/frontend/html/notebook/kernelmanager.py @@ -300,7 +300,21 @@ class MappingKernelManager(KernelManager): # 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) + self.log.info("Kernel restarted: %s" % new_kernel_id) return new_kernel_id + def create_iopub_stream(self, kernel_id): + if kernel_id not in self: + raise web.HTTPError(404) + return super(MappingKernelManager, self).create_iopub_stream(kernel_id) + + def create_shell_stream(self, kernel_id): + if kernel_id not in self: + raise web.HTTPError(404) + return super(MappingKernelManager, self).create_shell_stream(kernel_id) + + def create_hb_stream(self, kernel_id): + if kernel_id not in self: + raise web.HTTPError(404) + return super(MappingKernelManager, self).create_hb_stream(kernel_id) diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py index 0ff2054..237ec41 100644 --- a/IPython/frontend/html/notebook/notebookapp.py +++ b/IPython/frontend/html/notebook/notebookapp.py @@ -72,7 +72,7 @@ ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces class NotebookWebApplication(web.Application): - def __init__(self, kernel_manager, notebook_manager, log): + def __init__(self, ipython_app, kernel_manager, notebook_manager, log): handlers = [ (r"/", NBBrowserHandler), (r"/new", NewHandler), @@ -95,6 +95,7 @@ class NotebookWebApplication(web.Application): self.kernel_manager = kernel_manager self.log = log self.notebook_manager = notebook_manager + self.ipython_app = ipython_app #----------------------------------------------------------------------------- @@ -115,6 +116,7 @@ aliases.update({ 'port': 'IPythonNotebookApp.port', 'keyfile': 'IPythonNotebookApp.keyfile', 'certfile': 'IPythonNotebookApp.certfile', + 'ws-hostname': 'IPythonNotebookApp.ws_hostname', 'notebook-dir': 'NotebookManager.notebook_dir' }) @@ -160,6 +162,13 @@ class IPythonNotebookApp(BaseIPythonApplication): help="The port the notebook server will listen on." ) + ws_hostname = Unicode(LOCALHOST, config=True, + help="""The FQDN or IP for WebSocket connections. The default will work + fine when the server is listening on localhost, but this needs to + be set if the ip option is used. It will be used as the hostname part + of the WebSocket url: ws://hostname/path.""" + ) + certfile = Unicode(u'', config=True, help="""The full path to an SSL/TLS certificate file.""" ) @@ -168,6 +177,14 @@ class IPythonNotebookApp(BaseIPythonApplication): help="""The full path to a private key file for usage with SSL/TLS.""" ) + def get_ws_url(self): + """Return the WebSocket URL for this server.""" + if self.certfile: + prefix = u'wss://' + else: + prefix = u'ws://' + return prefix + self.ws_hostname + u':' + unicode(self.port) + def parse_command_line(self, argv=None): super(IPythonNotebookApp, self).parse_command_line(argv) if argv is None: @@ -202,7 +219,7 @@ class IPythonNotebookApp(BaseIPythonApplication): super(IPythonNotebookApp, self).initialize(argv) self.init_configurables() self.web_app = NotebookWebApplication( - self.kernel_manager, self.notebook_manager, self.log + self, 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 f7a22d2..d6a3857 100644 --- a/IPython/frontend/html/notebook/static/js/kernel.js +++ b/IPython/frontend/html/notebook/static/js/kernel.js @@ -61,9 +61,10 @@ var IPython = (function (IPython) { }; - Kernel.prototype._handle_start_kernel = function (kernel_id, callback) { + Kernel.prototype._handle_start_kernel = function (json, callback) { this.running = true; - this.kernel_id = kernel_id; + this.kernel_id = json.kernel_id; + this.ws_url = json.ws_url; this.kernel_url = this.base_url + "/" + this.kernel_id; this.start_channels(); callback(); @@ -73,7 +74,8 @@ var IPython = (function (IPython) { Kernel.prototype.start_channels = function () { this.stop_channels(); - var ws_url = "ws://127.0.0.1:8888" + this.kernel_url; + var ws_url = this.ws_url + this.kernel_url; + console.log("Starting WS:", ws_url); this.shell_channel = new WebSocket(ws_url + "/shell"); this.iopub_channel = new WebSocket(ws_url + "/iopub"); };