define([
'base/js/namespace',
'jquery',
'base/js/utils',
'./comm',
'./serialize',
'widgets/js/init'
], function(IPython, $, utils, comm, serialize, widgetmanager) {
"use strict";
var Kernel = function (kernel_service_url, ws_url, notebook, name) {
this.events = notebook.events;
this.id = null;
this.name = name;
this.ws = null;
this.kernel_service_url = kernel_service_url;
this.kernel_url = null;
this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
if (!this.ws_url) {
this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
}
this.username = "username";
this.session_id = utils.uuid();
this._msg_callbacks = {};
this._msg_queue = Promise.resolve();
this.info_reply = {};
if (typeof(WebSocket) !== 'undefined') {
this.WebSocket = WebSocket;
} else if (typeof(MozWebSocket) !== 'undefined') {
this.WebSocket = MozWebSocket;
} 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.');
}
this.bind_events();
this.init_iopub_handlers();
this.comm_manager = new comm.CommManager(this);
this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
this.last_msg_id = null;
this.last_msg_callbacks = {};
this._autorestart_attempt = 0;
this._reconnect_attempt = 0;
this.reconnect_limit = 7;
};
Kernel.prototype._get_msg = function (msg_type, content, metadata, buffers) {
var msg = {
header : {
msg_id : utils.uuid(),
username : this.username,
session : this.session_id,
msg_type : msg_type,
version : "5.0"
},
metadata : metadata || {},
content : content,
buffers : buffers || [],
parent_header : {}
};
return msg;
};
Kernel.prototype.bind_events = function () {
var that = this;
this.events.on('send_input_reply.Kernel', function(evt, data) {
that.send_input_reply(data);
});
var record_status = function (evt, info) {
console.log('Kernel: ' + evt.type + ' (' + info.kernel.id + ')');
};
this.events.on('kernel_created.Kernel', record_status);
this.events.on('kernel_reconnecting.Kernel', record_status);
this.events.on('kernel_connected.Kernel', record_status);
this.events.on('kernel_starting.Kernel', record_status);
this.events.on('kernel_restarting.Kernel', record_status);
this.events.on('kernel_autorestarting.Kernel', record_status);
this.events.on('kernel_interrupting.Kernel', record_status);
this.events.on('kernel_disconnected.Kernel', record_status);
this.events.on('kernel_ready.Kernel', record_status);
this.events.on('kernel_killed.Kernel', record_status);
this.events.on('kernel_dead.Kernel', record_status);
this.events.on('kernel_ready.Kernel', function () {
that._autorestart_attempt = 0;
});
this.events.on('kernel_connected.Kernel', function () {
that._reconnect_attempt = 0;
});
};
Kernel.prototype.init_iopub_handlers = function () {
var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
this._iopub_handlers = {};
this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
this.register_iopub_handler('execute_input', $.proxy(this._handle_input_message, this));
for (var i=0; i < output_msg_types.length; i++) {
this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
}
};
Kernel.prototype.list = function (success, error) {
$.ajax(this.kernel_service_url, {
processData: false,
cache: false,
type: "GET",
dataType: "json",
success: success,
error: this._on_error(error)
});
};
Kernel.prototype.start = function (params, success, error) {
var url = this.kernel_service_url;
var qs = $.param(params || {});
if (qs !== "") {
url = url + "?" + qs;
}
this.events.trigger('kernel_starting.Kernel', {kernel: this});
var that = this;
var on_success = function (data, status, xhr) {
that.events.trigger('kernel_created.Kernel', {kernel: that});
that._kernel_created(data);
if (success) {
success(data, status, xhr);
}
};
$.ajax(url, {
processData: false,
cache: false,
type: "POST",
data: JSON.stringify({name: this.name}),
dataType: "json",
success: this._on_success(on_success),
error: this._on_error(error)
});
return url;
};
Kernel.prototype.get_info = function (success, error) {
$.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "GET",
dataType: "json",
success: this._on_success(success),
error: this._on_error(error)
});
};
Kernel.prototype.kill = function (success, error) {
this.events.trigger('kernel_killed.Kernel', {kernel: this});
this._kernel_dead();
$.ajax(this.kernel_url, {
processData: false,
cache: false,
type: "DELETE",
dataType: "json",
success: this._on_success(success),
error: this._on_error(error)
});
};
Kernel.prototype.interrupt = function (success, error) {
this.events.trigger('kernel_interrupting.Kernel', {kernel: this});
var that = this;
var on_success = function (data, status, xhr) {
that.kernel_info();
if (success) {
success(data, status, xhr);
}
};
var url = utils.url_join_encode(this.kernel_url, 'interrupt');
$.ajax(url, {
processData: false,
cache: false,
type: "POST",
dataType: "json",
success: this._on_success(on_success),
error: this._on_error(error)
});
};
Kernel.prototype.restart = function (success, error) {
this.events.trigger('kernel_restarting.Kernel', {kernel: this});
this.stop_channels();
var that = this;
var on_success = function (data, status, xhr) {
that.events.trigger('kernel_created.Kernel', {kernel: that});
that._kernel_created(data);
if (success) {
success(data, status, xhr);
}
};
var on_error = function (xhr, status, err) {
that.events.trigger('kernel_dead.Kernel', {kernel: that});
that._kernel_dead();
if (error) {
error(xhr, status, err);
}
};
var url = utils.url_join_encode(this.kernel_url, 'restart');
$.ajax(url, {
processData: false,
cache: false,
type: "POST",
dataType: "json",
success: this._on_success(on_success),
error: this._on_error(on_error)
});
};
Kernel.prototype.reconnect = function () {
if (this.is_connected()) {
return;
}
this._reconnect_attempt = this._reconnect_attempt + 1;
this.events.trigger('kernel_reconnecting.Kernel', {
kernel: this,
attempt: this._reconnect_attempt,
});
this.start_channels();
};
Kernel.prototype._on_success = function (success) {
var that = this;
return function (data, status, xhr) {
if (data) {
that.id = data.id;
that.name = data.name;
}
that.kernel_url = utils.url_join_encode(that.kernel_service_url, that.id);
if (success) {
success(data, status, xhr);
}
};
};
Kernel.prototype._on_error = function (error) {
return function (xhr, status, err) {
utils.log_ajax_error(xhr, status, err);
if (error) {
error(xhr, status, err);
}
};
};
Kernel.prototype._kernel_created = function (data) {
this.id = data.id;
this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id);
this.start_channels();
};
Kernel.prototype._kernel_connected = function () {
this.events.trigger('kernel_connected.Kernel', {kernel: this});
var that = this;
this.kernel_info(function (reply) {
that.info_reply = reply.content;
that.events.trigger('kernel_ready.Kernel', {kernel: that});
});
};
Kernel.prototype._kernel_dead = function () {
this.stop_channels();
};
Kernel.prototype.start_channels = function () {
var that = this;
this.stop_channels();
var ws_host_url = this.ws_url + this.kernel_url;
console.log("Starting WebSockets:", ws_host_url);
this.ws = new this.WebSocket([
that.ws_url,
utils.url_join_encode(that.kernel_url, 'channels'),
"?session_id=" + that.session_id
].join('')
);
var already_called_onclose = false;
var ws_closed_early = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that.get_info(function () {
that._ws_closed(ws_host_url, false);
}, function () {
that.events.trigger('kernel_dead.Kernel', {kernel: that});
that._kernel_dead();
});
}
};
var ws_closed_late = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
if ( ! evt.wasClean ){
that._ws_closed(ws_host_url, false);
}
};
var ws_error = function(evt){
if (already_called_onclose){
return;
}
already_called_onclose = true;
that._ws_closed(ws_host_url, true);
};
this.ws.onopen = $.proxy(this._ws_opened, this);
this.ws.onclose = ws_closed_early;
this.ws.onerror = ws_error;
setTimeout(function() {
if (that.ws !== null) {
that.ws.onclose = ws_closed_late;
}
}, 1000);
this.ws.onmessage = $.proxy(this._handle_ws_message, this);
};
Kernel.prototype._ws_opened = function (evt) {
if (this.is_connected()) {
this._kernel_connected();
}
};
Kernel.prototype._ws_closed = function(ws_url, error) {
this.stop_channels();
this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
if (error) {
console.log('WebSocket connection failed: ', ws_url);
this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
}
this._schedule_reconnect();
};
Kernel.prototype._schedule_reconnect = function () {
if (this._reconnect_attempt < this.reconnect_limit) {
var timeout = Math.pow(2, this._reconnect_attempt);
console.log("Connection lost, reconnecting in " + timeout + " seconds.");
setTimeout($.proxy(this.reconnect, this), 1e3 * timeout);
} else {
this.events.trigger('kernel_connection_dead.Kernel', {
kernel: this,
reconnect_attempt: this._reconnect_attempt,
});
console.log("Failed to reconnect, giving up.");
}
};
Kernel.prototype.stop_channels = function () {
var that = this;
var close = function () {
if (that.ws && that.ws.readyState === WebSocket.CLOSED) {
that.ws = null;
}
};
if (this.ws !== null) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.onclose = close;
this.ws.close();
} else {
close();
}
}
};
Kernel.prototype.is_connected = function () {
if (this.ws === null) {
return false;
}
if (this.ws.readyState !== WebSocket.OPEN) {
return false;
}
return true;
};
Kernel.prototype.is_fully_disconnected = function () {
return (this.ws === null);
};
Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
if (!this.is_connected()) {
throw new Error("kernel is not connected");
}
var msg = this._get_msg(msg_type, content, metadata, buffers);
msg.channel = 'shell';
this.ws.send(serialize.serialize(msg));
this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
return msg.header.msg_id;
};
Kernel.prototype.kernel_info = function (callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
return this.send_shell_message("kernel_info_request", {}, callbacks);
};
Kernel.prototype.inspect = function (code, cursor_pos, callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
var content = {
code : code,
cursor_pos : cursor_pos,
detail_level : 0
};
return this.send_shell_message("inspect_request", content, callbacks);
};
Kernel.prototype.execute = function (code, callbacks, options) {
var content = {
code : code,
silent : true,
store_history : false,
user_expressions : {},
allow_stdin : false
};
callbacks = callbacks || {};
if (callbacks.input !== undefined) {
content.allow_stdin = true;
}
$.extend(true, content, options);
this.events.trigger('execution_request.Kernel', {kernel: this, content: content});
return this.send_shell_message("execute_request", content, callbacks);
};
Kernel.prototype.complete = function (code, cursor_pos, callback) {
var callbacks;
if (callback) {
callbacks = { shell : { reply : callback } };
}
var content = {
code : code,
cursor_pos : cursor_pos
};
return this.send_shell_message("complete_request", content, callbacks);
};
Kernel.prototype.send_input_reply = function (input) {
if (!this.is_connected()) {
throw new Error("kernel is not connected");
}
var content = {
value : input
};
this.events.trigger('input_reply.Kernel', {kernel: this, content: content});
var msg = this._get_msg("input_reply", content);
msg.channel = 'stdin';
this.ws.send(serialize.serialize(msg));
return msg.header.msg_id;
};
Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
this._iopub_handlers[msg_type] = callback;
};
Kernel.prototype.get_iopub_handler = function (msg_type) {
return this._iopub_handlers[msg_type];
};
Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
if (msg_id == this.last_msg_id) {
return this.last_msg_callbacks;
} else {
return this._msg_callbacks[msg_id];
}
};
Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
if (this._msg_callbacks[msg_id] !== undefined ) {
delete this._msg_callbacks[msg_id];
}
};
Kernel.prototype._finish_shell = function (msg_id) {
var callbacks = this._msg_callbacks[msg_id];
if (callbacks !== undefined) {
callbacks.shell_done = true;
if (callbacks.iopub_done) {
this.clear_callbacks_for_msg(msg_id);
}
}
};
Kernel.prototype._finish_iopub = function (msg_id) {
var callbacks = this._msg_callbacks[msg_id];
if (callbacks !== undefined) {
callbacks.iopub_done = true;
if (callbacks.shell_done) {
this.clear_callbacks_for_msg(msg_id);
}
}
};
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
this.last_msg_id = msg_id;
if (callbacks) {
var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
cbcopy.shell = callbacks.shell;
cbcopy.iopub = callbacks.iopub;
cbcopy.input = callbacks.input;
cbcopy.shell_done = (!callbacks.shell);
cbcopy.iopub_done = (!callbacks.iopub);
} else {
this.last_msg_callbacks = {};
}
};
Kernel.prototype._handle_ws_message = function (e) {
var that = this;
this._msg_queue = this._msg_queue.then(function() {
return serialize.deserialize(e.data);
}).then(function(msg) {return that._finish_ws_message(msg);})
.catch(utils.reject("Couldn't process kernel message", true));
};
Kernel.prototype._finish_ws_message = function (msg) {
switch (msg.channel) {
case 'shell':
return this._handle_shell_reply(msg);
break;
case 'iopub':
return this._handle_iopub_message(msg);
break;
case 'stdin':
return this._handle_input_request(msg);
break;
default:
console.error("unrecognized message channel", msg.channel, msg);
}
};
Kernel.prototype._handle_shell_reply = function (reply) {
this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
var that = this;
var content = reply.content;
var metadata = reply.metadata;
var parent_id = reply.parent_header.msg_id;
var callbacks = this.get_callbacks_for_msg(parent_id);
var promise = Promise.resolve();
if (!callbacks || !callbacks.shell) {
return;
}
var shell_callbacks = callbacks.shell;
this._finish_shell(parent_id);
if (shell_callbacks.reply !== undefined) {
promise = promise.then(function() {return shell_callbacks.reply(reply)});
}
if (content.payload && shell_callbacks.payload) {
promise = promise.then(function() {
return that._handle_payloads(content.payload, shell_callbacks.payload, reply);
});
}
return promise;
};
Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
var promise = [];
var l = payloads.length;
for (var i=0; i<l; i++) {
var payload = payloads[i];
var callback = payload_callbacks[payload.source];
if (callback) {
promise.push(callback(payload, msg));
}
}
return Promise.all(promise);
};
Kernel.prototype._handle_status_message = function (msg) {
var execution_state = msg.content.execution_state;
var parent_id = msg.parent_header.msg_id;
var callbacks = this.get_callbacks_for_msg(parent_id);
if (callbacks && callbacks.iopub && callbacks.iopub.status) {
try {
callbacks.iopub.status(msg);
} catch (e) {
console.log("Exception in status msg handler", e, e.stack);
}
}
if (execution_state === 'busy') {
this.events.trigger('kernel_busy.Kernel', {kernel: this});
} else if (execution_state === 'idle') {
this._finish_iopub(parent_id);
this.events.trigger('kernel_idle.Kernel', {kernel: this});
} else if (execution_state === 'starting') {
this.events.trigger('kernel_starting.Kernel', {kernel: this});
var that = this;
this.kernel_info(function (reply) {
that.info_reply = reply.content;
that.events.trigger('kernel_ready.Kernel', {kernel: that});
});
} else if (execution_state === 'restarting') {
this._autorestart_attempt = this._autorestart_attempt + 1;
this.events.trigger('kernel_restarting.Kernel', {kernel: this});
this.events.trigger('kernel_autorestarting.Kernel', {kernel: this, attempt: this._autorestart_attempt});
} else if (execution_state === 'dead') {
this.events.trigger('kernel_dead.Kernel', {kernel: this});
this._kernel_dead();
}
};
Kernel.prototype._handle_clear_output = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks || !callbacks.iopub) {
return;
}
var callback = callbacks.iopub.clear_output;
if (callback) {
callback(msg);
}
};
Kernel.prototype._handle_output_message = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks || !callbacks.iopub) {
this.events.trigger('received_unsolicited_message.Kernel', msg);
return;
}
var callback = callbacks.iopub.output;
if (callback) {
callback(msg);
}
};
Kernel.prototype._handle_input_message = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
if (!callbacks) {
this.events.trigger('received_unsolicited_message.Kernel', msg);
}
};
Kernel.prototype._handle_iopub_message = function (msg) {
var handler = this.get_iopub_handler(msg.header.msg_type);
if (handler !== undefined) {
return handler(msg);
}
};
Kernel.prototype._handle_input_request = function (request) {
var header = request.header;
var content = request.content;
var metadata = request.metadata;
var msg_type = header.msg_type;
if (msg_type !== 'input_request') {
console.log("Invalid input request!", request);
return;
}
var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
if (callbacks) {
if (callbacks.input) {
callbacks.input(request);
}
}
};
IPython.Kernel = Kernel;
return {'Kernel': Kernel};
});