Improve error and output handling

Display websocket and container status messages as well as line feeds
to the user.
This commit is contained in:
Janusch Jacoby
2015-09-16 19:14:36 +02:00
parent 3ccde378aa
commit 13be0f65dd
3 changed files with 61 additions and 88 deletions

View File

@ -25,7 +25,7 @@ $(function() {
numMessages = 0, numMessages = 0,
turtlecanvas = $('#turtlecanvas'), turtlecanvas = $('#turtlecanvas'),
prompt = $('#prompt'), prompt = $('#prompt'),
commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit'], commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'status'],
streams = ['stdin', 'stdout', 'stderr']; streams = ['stdin', 'stdout', 'stderr'];
var ENTER_KEY_CODE = 13; var ENTER_KEY_CODE = 13;
@ -192,20 +192,14 @@ $(function() {
var evaluateCodeWithStreamedResponse = function(url, callback) { var evaluateCodeWithStreamedResponse = function(url, callback) {
initWebsocketConnection(url); initWebsocketConnection(url);
console.log(callback);
// TODO only init turtle when required // TODO only init turtle when required
initTurtle(); initTurtle();
// TODO reimplement via websocket messsages // TODO reimplement via websocket messsages
/*var event_source = new EventSource(url); /*var event_source = new EventSource(url);
event_source.addEventListener('close', closeEventSource);
event_source.addEventListener('error', closeEventSource);
event_source.addEventListener('hint', renderHint); event_source.addEventListener('hint', renderHint);
event_source.addEventListener('info', storeContainerInformation); event_source.addEventListener('info', storeContainerInformation);
event_source.addEventListener('output', callback);
event_source.addEventListener('start', callback);
if ($('#flowrHint').isPresent()) { if ($('#flowrHint').isPresent()) {
event_source.addEventListener('output', handleStderrOutputForFlowr); event_source.addEventListener('output', handleStderrOutputForFlowr);
@ -214,11 +208,7 @@ $(function() {
if (qa_api) { if (qa_api) {
event_source.addEventListener('close', handleStreamedResponseForCodePilot); event_source.addEventListener('close', handleStreamedResponseForCodePilot);
} }*/
event_source.addEventListener('status', function(event) {
showStatus(JSON.parse(event.data));
});*/
}; };
var handleStreamedResponseForCodePilot = function(event) { var handleStreamedResponseForCodePilot = function(event) {
@ -976,7 +966,7 @@ $(function() {
if (output.status === 'timeout') { if (output.status === 'timeout') {
showTimeoutMessage(); showTimeoutMessage();
} else if (output.status === 'container_depleted') { } else if (output.status === 'container_depleted') {
showContainerDepletedMessage(); showContainerDepletedMessage();
} else if (output.stderr) { } else if (output.stderr) {
$.flash.danger({ $.flash.danger({
icon: ['fa', 'fa-bug'], icon: ['fa', 'fa-bug'],
@ -1010,6 +1000,12 @@ $(function() {
}); });
}; };
var showWebsocketError = function() {
$.flash.danger({
text: $('#flash').data('message-failure')
});
}
var showWorkspaceTab = function(event) { var showWorkspaceTab = function(event) {
event.preventDefault(); event.preventDefault();
showTab(1); showTab(1);
@ -1018,40 +1014,29 @@ $(function() {
var stopCode = function(event) { var stopCode = function(event) {
event.preventDefault(); event.preventDefault();
if ($('#stop').is(':visible')) { if ($('#stop').is(':visible')) {
killWebsocket(); killWebsocketAndContainer();
stopContainer();
} }
}; };
// todo we are missing the url here var killWebsocketAndContainer = function() {
// we could also hide the container completely by killing it on the server and only exposing the websocket
var stopContainer = function() {
var jqxhr = ajax({
data: {
container_id: $('#stop').data('container').id
},
url: $('#stop').data('url')
});
jqxhr.always(function() {
hideSpinner();
running = false;
toggleButtonStates();
});
jqxhr.fail(ajaxError);
}
var killWebsocket = function() {
if (websocket.readyState != WebSocket.OPEN) { if (websocket.readyState != WebSocket.OPEN) {
return; return;
} }
// todo flash notification
websocket.send(JSON.stringify({cmd: 'exit'})); websocket.send(JSON.stringify({cmd: 'exit'}));
websocket.flush(); websocket.flush();
websocket.close(); websocket.close();
// todo remove this once xhr works or container is killed on the server
hideSpinner(); hideSpinner();
running = false; running = false;
toggleButtonStates(); toggleButtonStates();
hidePrompt();
flashKillMessage();
}
var flashKillMessage = function() {
$.flash.info({
icon: ['fa', 'fa-clock-o'],
text: "Your program was stopped." // todo get data attribute
});
} }
// todo set this from websocket command, required to e.g. stop container // todo set this from websocket command, required to e.g. stop container
@ -1111,10 +1096,10 @@ $(function() {
var initWebsocketConnection = function(url) { var initWebsocketConnection = function(url) {
websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url); websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url);
websocket.onopen = function(evt) { onWebSocketOpen(evt) }; websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
websocket.onclose = function(evt) { onWebSocketClose(evt) }; websocket.onclose = function(evt) { /* expected at some point */ };
websocket.onmessage = function(evt) { onWebSocketMessage(evt) }; websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
websocket.onerror = function(evt) { onWebSocketError(evt) }; websocket.onerror = function(evt) { showWebsocketError(); };
websocket.flush = function() { this.send('\n'); } websocket.flush = function() { this.send('\n'); }
}; };
@ -1136,51 +1121,42 @@ $(function() {
} }
} }
var onWebSocketOpen = function(evt) {
resetOutputTab();
};
var onWebSocketClose = function(evt) {
// no reason to alert since this will happen either way
};
var onWebSocketMessage = function(evt) {
parseCanvasMessage(evt.data, true);
};
var onWebSocketError = function(evt) {
// todo flash error message
};
var executeWebsocketCommand = function(msg) { var executeWebsocketCommand = function(msg) {
if ($.inArray(msg.cmd, commands) == -1) { if ($.inArray(msg.cmd, commands) == -1) {
console.log("Unknown command: " + msg.cmd); console.log("Unknown command: " + msg.cmd);
// skipping unregistered commands is required // skipping unregistered commands is required
// as we may receive mirrored response due to internal behaviour // as we may receive mirrored response due to internal behaviour
return; return;
} }
switch(msg.cmd) { switch(msg.cmd) {
case 'input': case 'input':
showPrompt(); showPrompt();
break; break;
case 'write': case 'write':
printWebsocketOutput(msg); printWebsocketOutput(msg);
break; break;
case 'turtle': case 'turtle':
showCanvas(); showCanvas();
handleTurtleCommand(msg); handleTurtleCommand(msg);
break; break;
case 'turtlebatch': case 'turtlebatch':
showCanvas(); showCanvas();
handleTurtlebatchCommand(msg); handleTurtlebatchCommand(msg);
break; break;
case 'exit': case 'exit':
killWebsocket(); killWebsocketAndContainer();
break; break;
case 'status':
showStatus(msg)
break;
} }
}; };
var printWebsocketOutput = function(msg) { var printWebsocketOutput = function(msg) {
if (!msg.data) {
return;
}
msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "</br>");
var stream = {}; var stream = {};
stream[msg.stream] = msg.data; stream[msg.stream] = msg.data;
printOutput(stream, true, 0); printOutput(stream, true, 0);
@ -1210,7 +1186,6 @@ $(function() {
} }
var submitPromptInput = function() { var submitPromptInput = function() {
// todo make sure websocket is actually open
var input = $('#prompt-input'); var input = $('#prompt-input');
var message = input.val(); var message = input.val();
websocket.send(JSON.stringify({cmd: 'result', 'data': message})); websocket.send(JSON.stringify({cmd: 'result', 'data': message}));

View File

@ -78,8 +78,6 @@ class SubmissionsController < ApplicationController
# server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout] # server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout]
# server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr] # server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr]
# server_sent_event.write({status: output[:status]}, event: 'status')
# unless output[:stderr].nil? # unless output[:stderr].nil?
# if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr]) # if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr])
# server_sent_event.write(hint, event: 'hint') # server_sent_event.write(hint, event: 'hint')
@ -93,11 +91,7 @@ class SubmissionsController < ApplicationController
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
result = @docker_client.execute_run_command(@submission, params[:filename]) result = @docker_client.execute_run_command(@submission, params[:filename])
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})
if result[:status] == :container_depleted
tubesock.send_data JSON.dump({'cmd' => 'container_depleted'})
kill_socket(tubesock)
end
if result[:status] == :container_running if result[:status] == :container_running
socket = result[:socket] socket = result[:socket]
@ -127,6 +121,8 @@ class SubmissionsController < ApplicationController
socket.send data socket.send data
end end
end end
else
kill_socket(tubesock)
end end
end end
end end

View File

@ -164,7 +164,7 @@ class DockerClient
def kill_after_timeout(container) def kill_after_timeout(container)
""" """
We need to start a second thread to kill the websocket connection, We need to start a second thread to kill the websocket connection,
as it is impossible to determine when no more input is requested. as it is impossible to determine whether further input is requested.
""" """
Thread.new do Thread.new do
timeout = @execution_environment.permitted_execution_time.to_i # seconds timeout = @execution_environment.permitted_execution_time.to_i # seconds
@ -175,9 +175,11 @@ class DockerClient
end end
def kill_container(container) def kill_container(container)
"""
# todo won't this always create a new container? Please note that we cannot properly recycle containers when using
# It does, because it's impossible to determine wether a programm is still running or not while using ws to run. websockets because it is impossible to determine whether a program
is still running.
"""
# remove container from pool, then destroy it # remove container from pool, then destroy it
(DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : (DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) :