Merge pull request #955 from minrk/websocket

Websocket fixes:

1. alert client on failed and lost web socket connections

A long message is given if the connection fails within 1s, which assumes the connection did not succeed. Otherwise, it is a short 'connection closed unexpectedly'.

This also means that clients are notified on server termination (for better or worse).

2. remove superfluous ws-hostname parameter from notebook

This made the notebook server artificially and unnecessarily brittle against tunneling and explicit hostname resolution.  Now, the ws_url is defined based on the Origin of the request for the url, so it always matches the http[s] url.  This means that it will follow the same tunnel, and the hostname will be already resolved.  Resolving the hostname twice makes no sense at all unless the websockets are going to a different server than the http requests.

Implemented as a property, so it should still be easy to change for future cases where it might behave differently (e.g. websockets on a different host, or at a non-root url).
This commit is contained in:
Fernando Perez 2011-11-10 19:31:27 -08:00
commit 1bb4c726c3
3 changed files with 70 additions and 22 deletions

View File

@ -139,7 +139,16 @@ class AuthenticatedHandler(web.RequestHandler):
return True return True
else: else:
return False return False
@property
def ws_url(self):
"""websocket url matching the current request
turns http[s]://host[:port] into
ws[s]://host[:port]
"""
proto = self.request.protocol.replace('http', 'ws')
return "%s://%s" % (proto, self.request.host)
class ProjectDashboardHandler(AuthenticatedHandler): class ProjectDashboardHandler(AuthenticatedHandler):
@ -221,8 +230,7 @@ class MainKernelHandler(AuthenticatedHandler):
km = self.application.kernel_manager km = self.application.kernel_manager
notebook_id = self.get_argument('notebook', default=None) notebook_id = self.get_argument('notebook', default=None)
kernel_id = km.start_kernel(notebook_id) kernel_id = km.start_kernel(notebook_id)
ws_url = self.application.ipython_app.get_ws_url() data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
data = {'ws_url':ws_url,'kernel_id':kernel_id}
self.set_header('Location', '/'+kernel_id) self.set_header('Location', '/'+kernel_id)
self.finish(jsonapi.dumps(data)) self.finish(jsonapi.dumps(data))
@ -249,8 +257,7 @@ class KernelActionHandler(AuthenticatedHandler):
self.set_status(204) self.set_status(204)
if action == 'restart': if action == 'restart':
new_kernel_id = km.restart_kernel(kernel_id) new_kernel_id = km.restart_kernel(kernel_id)
ws_url = self.application.ipython_app.get_ws_url() data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
self.set_header('Location', '/'+new_kernel_id) self.set_header('Location', '/'+new_kernel_id)
self.write(jsonapi.dumps(data)) self.write(jsonapi.dumps(data))
self.finish() self.finish()

View File

@ -147,7 +147,6 @@ aliases.update({
'port': 'NotebookApp.port', 'port': 'NotebookApp.port',
'keyfile': 'NotebookApp.keyfile', 'keyfile': 'NotebookApp.keyfile',
'certfile': 'NotebookApp.certfile', 'certfile': 'NotebookApp.certfile',
'ws-hostname': 'NotebookApp.ws_hostname',
'notebook-dir': 'NotebookManager.notebook_dir', 'notebook-dir': 'NotebookManager.notebook_dir',
}) })
@ -155,7 +154,7 @@ aliases.update({
# multi-kernel evironment: # multi-kernel evironment:
aliases.pop('f', None) aliases.pop('f', None)
notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'ws-hostname', notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
u'notebook-dir'] u'notebook-dir']
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
@ -200,13 +199,6 @@ class NotebookApp(BaseIPythonApplication):
help="The port the notebook server will listen on." 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, certfile = Unicode(u'', config=True,
help="""The full path to an SSL/TLS certificate file.""" help="""The full path to an SSL/TLS certificate file."""
) )
@ -226,14 +218,6 @@ class NotebookApp(BaseIPythonApplication):
help="Whether to prevent editing/execution of notebooks." help="Whether to prevent editing/execution of notebooks."
) )
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): def parse_command_line(self, argv=None):
super(NotebookApp, self).parse_command_line(argv) super(NotebookApp, self).parse_command_line(argv)
if argv is None: if argv is None:

View File

@ -27,7 +27,7 @@ var IPython = (function (IPython) {
} else if (typeof(MozWebSocket) !== 'undefined') { } else if (typeof(MozWebSocket) !== 'undefined') {
this.WebSocket = MozWebSocket this.WebSocket = MozWebSocket
} else { } else {
alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.'); alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
}; };
}; };
@ -87,8 +87,37 @@ var IPython = (function (IPython) {
IPython.kernel_status_widget.status_idle(); IPython.kernel_status_widget.status_idle();
}; };
Kernel.prototype._websocket_closed = function(ws_url, early){
var msg;
var parent_item = $('body');
if (early) {
msg = "Websocket connection to " + ws_url + " could not be established.<br/>" +
" You will NOT be able to run code.<br/>" +
" Your browser may not be compatible with the websocket version in the server," +
" or if the url does not look right, there could be an error in the" +
" server's configuration."
} else {
msg = "Websocket connection closed unexpectedly.<br/>" +
" The kernel will no longer be responsive."
}
var dialog = $('<div/>');
dialog.html(msg);
parent_item.append(dialog);
dialog.dialog({
resizable: false,
modal: true,
title: "Websocket closed",
buttons : {
"Okay": function () {
$(this).dialog('close');
}
}
});
}
Kernel.prototype.start_channels = function () { Kernel.prototype.start_channels = function () {
var that = this;
this.stop_channels(); this.stop_channels();
var ws_url = this.ws_url + this.kernel_url; var ws_url = this.ws_url + this.kernel_url;
console.log("Starting WS:", ws_url); console.log("Starting WS:", ws_url);
@ -97,17 +126,45 @@ var IPython = (function (IPython) {
send_cookie = function(){ send_cookie = function(){
this.send(document.cookie); this.send(document.cookie);
} }
var already_called_onclose = false; // only alert once
ws_closed_early = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that._websocket_closed(ws_url, true);
}
}
ws_closed_late = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that._websocket_closed(ws_url, false);
}
}
this.shell_channel.onopen = send_cookie; this.shell_channel.onopen = send_cookie;
this.shell_channel.onclose = ws_closed_early;
this.iopub_channel.onopen = send_cookie; this.iopub_channel.onopen = send_cookie;
this.iopub_channel.onclose = ws_closed_early;
// switch from early-close to late-close message after 1s
setTimeout(function(){
that.shell_channel.onclose = ws_closed_late;
that.iopub_channel.onclose = ws_closed_late;
}, 1000);
}; };
Kernel.prototype.stop_channels = function () { Kernel.prototype.stop_channels = function () {
if (this.shell_channel !== null) { if (this.shell_channel !== null) {
this.shell_channel.onclose = function (evt) {null};
this.shell_channel.close(); this.shell_channel.close();
this.shell_channel = null; this.shell_channel = null;
}; };
if (this.iopub_channel !== null) { if (this.iopub_channel !== null) {
this.iopub_channel.onclose = function (evt) {null};
this.iopub_channel.close(); this.iopub_channel.close();
this.iopub_channel = null; this.iopub_channel = null;
}; };