1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@
|
||||
/tmp
|
||||
/vagrant/
|
||||
*.sublime-*
|
||||
/.idea
|
2
Gemfile
2
Gemfile
@ -33,6 +33,8 @@ gem 'thread_safe'
|
||||
gem 'turbolinks'
|
||||
gem 'uglifier', '>= 1.3.0'
|
||||
gem 'will_paginate', '~> 3.0'
|
||||
gem 'tubesock'
|
||||
gem 'faye-websocket'
|
||||
|
||||
group :development do
|
||||
gem 'better_errors', platform: :ruby
|
||||
|
16
Gemfile.lock
16
Gemfile.lock
@ -50,8 +50,6 @@ GEM
|
||||
bootstrap-will_paginate (0.0.10)
|
||||
will_paginate
|
||||
builder (3.2.2)
|
||||
byebug (4.0.5)
|
||||
columnize (= 0.9.0)
|
||||
capistrano (3.3.5)
|
||||
capistrano-stats (~> 1.1.0)
|
||||
i18n
|
||||
@ -96,7 +94,6 @@ GEM
|
||||
execjs
|
||||
coffee-script-source (1.9.1)
|
||||
colorize (0.7.7)
|
||||
columnize (0.9.0)
|
||||
concurrent-ruby (0.8.0)
|
||||
ref (~> 1.0, >= 1.0.5)
|
||||
concurrent-ruby (0.8.0-java)
|
||||
@ -110,6 +107,7 @@ GEM
|
||||
excon (>= 0.38.0)
|
||||
json
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.0.8)
|
||||
excon (0.45.2)
|
||||
execjs (2.5.2)
|
||||
factory_girl (4.5.0)
|
||||
@ -119,6 +117,9 @@ GEM
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.9.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faye-websocket (0.10.0)
|
||||
eventmachine (>= 0.12.0)
|
||||
websocket-driver (>= 0.5.1)
|
||||
ffi (1.9.8)
|
||||
ffi (1.9.8-java)
|
||||
forgery (0.6.0)
|
||||
@ -302,6 +303,9 @@ GEM
|
||||
thread_safe (0.3.5)
|
||||
thread_safe (0.3.5-java)
|
||||
tilt (1.4.1)
|
||||
tubesock (0.2.5)
|
||||
rack (>= 1.5.0)
|
||||
websocket (>= 1.1.0)
|
||||
turbolinks (2.5.3)
|
||||
coffee-rails
|
||||
tzinfo (1.2.2)
|
||||
@ -315,6 +319,9 @@ GEM
|
||||
railties (>= 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
websocket (1.2.1)
|
||||
websocket-driver (0.6.2)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
will_paginate (3.0.7)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
@ -330,7 +337,6 @@ DEPENDENCIES
|
||||
better_errors
|
||||
binding_of_caller
|
||||
bootstrap-will_paginate
|
||||
byebug
|
||||
capistrano (~> 3.3.0)
|
||||
capistrano-rails
|
||||
capistrano-rvm
|
||||
@ -345,6 +351,7 @@ DEPENDENCIES
|
||||
database_cleaner
|
||||
docker-api (~> 1.21.1)
|
||||
factory_girl_rails (~> 4.0)
|
||||
faye-websocket
|
||||
forgery
|
||||
highline
|
||||
ims-lti
|
||||
@ -375,6 +382,7 @@ DEPENDENCIES
|
||||
sorcery
|
||||
spring
|
||||
thread_safe
|
||||
tubesock
|
||||
turbolinks
|
||||
uglifier (>= 1.3.0)
|
||||
web-console (~> 2.0)
|
||||
|
@ -11,6 +11,7 @@ $(function() {
|
||||
var FILENAME_URL_PLACEHOLDER = '{filename}';
|
||||
var SUCCESSFULL_PERCENTAGE = 90;
|
||||
var THEME = 'ace/theme/textmate';
|
||||
var REMEMBER_TAB = false;
|
||||
var AUTOSAVE_INTERVAL = 15 * 1000;
|
||||
|
||||
var editors = [];
|
||||
@ -20,6 +21,16 @@ $(function() {
|
||||
var qa_api = undefined;
|
||||
var output_mode_is_streaming = true;
|
||||
|
||||
var websocket,
|
||||
turtlescreen,
|
||||
numMessages = 0,
|
||||
turtlecanvas = $('#turtlecanvas'),
|
||||
prompt = $('#prompt'),
|
||||
commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'status'],
|
||||
streams = ['stdin', 'stdout', 'stderr'];
|
||||
|
||||
var ENTER_KEY_CODE = 13;
|
||||
|
||||
var flowrResultHtml = '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>'
|
||||
|
||||
var ajax = function(options) {
|
||||
@ -49,7 +60,7 @@ $(function() {
|
||||
|
||||
if (event.type === 'error' || JSON.parse(event.data).code !== 200) {
|
||||
ajaxError();
|
||||
showTab(1);
|
||||
showTab(0);
|
||||
}
|
||||
};
|
||||
|
||||
@ -181,14 +192,15 @@ $(function() {
|
||||
};
|
||||
|
||||
var evaluateCodeWithStreamedResponse = function(url, callback) {
|
||||
var event_source = new EventSource(url);
|
||||
initWebsocketConnection(url);
|
||||
|
||||
event_source.addEventListener('close', closeEventSource);
|
||||
event_source.addEventListener('error', closeEventSource);
|
||||
// TODO only init turtle when required
|
||||
initTurtle();
|
||||
|
||||
// TODO reimplement via websocket messsages
|
||||
/*var event_source = new EventSource(url);
|
||||
event_source.addEventListener('hint', renderHint);
|
||||
event_source.addEventListener('info', storeContainerInformation);
|
||||
event_source.addEventListener('output', callback);
|
||||
event_source.addEventListener('start', callback);
|
||||
|
||||
if ($('#flowrHint').isPresent()) {
|
||||
event_source.addEventListener('output', handleStderrOutputForFlowr);
|
||||
@ -197,11 +209,7 @@ $(function() {
|
||||
|
||||
if (qa_api) {
|
||||
event_source.addEventListener('close', handleStreamedResponseForCodePilot);
|
||||
}
|
||||
|
||||
event_source.addEventListener('status', function(event) {
|
||||
showStatus(JSON.parse(event.data));
|
||||
});
|
||||
}*/
|
||||
};
|
||||
|
||||
var handleStreamedResponseForCodePilot = function(event) {
|
||||
@ -255,13 +263,11 @@ $(function() {
|
||||
|
||||
var handleKeyPress = function(event) {
|
||||
if (event.which === ALT_1_KEY_CODE) {
|
||||
showTab(0);
|
||||
} else if (event.which === ALT_2_KEY_CODE) {
|
||||
showWorkspaceTab(event);
|
||||
} else if (event.which === ALT_2_KEY_CODE) {
|
||||
showTab(1);
|
||||
} else if (event.which === ALT_3_KEY_CODE) {
|
||||
showTab(2);
|
||||
} else if (event.which === ALT_4_KEY_CODE) {
|
||||
showTab(3);
|
||||
} else if (event.which === ALT_R_KEY_CODE) {
|
||||
$('#run').trigger('click');
|
||||
} else if (event.which === ALT_S_KEY_CODE) {
|
||||
@ -304,7 +310,7 @@ $(function() {
|
||||
}, 0).toFixed(2);
|
||||
$('#score').data('score', score);
|
||||
renderScore();
|
||||
showTab(3);
|
||||
showTab(2);
|
||||
};
|
||||
|
||||
var stderrOutput = '';
|
||||
@ -357,7 +363,7 @@ $(function() {
|
||||
qa_api.executeCommand('syncOutput', [response]);
|
||||
}
|
||||
showStatus(response[0]);
|
||||
showTab(2);
|
||||
showTab(1);
|
||||
};
|
||||
|
||||
var hideSpinner = function() {
|
||||
@ -428,7 +434,7 @@ $(function() {
|
||||
handleSidebarClick(e);
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
//session
|
||||
session.on('annotationRemoval', handleAnnotationRemoval);
|
||||
session.on('annotationChange', handleAnnotationChange);
|
||||
@ -631,11 +637,11 @@ $(function() {
|
||||
};
|
||||
|
||||
var initializeWorkspaceButtons = function() {
|
||||
$('#assess').on('click', scoreCode);
|
||||
$('#assess').on('click', scoreCode); // todo
|
||||
$('#dropdown-render, #render').on('click', renderCode);
|
||||
$('#dropdown-run, #run').on('click', runCode);
|
||||
$('#dropdown-stop, #stop').on('click', stopCode);
|
||||
$('#dropdown-test, #test').on('click', testCode);
|
||||
$('#dropdown-stop, #stop').on('click', stopCode); // todo
|
||||
$('#dropdown-test, #test').on('click', testCode); // todo
|
||||
$('#save').on('click', saveCode);
|
||||
$('#start-over').on('click', confirmReset);
|
||||
};
|
||||
@ -676,6 +682,7 @@ $(function() {
|
||||
};
|
||||
|
||||
var isBrowserSupported = function() {
|
||||
// todo event streams are no longer required with websockets
|
||||
return window.EventSource !== undefined;
|
||||
};
|
||||
|
||||
@ -703,12 +710,16 @@ $(function() {
|
||||
chunkBuffer.push(output);
|
||||
}
|
||||
} else {
|
||||
resetOutputTab();
|
||||
}
|
||||
};
|
||||
|
||||
var resetOutputTab = function() {
|
||||
clearOutput();
|
||||
$('#hint').fadeOut();
|
||||
$('#flowrHint').fadeOut();
|
||||
showTab(2);
|
||||
}
|
||||
};
|
||||
showTab(1);
|
||||
}
|
||||
|
||||
var printOutput = function(output, colorize, index) {
|
||||
var element = findOrCreateOutputElement(index);
|
||||
@ -808,7 +819,7 @@ $(function() {
|
||||
stderr: message
|
||||
}, true, 0);
|
||||
sendError(message, response.id);
|
||||
showTab(2);
|
||||
showTab(1);
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -931,16 +942,21 @@ $(function() {
|
||||
|
||||
var showOutput = function(event) {
|
||||
event.preventDefault();
|
||||
showTab(2);
|
||||
showTab(1);
|
||||
$('#output').scrollTo($(this).attr('href'));
|
||||
};
|
||||
|
||||
var showRequestedTab = function() {
|
||||
var regexp = /tab=(\d+)/;
|
||||
if (regexp.test(window.location.search)) {
|
||||
var index = regexp.exec(window.location.search)[1] - 1;
|
||||
if(REMEMBER_TAB){
|
||||
var regexp = /tab=(\d+)/;
|
||||
if (regexp.test(window.location.search)) {
|
||||
var index = regexp.exec(window.location.search)[1] - 1;
|
||||
} else {
|
||||
var index = localStorage.tab;
|
||||
}
|
||||
} else {
|
||||
var index = localStorage.tab;
|
||||
// else start with first tab.
|
||||
var index = 0;
|
||||
}
|
||||
showTab(index);
|
||||
};
|
||||
@ -954,7 +970,7 @@ $(function() {
|
||||
if (output.status === 'timeout') {
|
||||
showTimeoutMessage();
|
||||
} else if (output.status === 'container_depleted') {
|
||||
showContainerDepletedMessage();
|
||||
showContainerDepletedMessage();
|
||||
} else if (output.stderr) {
|
||||
$.flash.danger({
|
||||
icon: ['fa', 'fa-bug'],
|
||||
@ -988,29 +1004,46 @@ $(function() {
|
||||
});
|
||||
};
|
||||
|
||||
var showWebsocketError = function() {
|
||||
$.flash.danger({
|
||||
text: $('#flash').data('message-failure')
|
||||
});
|
||||
}
|
||||
|
||||
var showWorkspaceTab = function(event) {
|
||||
event.preventDefault();
|
||||
showTab(1);
|
||||
showTab(0);
|
||||
};
|
||||
|
||||
var stopCode = function(event) {
|
||||
event.preventDefault();
|
||||
if ($('#stop').is(':visible')) {
|
||||
var jqxhr = ajax({
|
||||
data: {
|
||||
container_id: $('#stop').data('container').id
|
||||
},
|
||||
url: $('#stop').data('url')
|
||||
});
|
||||
jqxhr.always(function() {
|
||||
hideSpinner();
|
||||
running = false;
|
||||
toggleButtonStates();
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
killWebsocketAndContainer();
|
||||
}
|
||||
};
|
||||
|
||||
var killWebsocketAndContainer = function() {
|
||||
if (websocket.readyState != WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
websocket.send(JSON.stringify({cmd: 'exit'}));
|
||||
websocket.flush();
|
||||
websocket.close();
|
||||
hideSpinner();
|
||||
running = false;
|
||||
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
|
||||
var storeContainerInformation = function(event) {
|
||||
var container_information = JSON.parse(event.data);
|
||||
$('#stop').data('container', container_information);
|
||||
@ -1065,6 +1098,164 @@ $(function() {
|
||||
$('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary());
|
||||
};
|
||||
|
||||
var initWebsocketConnection = function(url) {
|
||||
websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url);
|
||||
websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
|
||||
websocket.onclose = function(evt) { /* expected at some point */ };
|
||||
websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
|
||||
websocket.onerror = function(evt) { showWebsocketError(); };
|
||||
websocket.flush = function() { this.send('\n'); }
|
||||
};
|
||||
|
||||
var initTurtle = function() {
|
||||
// todo guard clause if turtle is not required for the current exercise
|
||||
turtlescreen = new Turtle(websocket, turtlecanvas);
|
||||
if ($('#run').isPresent()) {
|
||||
$('#run').bind('click', hideCanvas);
|
||||
}
|
||||
};
|
||||
|
||||
var initPrompt = function() {
|
||||
if ($('#run').isPresent()) {
|
||||
$('#run').bind('click', hidePrompt);
|
||||
}
|
||||
if ($('#prompt').isPresent()) {
|
||||
$('#prompt').on('keypress', handlePromptKeyPress);
|
||||
$('#prompt-submit').on('click', submitPromptInput);
|
||||
}
|
||||
}
|
||||
|
||||
var executeWebsocketCommand = function(msg) {
|
||||
if ($.inArray(msg.cmd, commands) == -1) {
|
||||
console.log("Unknown command: " + msg.cmd);
|
||||
// skipping unregistered commands is required
|
||||
// as we may receive mirrored response due to internal behaviour
|
||||
return;
|
||||
}
|
||||
switch(msg.cmd) {
|
||||
case 'input':
|
||||
showPrompt();
|
||||
break;
|
||||
case 'write':
|
||||
printWebsocketOutput(msg);
|
||||
break;
|
||||
case 'turtle':
|
||||
showCanvas();
|
||||
handleTurtleCommand(msg);
|
||||
break;
|
||||
case 'turtlebatch':
|
||||
showCanvas();
|
||||
handleTurtlebatchCommand(msg);
|
||||
break;
|
||||
case 'exit':
|
||||
killWebsocketAndContainer();
|
||||
break;
|
||||
case 'status':
|
||||
showStatus(msg)
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var printWebsocketOutput = function(msg) {
|
||||
if (!msg.data) {
|
||||
return;
|
||||
}
|
||||
//msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "<br />");
|
||||
msg.data = msg.data.replace(/(\r)/gm, "\n");
|
||||
var stream = {};
|
||||
stream[msg.stream] = msg.data;
|
||||
printOutput(stream, true, 0);
|
||||
};
|
||||
|
||||
var handleTurtleCommand = function(msg) {
|
||||
if (msg.action in turtlescreen) {
|
||||
result = turtlescreen[msg.action].apply(turtlescreen, msg.args);
|
||||
websocket.send(JSON.stringify({cmd: 'result', 'result': result}));
|
||||
} else {
|
||||
websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action}));
|
||||
}
|
||||
websocket.flush();
|
||||
};
|
||||
|
||||
var handleTurtlebatchCommand = function(msg) {
|
||||
for (i = 0; i < msg.batch.length; i++) {
|
||||
cmd = msg.batch[i];
|
||||
turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]);
|
||||
}
|
||||
};
|
||||
|
||||
var handlePromptKeyPress = function(evt) {
|
||||
if (evt.which === ENTER_KEY_CODE) {
|
||||
submitPromptInput();
|
||||
}
|
||||
}
|
||||
|
||||
var submitPromptInput = function() {
|
||||
var input = $('#prompt-input');
|
||||
var message = input.val();
|
||||
websocket.send(JSON.stringify({cmd: 'result', 'data': message}));
|
||||
websocket.flush();
|
||||
input.val('');
|
||||
hidePrompt();
|
||||
}
|
||||
|
||||
var parseCanvasMessage = function(message, recursive) {
|
||||
var msg;
|
||||
message = message.replace(/^\s+|\s+$/g, "");
|
||||
try {
|
||||
// todo validate json instead of catching
|
||||
msg = JSON.parse(message);
|
||||
} catch (e) {
|
||||
if (!recursive) {
|
||||
return false;
|
||||
}
|
||||
// why does docker sometimes send multiple commands at once?
|
||||
message = message.replace(/^\s+|\s+$/g, "");
|
||||
messages = message.split("\n");
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (!messages[i]) {
|
||||
continue;
|
||||
}
|
||||
parseCanvasMessage(messages[i], false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
executeWebsocketCommand(msg);
|
||||
};
|
||||
|
||||
var showPrompt = function() {
|
||||
if (prompt.isPresent() && prompt.hasClass('hidden')) {
|
||||
prompt.removeClass('hidden');
|
||||
}
|
||||
prompt.focus();
|
||||
}
|
||||
|
||||
var hidePrompt = function() {
|
||||
if (prompt.isPresent() && !prompt.hasClass('hidden')) {
|
||||
prompt.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
var showCanvas = function() {
|
||||
if ($('#turtlediv').isPresent()
|
||||
&& turtlecanvas.hasClass('hidden')) {
|
||||
// initialize two-column layout
|
||||
$('#output-col1').addClass('col-lg-7 col-md-7 two-column');
|
||||
turtlecanvas.removeClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
var hideCanvas = function() {
|
||||
if ($('#turtlediv').isPresent()
|
||||
&& !(turtlecanvas.hasClass('hidden'))) {
|
||||
output = $('#output-col1');
|
||||
if (output.hasClass('two-column')) {
|
||||
output.removeClass('col-lg-7 col-md-7 two-column');
|
||||
}
|
||||
turtlecanvas.addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
var requestComments = function(e) {
|
||||
var user_id = $('#editor').data('user-id')
|
||||
var exercise_id = $('#editor').data('exercise-id')
|
||||
@ -1123,6 +1314,7 @@ $(function() {
|
||||
initializeEventHandlers();
|
||||
initializeFileTree();
|
||||
initializeTooltips();
|
||||
initPrompt();
|
||||
renderScore();
|
||||
showFirstFile();
|
||||
showRequestedTab();
|
||||
|
222
app/assets/javascripts/turtle.js
Normal file
222
app/assets/javascripts/turtle.js
Normal file
@ -0,0 +1,222 @@
|
||||
var output;
|
||||
var editor;
|
||||
var pipeurl;
|
||||
var filename;
|
||||
var pendingChanges = -1;
|
||||
|
||||
function Turtle(pipe, canvas) {
|
||||
var dx, dy, xpos, ypos;
|
||||
this.canvas = canvas; // jQuery object
|
||||
this.items = [];
|
||||
this.canvas.off('click');
|
||||
this.canvas.click(function (e) {
|
||||
if (e.eventPhase !== 2) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
dx = this.width / 2;
|
||||
dy = this.height / 2;
|
||||
if(e.offsetX==undefined)
|
||||
{
|
||||
var offset = canvas.offset();
|
||||
xpos = e.pageX-offset.left;
|
||||
ypos = e.pageY-offset.top;
|
||||
}
|
||||
else
|
||||
{
|
||||
xpos = e.offsetX;
|
||||
ypos = e.offsetY;
|
||||
}
|
||||
pipe.send(JSON.stringify({
|
||||
'cmd': 'canvasevent',
|
||||
'type': '<Button-1>',
|
||||
'x': xpos - dx,
|
||||
'y': ypos - dy
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
Turtle.prototype.update = function () {
|
||||
var i, k, canvas, ctx, dx, dy, item, c, length;
|
||||
canvas = this.canvas[0];
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
length = this.items.length;
|
||||
dx = canvas.width / 2;
|
||||
dy = canvas.height / 2;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
item = this.items[i];
|
||||
c = item.coords;
|
||||
switch (item.type) {
|
||||
case 'line':
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c[0] + dx, c[1] + dy);
|
||||
for (k = 2; k < c.length; k += 2) {
|
||||
ctx.lineTo(c[k] + dx, c[k + 1] + dy);
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.strokeStyle = this.fill;
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
break;
|
||||
case 'polygon':
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c[0] + dx, c[1] + dy);
|
||||
for (k = 2; k < c.length; k += 2) {
|
||||
ctx.lineTo(c[k] + dx, c[k + 1] + dy);
|
||||
}
|
||||
ctx.closePath();
|
||||
if (item.fill !== "") {
|
||||
ctx.fillStyle = item.fill;
|
||||
ctx.strokeStyle = item.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.stroke();
|
||||
break;
|
||||
case 'image':
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Turtle.prototype.get_width = function () {
|
||||
return this.canvas[0].width;
|
||||
}
|
||||
|
||||
Turtle.prototype.get_height = function () {
|
||||
return this.canvas[0].height;
|
||||
}
|
||||
|
||||
Turtle.prototype.delete = function (item) {
|
||||
if (item == 'all') {
|
||||
this.items = [];
|
||||
} else {
|
||||
delete this.items[item];
|
||||
}
|
||||
}
|
||||
|
||||
Turtle.prototype.create_image = function (image) {
|
||||
this.items.push({type:'image',image:image});
|
||||
return this.items.length - 1;
|
||||
}
|
||||
|
||||
Turtle.prototype.create_line = function () {
|
||||
this.items.push({type:'line',
|
||||
fill: '',
|
||||
coords:[0,0,0,0],
|
||||
width:2,
|
||||
capstyle:'round'});
|
||||
return this.items.length - 1;
|
||||
}
|
||||
|
||||
Turtle.prototype.create_polygon = function () {
|
||||
this.items.push({type:'polygon',
|
||||
// fill: "" XXX
|
||||
// outline: "" XXX
|
||||
coords:[0,0,0,0,0,0]
|
||||
});
|
||||
return this.items.length - 1;
|
||||
}
|
||||
|
||||
// XXX might make this varargs as in Tkinter
|
||||
Turtle.prototype.coords = function (item, coords) {
|
||||
if (coords === undefined) {
|
||||
return this.items[item].coords;
|
||||
}
|
||||
this.items[item].coords = coords;
|
||||
}
|
||||
|
||||
Turtle.prototype.itemconfigure = function (item, key, value) {
|
||||
this.items[item][key] = value;
|
||||
}
|
||||
|
||||
// value might be undefined
|
||||
Turtle.prototype.css = function (key, value) {
|
||||
if (value === undefined) {
|
||||
return this.canvas.css(key);
|
||||
} else {
|
||||
// jQuery return value is confusing when the css is set
|
||||
this.canvas.css(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function run(launchmsg) {
|
||||
var i, turtlescreen, msg, result, cmd;
|
||||
$('#assess').empty();
|
||||
|
||||
turtlescreen = new Turtle();
|
||||
|
||||
output = $('#output');
|
||||
output.empty();
|
||||
if (typeof pipeurl === 'undefined') {
|
||||
if (wp_port == '443') {
|
||||
pipeurl = 'wss://'+wp_hostname+'/pipe';
|
||||
} else {
|
||||
pipeurl = 'ws://'+wp_hostname+':'+wp_port+'/pipe';
|
||||
}
|
||||
}
|
||||
saveFile();
|
||||
output.pipe = new WebSocket(pipeurl);
|
||||
output.pipe.onopen = function () {
|
||||
output.pipe.send(JSON.stringify(launchmsg));
|
||||
};
|
||||
output.pipe.onmessage = function (response) {
|
||||
msg = JSON.parse(response.data);
|
||||
if (msg.cmd == 'input') {
|
||||
output.inputelem = $('<input>',{'size':40});
|
||||
submit = $('<input>',{'type':'submit'});
|
||||
submit.click(function (){
|
||||
text = output.inputelem.val();
|
||||
output.input.replaceWith($('<code>', {text:text+'\n'}));
|
||||
output.pipe.send(JSON.stringify({'cmd':'inputresult',
|
||||
'data':text}));
|
||||
});
|
||||
output.inputelem.keydown(function(event){
|
||||
if(event.keyCode == 13){
|
||||
submit.click();
|
||||
}
|
||||
});
|
||||
output.append($('<code>', {text:msg.data}));
|
||||
output.input = $('<span>').append(output.inputelem).append(submit);
|
||||
output.append(output.input);
|
||||
output.inputelem.focus();
|
||||
} else if (msg.cmd == 'stop') {
|
||||
if (launchmsg.cmd == 'runscript') {
|
||||
if (msg.timedout) {
|
||||
output.append('<hr>Dein Programm hat zu lange gerechnet und wurde beendet.');
|
||||
} else {
|
||||
output.append('<hr>Dein Progamm wurde beendet');
|
||||
}
|
||||
}
|
||||
output.pipe.close();
|
||||
} else if (msg.cmd == 'passed') {
|
||||
$('#assess').html("Herzlich Glückwunsch! Dein Programm funktioniert korrekt.");
|
||||
} else if (msg.cmd == 'failed') {
|
||||
$('#assess').html(msg.data);
|
||||
} else if (msg.cmd == 'turtle') {
|
||||
if (msg.action in turtlescreen) {
|
||||
result = turtlescreen[msg.action].apply(turtlescreen, msg.args);
|
||||
output.pipe.send(JSON.stringify({cmd:'result', 'result':result}));
|
||||
} else {
|
||||
output.pipe.send(JSON.stringify({cmd:'exception', exception:'AttributeError',
|
||||
message:msg.action}));
|
||||
}
|
||||
} else if (msg.cmd == 'turtlebatch') {
|
||||
for (i=0; i < msg.batch.length; i += 1) {
|
||||
cmd = msg.batch[i];
|
||||
turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]);
|
||||
}
|
||||
} else {
|
||||
if(msg.stream == 'internal') {
|
||||
output.append('<hr><em>Interner Fehler (bitte melden):</em>\n');
|
||||
}
|
||||
else if (msg.stream == 'stderr') {
|
||||
showConsole();
|
||||
$('#consoleradio').prop('checked', 'checked');
|
||||
}
|
||||
output.append($('<code>',{text:msg.data, 'class':msg.stream}));
|
||||
}
|
||||
};
|
||||
}
|
@ -62,7 +62,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
|
@ -4,6 +4,7 @@ class SubmissionsController < ApplicationController
|
||||
include Lti
|
||||
include SubmissionParameters
|
||||
include SubmissionScoring
|
||||
include Tubesock::Hijack
|
||||
|
||||
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
|
||||
before_action :set_docker_client, only: [:run, :test]
|
||||
@ -70,20 +71,95 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def run
|
||||
with_server_sent_events do |server_sent_event|
|
||||
output = @docker_client.execute_run_command(@submission, params[:filename])
|
||||
|
||||
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({status: output[:status]}, event: 'status')
|
||||
|
||||
unless output[:stderr].nil?
|
||||
if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr])
|
||||
server_sent_event.write(hint, event: 'hint')
|
||||
else
|
||||
store_error(output[:stderr])
|
||||
# TODO reimplement SSEs with websocket commands
|
||||
# with_server_sent_events do |server_sent_event|
|
||||
# output = @docker_client.execute_run_command(@submission, params[:filename])
|
||||
|
||||
# server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout]
|
||||
# server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr]
|
||||
|
||||
# unless output[:stderr].nil?
|
||||
# if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr])
|
||||
# server_sent_event.write(hint, event: 'hint')
|
||||
# else
|
||||
# store_error(output[:stderr])
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
hijack do |tubesock|
|
||||
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||
|
||||
result = @docker_client.execute_run_command(@submission, params[:filename])
|
||||
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})
|
||||
|
||||
if result[:status] == :container_running
|
||||
socket = result[:socket]
|
||||
|
||||
socket.on :message do |event|
|
||||
Rails.logger.info("Docker sending: " + event.data)
|
||||
handle_message(event.data, tubesock)
|
||||
end
|
||||
|
||||
socket.on :close do |event|
|
||||
kill_socket(tubesock)
|
||||
end
|
||||
|
||||
tubesock.onmessage do |data|
|
||||
Rails.logger.info("Client sending: " + data)
|
||||
# Check wether the client send a JSON command and kill container
|
||||
# if the command is 'exit', send it to docker otherwise.
|
||||
begin
|
||||
parsed = JSON.parse(data)
|
||||
if parsed['cmd'] == 'exit'
|
||||
Rails.logger.info("Client killed container.")
|
||||
@docker_client.kill_container(result[:container])
|
||||
else
|
||||
socket.send data
|
||||
end
|
||||
rescue JSON::ParserError
|
||||
socket.send data
|
||||
end
|
||||
end
|
||||
else
|
||||
kill_socket(tubesock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def kill_socket(tubesock)
|
||||
# Hijacked connection needs to be notified correctly
|
||||
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
||||
tubesock.close
|
||||
end
|
||||
|
||||
def handle_message(message, tubesock)
|
||||
# Handle special commands first
|
||||
if (/^exit/.match(message))
|
||||
kill_socket(tubesock)
|
||||
else
|
||||
# Filter out information about run_command, test_command, user or working directory
|
||||
run_command = @submission.execution_environment.run_command
|
||||
test_command = @submission.execution_environment.test_command
|
||||
if !(/root|workspace|#{run_command}|#{test_command}/.match(message))
|
||||
parse_message(message, 'stdout', tubesock)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_message(message, output_stream, socket, recursive = true)
|
||||
begin
|
||||
parsed = JSON.parse(message)
|
||||
socket.send_data message
|
||||
rescue JSON::ParserError => e
|
||||
# Check wether the message contains multiple lines, if true try to parse each line
|
||||
if ((recursive == true) && (message.include? "\n"))
|
||||
for part in message.split("\n")
|
||||
self.parse_message(part,output_stream,socket,false)
|
||||
end
|
||||
else
|
||||
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message}
|
||||
socket.send_data JSON.dump(parsed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,6 +1,6 @@
|
||||
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id
|
||||
.col-sm-3 = render('editor_file_tree', files: @files)
|
||||
#frames.col-sm-9
|
||||
div class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') = render('editor_file_tree', files: @files)
|
||||
div id='frames' class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9')
|
||||
- @files.each do |file|
|
||||
= render('editor_frame', exercise: exercise, file: file)
|
||||
#autosave-label
|
||||
|
@ -23,6 +23,10 @@
|
||||
label
|
||||
= f.check_box(:public)
|
||||
= t('activerecord.attributes.exercise.public')
|
||||
.checkbox
|
||||
label
|
||||
= f.check_box(:hide_file_tree)
|
||||
= t('activerecord.attributes.exercise.hide_file_tree')
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
ul#files.list-unstyled
|
||||
= f.fields_for :files do |files_form|
|
||||
|
@ -13,44 +13,46 @@
|
||||
#development-environment
|
||||
ul.nav.nav-justified.nav-tabs role='tablist'
|
||||
li.active
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#instructions' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1')
|
||||
i.fa.fa-question
|
||||
= t('activerecord.attributes.exercise.instructions')
|
||||
li
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2')
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1')
|
||||
i.fa.fa-code
|
||||
= t('.workspace')
|
||||
li
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#outputInformation' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 3')
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#outputInformation' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2')
|
||||
i.fa.fa-terminal
|
||||
= t('.output')
|
||||
li
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#progress' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 4')
|
||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#progress' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 3')
|
||||
i.fa.fa-line-chart
|
||||
= t('.progress')
|
||||
|
||||
hr
|
||||
|
||||
.tab-content
|
||||
#instructions.tab-pane.active
|
||||
p = render_markdown(@exercise.instructions)
|
||||
br
|
||||
p.text-center
|
||||
a#start.btn.btn-lg.btn-success
|
||||
i.fa.fa-code
|
||||
= t('.start')
|
||||
#workspace.tab-pane = render('editor', exercise: @exercise, files: @files, submission: @submission)
|
||||
#workspace.tab-pane.active = render('editor', exercise: @exercise, files: @files, submission: @submission)
|
||||
#outputInformation.tab-pane data-message-no-output=t('.no_output')
|
||||
#hint
|
||||
.panel.panel-warning
|
||||
.panel-heading = t('.hint')
|
||||
.panel-body
|
||||
#output
|
||||
pre = t('.no_output_yet')
|
||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
|
||||
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
||||
.panel-heading = 'Gain more insights here'
|
||||
.panel-body
|
||||
.row
|
||||
/ #output-col1.col-sm-12
|
||||
#output-col1
|
||||
// todo set to full width if turtle isnt used
|
||||
#prompt.input-group.hidden
|
||||
span.input-group-addon = 'Your input'
|
||||
input#prompt-input.form-control type='text'
|
||||
span.input-group-btn
|
||||
button#prompt-submit.btn.btn-primary type="button" = 'Send'
|
||||
#output
|
||||
pre = t('.no_output_yet')
|
||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
|
||||
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
||||
.panel-heading = 'Gain more insights here'
|
||||
.panel-body
|
||||
#output-col2.col-lg-5.col-md-5
|
||||
#turtlediv
|
||||
// todo what should the canvas default size be?
|
||||
canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin'
|
||||
#progress.tab-pane
|
||||
#results
|
||||
h2 = t('.results')
|
||||
@ -79,4 +81,4 @@
|
||||
- if qa_url
|
||||
#questions-column
|
||||
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
|
||||
= qa_js_tag
|
||||
= qa_js_tag
|
||||
|
@ -14,6 +14,7 @@ h1
|
||||
= row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil)
|
||||
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
|
||||
= row(label: 'exercise.public', value: @exercise.public?)
|
||||
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
|
||||
= row(label: 'exercise.embedding_parameters') do
|
||||
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
||||
|
||||
|
@ -7,7 +7,9 @@ default: &default
|
||||
development:
|
||||
<<: *default
|
||||
host: tcp://192.168.59.104:2376
|
||||
workspace_root: <%= File.join('/', 'shared', Rails.env) %>
|
||||
ws_host: ws://192.168.59.104:2376
|
||||
#workspace_root: <%= File.join('/', 'shared', Rails.env) %>
|
||||
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
|
@ -28,6 +28,7 @@ de:
|
||||
execution_environment: Ausführungsumgebung
|
||||
execution_environment_id: Ausführungsumgebung
|
||||
files: Dateien
|
||||
hide_file_tree: Dateibaum verstecken
|
||||
instructions: Anweisungen
|
||||
maximum_score: Erreichbare Punktzahl
|
||||
public: Öffentlich
|
||||
|
@ -28,6 +28,7 @@ en:
|
||||
execution_environment: Execution Environment
|
||||
execution_environment_id: Execution Environment
|
||||
files: Files
|
||||
hide_file_tree: Hide File Tree
|
||||
instructions: Instructions
|
||||
maximum_score: Maximum Score
|
||||
public: Public
|
||||
|
@ -0,0 +1,5 @@
|
||||
class AddHideFileTreeToExercises < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :exercises, :hide_file_tree, :boolean
|
||||
end
|
||||
end
|
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20150903152727) do
|
||||
ActiveRecord::Schema.define(version: 20150922125415) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -79,6 +79,7 @@ ActiveRecord::Schema.define(version: 20150903152727) do
|
||||
t.string "user_type"
|
||||
t.string "token"
|
||||
t.integer "team_id"
|
||||
t.boolean "hide_file_tree"
|
||||
end
|
||||
|
||||
create_table "external_users", force: true do |t|
|
||||
|
@ -11,6 +11,7 @@ class DockerClient
|
||||
RETRY_COUNT = 2
|
||||
|
||||
attr_reader :container
|
||||
attr_reader :socket
|
||||
|
||||
def self.check_availability!
|
||||
Timeout.timeout(config[:connection_timeout]) { Docker.version }
|
||||
@ -41,7 +42,12 @@ class DockerClient
|
||||
'Memory' => execution_environment.memory_limit.megabytes,
|
||||
'NetworkDisabled' => !execution_environment.network_enabled?,
|
||||
'OpenStdin' => true,
|
||||
'StdinOnce' => true
|
||||
'StdinOnce' => true,
|
||||
# required to expose standard streams over websocket
|
||||
'AttachStdout' => true,
|
||||
'AttachStdin' => true,
|
||||
'AttachStderr' => true,
|
||||
'Tty' => true
|
||||
}
|
||||
end
|
||||
|
||||
@ -52,6 +58,29 @@ class DockerClient
|
||||
}
|
||||
end
|
||||
|
||||
def create_socket(container, stderr=false)
|
||||
# todo factor out query params
|
||||
# todo separate stderr
|
||||
query_params = 'logs=1&stream=1&' + (stderr ? 'stderr=1' : 'stdout=1&stdin=1')
|
||||
|
||||
# Headers are required by Docker
|
||||
headers = {'Origin' => 'http://localhost'}
|
||||
|
||||
socket = Faye::WebSocket::Client.new(DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params, [], :headers => headers)
|
||||
|
||||
socket.on :error do |event|
|
||||
Rails.logger.info "Websocket error: " + event.message
|
||||
end
|
||||
socket.on :close do |event|
|
||||
Rails.logger.info "Websocket closed."
|
||||
end
|
||||
socket.on :open do |event|
|
||||
Rails.logger.info "Websocket created."
|
||||
kill_after_timeout(container)
|
||||
end
|
||||
socket
|
||||
end
|
||||
|
||||
def copy_file_to_workspace(options = {})
|
||||
FileUtils.cp(options[:file].native_file.path, local_file_path(options))
|
||||
end
|
||||
@ -118,14 +147,71 @@ class DockerClient
|
||||
#(tries += 1) <= RETRY_COUNT ? retry : raise(error)
|
||||
end
|
||||
|
||||
[:run, :test].each do |cause|
|
||||
define_method("execute_#{cause}_command") do |submission, filename, &block|
|
||||
command = submission.execution_environment.send(:"#{cause}_command") % command_substitutions(filename)
|
||||
create_workspace_files = proc { create_workspace_files(container, submission) }
|
||||
execute_command(command, create_workspace_files, block)
|
||||
def execute_websocket_command(command, before_execution_block, output_consuming_block)
|
||||
@container = DockerContainerPool.get_container(@execution_environment)
|
||||
if @container
|
||||
before_execution_block.try(:call)
|
||||
# todo catch exception if socket could not be created
|
||||
@socket ||= create_socket(@container)
|
||||
# Newline required to flush
|
||||
@socket.send command + "\n"
|
||||
{status: :container_running, socket: @socket, container: @container}
|
||||
else
|
||||
{status: :container_depleted}
|
||||
end
|
||||
end
|
||||
|
||||
def kill_after_timeout(container)
|
||||
"""
|
||||
We need to start a second thread to kill the websocket connection,
|
||||
as it is impossible to determine whether further input is requested.
|
||||
"""
|
||||
Thread.new do
|
||||
timeout = @execution_environment.permitted_execution_time.to_i # seconds
|
||||
sleep(timeout)
|
||||
Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.")
|
||||
kill_container(container)
|
||||
end
|
||||
end
|
||||
|
||||
def kill_container(container)
|
||||
"""
|
||||
Please note that we cannot properly recycle containers when using
|
||||
websockets because it is impossible to determine whether a program
|
||||
is still running.
|
||||
"""
|
||||
# remove container from pool, then destroy it
|
||||
(DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) :
|
||||
|
||||
#destroy container
|
||||
self.class.destroy_container(container)
|
||||
|
||||
# if we recylce containers, we start a fresh one
|
||||
if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS)
|
||||
# create new container and add it to @all_containers and @containers.
|
||||
container = self.class.create_container(@execution_environment)
|
||||
DockerContainerPool.add_to_all_containers(container, @execution_environment)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_run_command(submission, filename, &block)
|
||||
"""
|
||||
Run commands by attaching a websocket to Docker.
|
||||
"""
|
||||
command = submission.execution_environment.send(:"run_command") % command_substitutions(filename)
|
||||
create_workspace_files = proc { create_workspace_files(container, submission) }
|
||||
execute_websocket_command(command, create_workspace_files, block)
|
||||
end
|
||||
|
||||
def execute_test_command(submission, filename, &block)
|
||||
"""
|
||||
Stick to existing Docker API with exec command.
|
||||
"""
|
||||
command = submission.execution_environment.send(:"test_command") % command_substitutions(filename)
|
||||
create_workspace_files = proc { create_workspace_files(container, submission) }
|
||||
execute_command(command, create_workspace_files, block)
|
||||
end
|
||||
|
||||
def self.find_image_by_tag(tag)
|
||||
Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) }
|
||||
end
|
||||
|
@ -1,5 +1,5 @@
|
||||
class PyUnitAdapter < TestingFrameworkAdapter
|
||||
COUNT_REGEXP = /Ran (\d+) tests/
|
||||
COUNT_REGEXP = /Ran (\d+) test/
|
||||
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/
|
||||
|
||||
def self.framework_name
|
||||
|
12
webpython/Dockerfile
Normal file
12
webpython/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM ubuntu:14.04
|
||||
MAINTAINER "Martin v. Löwis"
|
||||
RUN locale-gen en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV PYTHONPATH $PYTHONPATH:/usr/lib/python3.4:/workspace
|
||||
ENV PATH $PATH:/usr/lib/python3.4
|
||||
ADD assess.py /usr/lib/python3.4/assess.py
|
||||
ADD webpython.py /usr/lib/python3.4/webpython.py
|
||||
RUN rm /usr/lib/python3.4/turtle.py
|
||||
ADD turtle.py /usr/lib/python3.4/turtle.py
|
||||
RUN adduser --disabled-password --gecos Python python
|
||||
USER python
|
58
webpython/Makefile
Normal file
58
webpython/Makefile
Normal file
@ -0,0 +1,58 @@
|
||||
CODEMIRROR=html/js/codemirror.js
|
||||
|
||||
all: logs
|
||||
|
||||
logs:
|
||||
mkdir logs
|
||||
|
||||
# These are put into source control
|
||||
generated: $(CODEMIRROR)
|
||||
|
||||
|
||||
$(CODEMIRROR): CodeMirror/lib/codemirror.js CodeMirror/mode/python/python.js
|
||||
(cd CodeMirror; bin/compress codemirror python) > $@
|
||||
|
||||
run: all kill
|
||||
twistd -l logs/webpython.log -y webpython.tac
|
||||
|
||||
runpy: killpy
|
||||
twistd --pidfile pylaunch.pid -l logs/pylaunch.log -y pylaunch.tac
|
||||
|
||||
manager: all killmanager
|
||||
twistd --pidfile manager.pid -l logs/manager.log -y manager.tac
|
||||
|
||||
kill:
|
||||
if [ -f twistd.pid ];\
|
||||
then\
|
||||
kill `cat twistd.pid`;\
|
||||
fi
|
||||
|
||||
killpy:
|
||||
if [ -f pylaunch.pid ];\
|
||||
then\
|
||||
kill `cat pylaunch.pid`;\
|
||||
fi
|
||||
|
||||
killmanager:
|
||||
if [ -f manager.pid ];\
|
||||
then\
|
||||
kill `cat manager.pid`;\
|
||||
fi
|
||||
|
||||
docker:
|
||||
docker.io build --rm -t webpython .
|
||||
|
||||
update:
|
||||
git pull
|
||||
make docker
|
||||
service webpython-worker restart
|
||||
|
||||
rmexited:
|
||||
docker.io ps -a|grep 'Exit '|awk '{print $$1;}'|xargs docker.io rm
|
||||
|
||||
rmi:
|
||||
docker.io rmi $$(docker.io images | grep "^<none>" | awk '{print $$3;}')
|
||||
|
||||
killold:
|
||||
-docker.io ps | egrep 'hours? ago' |awk '{print $$1}'|xargs docker.io kill
|
||||
-killall -o 30m -9 python3
|
47
webpython/README.md
Normal file
47
webpython/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
Local setup
|
||||
===========
|
||||
|
||||
1. `git checkout webpython-hybrid`
|
||||
2. Make sure to install all dependencies and migrations:
|
||||
|
||||
rake db:migrate
|
||||
bundle install
|
||||
|
||||
3. Create a new docker image containing the Turtle library and the i/o wrapper:
|
||||
|
||||
cd webpython
|
||||
docker build -t IMAGE_NAME .
|
||||
|
||||
4. Configure your Docker host at `config/docker.yml.erb`. Make sure to add a websocket host, for example like this (this is probably different for you):
|
||||
|
||||
host: tcp://localhost:2375
|
||||
ws_host: ws://localhost:2375
|
||||
|
||||
5. Run the CodeOcean server with `rails s -p 3333`
|
||||
|
||||
6. Login with admin@example.org (pw: admin) and create a new execution environment picking the newly created Docker image from the dropdown. Set the initial command to:
|
||||
|
||||
cd /usr/lib/python3.4 && python3 webpython.py
|
||||
|
||||
7. Create a new exercise for the newly created execution environment with an arbritrary main file.
|
||||
8. Implement the exercise. The code below can be used as an example to see the canvas and I/O in action:
|
||||
|
||||
import turtle
|
||||
wn = turtle.Screen()
|
||||
alex = turtle.Turtle()
|
||||
|
||||
# i/o test
|
||||
print("hello!")
|
||||
print("please enter your name")
|
||||
name = input()
|
||||
print("your name is", name)
|
||||
|
||||
# canvas test
|
||||
alex.forward(50)
|
||||
alex.right(90)
|
||||
alex.forward(30)
|
||||
alex.right(90)
|
||||
alex.forward(30)
|
||||
|
||||
wn.mainloop()
|
||||
|
234
webpython/assess.py
Normal file
234
webpython/assess.py
Normal file
@ -0,0 +1,234 @@
|
||||
import webpython, contextlib, io, json, sys, turtle, ast
|
||||
from webpython import shell
|
||||
|
||||
turtle_operations = []
|
||||
bindings = {}
|
||||
|
||||
class RecordingPen:
|
||||
_pen = None
|
||||
_screen = None
|
||||
def __init__(self):
|
||||
self.operations = turtle_operations
|
||||
self._pos = (0,0)
|
||||
turtle_operations.append(('__init__',()))
|
||||
|
||||
def reset(self):
|
||||
turtle_operations.clear()
|
||||
|
||||
def onclick(self, fun, btn=1, add=None):
|
||||
self.operations.append(('onclick', (fun,)))
|
||||
def eventfun(event):
|
||||
fun(event.x, event.y)
|
||||
bindings['<Button-1>'] = eventfun
|
||||
|
||||
def goto(self, x, y):
|
||||
self._pos = (x,y)
|
||||
self.operations.append(('goto', (x,y)))
|
||||
|
||||
def pos(self):
|
||||
self.operations.append(('pos', ()))
|
||||
return self._pos
|
||||
|
||||
def __getattr__(self, method):
|
||||
def func(*args):
|
||||
self.operations.append((method, args))
|
||||
return func
|
||||
|
||||
class FakeCanvas(turtle.WebCanvas):
|
||||
def flushbatch(self):
|
||||
pass
|
||||
|
||||
def get_width(self):
|
||||
return 400
|
||||
|
||||
def get_height(self):
|
||||
return 400
|
||||
|
||||
def delete(self, item):
|
||||
pass
|
||||
|
||||
def css(self, key, value):
|
||||
pass
|
||||
|
||||
fake_events = []
|
||||
def mainloop():
|
||||
while fake_events:
|
||||
e = turtle.Event(fake_events.pop(0))
|
||||
if e.type in bindings:
|
||||
bindings[e.type](e)
|
||||
|
||||
turtle.Turtle = RecordingPen
|
||||
turtle.WebCanvas = FakeCanvas
|
||||
pen = turtle._getpen()
|
||||
turtle.mainloop = mainloop
|
||||
|
||||
def filter_operations(name):
|
||||
return [o for o in turtle_operations if o[0] == name]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def capture():
|
||||
global captured_out
|
||||
import sys
|
||||
oldout,olderr = sys.stdout, sys.stderr
|
||||
try:
|
||||
out=[io.StringIO(), io.StringIO()]
|
||||
captured_out = out
|
||||
sys.stdout,sys.stderr = out
|
||||
yield out
|
||||
finally:
|
||||
sys.stdout,sys.stderr = oldout, olderr
|
||||
out[0] = out[0].getvalue()
|
||||
out[1] = out[1].getvalue()
|
||||
|
||||
def get_source():
|
||||
message = json.loads(sys.argv[1])
|
||||
return message['data']
|
||||
|
||||
def get_ast():
|
||||
s = get_source()
|
||||
return ast.parse(s, "programm.py", "exec")
|
||||
|
||||
def has_bare_except():
|
||||
for node in ast.walk(get_ast()):
|
||||
if isinstance(node, ast.ExceptHandler):
|
||||
if node.type is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def runcaptured(prefix='', tracing=None, variables=None, source=''):
|
||||
#message = json.loads(sys.argv[1])
|
||||
#source = prefix + message['data']
|
||||
with open("programm.py", "w", encoding='utf-8') as f:
|
||||
f.write(source)
|
||||
c = compile(source, "programm.py", 'exec')
|
||||
with capture() as out, trace(tracing):
|
||||
if variables is None:
|
||||
variables = {}
|
||||
exec(c, variables)
|
||||
return source, out[0], out[1], variables
|
||||
|
||||
def runfunc(func, *args, tracing=None):
|
||||
with capture() as out, trace(tracing):
|
||||
res = func(*args)
|
||||
return out[0], out[1], res
|
||||
|
||||
def passed():
|
||||
msg_in = json.loads(sys.argv[1])
|
||||
msg_out = {'cmd':'passed'}
|
||||
msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url']
|
||||
msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid']
|
||||
webpython.shell.sendpickle(msg_out)
|
||||
|
||||
def failed(msg):
|
||||
msg_in = json.loads(sys.argv[1])
|
||||
msg_out = {'cmd':'failed', 'data':'Dein Programm ist leider falsch:\n'+msg}
|
||||
msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url']
|
||||
msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid']
|
||||
webpython.shell.sendpickle(msg_out)
|
||||
|
||||
def modified(variables, name, val):
|
||||
if variables.get(name) != val:
|
||||
msg_in = json.loads(sys.argv[1])
|
||||
msg_out = {'cmd':'failed',
|
||||
'data':('Bitte lösche Deine Zuweisung der Variable %s, '+
|
||||
'damit wir Dein Programm überprüfen können.') % name}
|
||||
msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url']
|
||||
msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid']
|
||||
webpython.shell.sendpickle(msg_out)
|
||||
return True
|
||||
return False
|
||||
|
||||
undefined = object()
|
||||
def getvar(variables, name):
|
||||
try:
|
||||
return variables['name']
|
||||
except KeyError:
|
||||
name = name.lower()
|
||||
for k,v in variables.items():
|
||||
if k.lower() == name:
|
||||
return v
|
||||
return undefined
|
||||
|
||||
def _match(n1, n2):
|
||||
if n1 == n2:
|
||||
return True
|
||||
if n1 is None or n2 is None:
|
||||
return False
|
||||
return n1.lower() == n2.lower()
|
||||
|
||||
class Call:
|
||||
def __init__(self, name, args):
|
||||
self.name = name
|
||||
self.args = args
|
||||
self.calls = []
|
||||
self.current = None
|
||||
|
||||
def findcall(self, f):
|
||||
if _match(self.name, f):
|
||||
return self
|
||||
for c in self.calls:
|
||||
r = c.findcall(f)
|
||||
if r:
|
||||
return r
|
||||
return None
|
||||
|
||||
def calling(self, caller, callee):
|
||||
if _match(self.name, caller):
|
||||
for c in self.calls:
|
||||
if _match(c.name, callee):
|
||||
return True
|
||||
for c in self.calls:
|
||||
if c.calling(caller, callee):
|
||||
return True
|
||||
return False
|
||||
|
||||
def countcalls(self, caller, callee):
|
||||
calls = 0
|
||||
if _match(self.name, caller):
|
||||
for c in self.calls:
|
||||
if _match(c.name, callee):
|
||||
calls += 1
|
||||
return calls
|
||||
for c in self.calls:
|
||||
r = c.countcalls(caller, callee)
|
||||
if r > 0:
|
||||
return r
|
||||
return 0
|
||||
|
||||
class Tracing(Call):
|
||||
def __init__(self):
|
||||
Call.__init__(self, None, None)
|
||||
|
||||
def trace(self, frame, event, arg):
|
||||
if event == 'call':
|
||||
c = Call(frame.f_code.co_name, frame.f_locals.copy())
|
||||
cur = self
|
||||
while cur.current:
|
||||
cur = cur.current
|
||||
cur.calls.append(c)
|
||||
cur.current = c
|
||||
return self.trace
|
||||
elif event in ('return', 'exception'):
|
||||
cur = self
|
||||
if not cur.current:
|
||||
# XXX return without call? happens when invocation of top function fails
|
||||
return
|
||||
while cur.current.current:
|
||||
cur = cur.current
|
||||
cur.current = None
|
||||
|
||||
def start(self):
|
||||
sys.settrace(self.trace)
|
||||
|
||||
def stop(self):
|
||||
sys.settrace(None)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def trace(t):
|
||||
try:
|
||||
if t:
|
||||
t.start()
|
||||
yield
|
||||
finally:
|
||||
if t:
|
||||
t.stop()
|
4078
webpython/turtle.py
Normal file
4078
webpython/turtle.py
Normal file
File diff suppressed because it is too large
Load Diff
178
webpython/webpython.py
Normal file
178
webpython/webpython.py
Normal file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/python3
|
||||
# Main interpreter entry for webpython
|
||||
import io, select, sys, os, threading, code
|
||||
import pickle, struct, builtins, json
|
||||
#, ressource
|
||||
from queue import Queue
|
||||
from argparse import ArgumentParser
|
||||
|
||||
# hard limit to 64M
|
||||
#try:
|
||||
# resource.setrlimit(resource.RLIMIT_AS, (1<<26, 1<<26))
|
||||
#except ValueError:
|
||||
# tried to raise it
|
||||
# pass
|
||||
|
||||
# output limit 16MiB
|
||||
output_capacity = 16*1024*1024
|
||||
# adapted from IDLE (PyShell.py)
|
||||
class PseudoFile(io.TextIOBase):
|
||||
|
||||
def __init__(self, shell, name):
|
||||
self.shell = shell
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return "UTF-8"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '<%s>' % self._name
|
||||
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
class PseudoInputFile(PseudoFile):
|
||||
|
||||
def __init__(self, shell, name):
|
||||
PseudoFile.__init__(self, shell, name)
|
||||
self._line_buffer = ''
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def read(self, size=-1):
|
||||
if self.closed:
|
||||
raise ValueError("read from closed file")
|
||||
if size is None:
|
||||
size = -1
|
||||
elif not isinstance(size, int):
|
||||
raise TypeError('must be int, not ' + type(size).__name__)
|
||||
result = self._line_buffer
|
||||
self._line_buffer = ''
|
||||
if size < 0:
|
||||
while True:
|
||||
line = self.shell.readline()
|
||||
if not line: break
|
||||
result += line
|
||||
else:
|
||||
while len(result) < size:
|
||||
line = self.shell.readline()
|
||||
if not line: break
|
||||
result += line
|
||||
self._line_buffer = result[size:]
|
||||
result = result[:size]
|
||||
return result
|
||||
|
||||
def readline(self, size=-1):
|
||||
if self.closed:
|
||||
raise ValueError("read from closed file")
|
||||
if size is None:
|
||||
size = -1
|
||||
elif not isinstance(size, int):
|
||||
raise TypeError('must be int, not ' + type(size).__name__)
|
||||
line = self._line_buffer or self.shell.readline()
|
||||
if size < 0:
|
||||
size = len(line)
|
||||
self._line_buffer = line[size:]
|
||||
return line[:size]
|
||||
|
||||
def close(self):
|
||||
self.shell.close()
|
||||
|
||||
class PseudoOutputFile(PseudoFile):
|
||||
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
def write(self, s):
|
||||
if self.closed:
|
||||
raise ValueError("write to closed file")
|
||||
if not isinstance(s, str):
|
||||
raise TypeError('must be str, not ' + type(s).__name__)
|
||||
return self.shell.write(s, self._name)
|
||||
|
||||
# RPC proxy
|
||||
orig_stdin = sys.stdin
|
||||
orig_stdout = sys.stdout
|
||||
orig_stderr = sys.stderr
|
||||
class Shell:
|
||||
def __init__(self):
|
||||
self.stdin = io.FileIO(0)
|
||||
self.buf = b''
|
||||
self.canvas = []
|
||||
self.messages = []
|
||||
self.capacity = output_capacity
|
||||
|
||||
# PseudoFile interaction
|
||||
#def readline(self):
|
||||
# self.sendpickle({'cmd':'readline',
|
||||
# 'stream':'stdin',
|
||||
# })
|
||||
# return self.inputq.get()
|
||||
|
||||
def write(self, data, name):
|
||||
self.sendpickle({'cmd':'write',
|
||||
'stream':name,
|
||||
'data':data
|
||||
})
|
||||
|
||||
def input(self, prompt=''):
|
||||
self.sendpickle({'cmd':'input',
|
||||
'stream':'stdin',
|
||||
'data':prompt})
|
||||
result = self.receivemsg()
|
||||
return result['data']
|
||||
|
||||
# internal
|
||||
def sendpickle(self, data):
|
||||
data = json.dumps(data) + "\n\r"
|
||||
self.capacity -= len(data)
|
||||
if self.capacity < 0:
|
||||
data = json.dumps({'cmd':'stop',
|
||||
'timedout':True}, 2)
|
||||
orig_stdout.write(data)
|
||||
raise SystemExit
|
||||
orig_stdout.write(data)
|
||||
|
||||
def receivepickle(self):
|
||||
msg = json.loads(orig_stdin.readline())
|
||||
if msg['cmd'] == 'canvasevent':
|
||||
self.canvas.append(msg)
|
||||
else:
|
||||
self.messages.append(msg)
|
||||
|
||||
def receivemsg(self):
|
||||
while not self.messages:
|
||||
self.receivepickle()
|
||||
return self.messages.pop()
|
||||
|
||||
def receivecanvas(self):
|
||||
while not self.canvas:
|
||||
self.receivepickle()
|
||||
return self.canvas.pop(0)
|
||||
|
||||
# Hide 0/1 from sys
|
||||
shell = Shell()
|
||||
sys.__stdin__ = sys.stdin = PseudoInputFile(shell, 'stdin')
|
||||
sys.__stdout__ = sys.stdout = PseudoOutputFile(shell, 'stdout')
|
||||
#sys.__stderr__ = sys.stderr = PseudoOutputFile(shell, 'stderr')
|
||||
builtins.input = shell.input
|
||||
|
||||
#iothread = threading.Thread(target=shell.run)
|
||||
#iothread.start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser(description='A python interpreter that generates json commands based on the standard I/O streams.')
|
||||
parser.add_argument('-f', '--filename', type=str, required=True, default='exercise.py', help='Python file to be interpreted.')
|
||||
args = parser.parse_args()
|
||||
|
||||
filepath = os.path.join("/", "workspace", args.filename)
|
||||
with open(filepath, "r", encoding='utf-8') as f:
|
||||
script = f.read()
|
||||
c = compile(script, args.filename, 'exec')
|
||||
exec(c, {})
|
||||
|
||||
# work-around for docker not terminating properly
|
||||
shell.sendpickle({'cmd':'exit'})
|
Reference in New Issue
Block a user