1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@
|
|||||||
/tmp
|
/tmp
|
||||||
/vagrant/
|
/vagrant/
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
|
/.idea
|
2
Gemfile
2
Gemfile
@ -33,6 +33,8 @@ gem 'thread_safe'
|
|||||||
gem 'turbolinks'
|
gem 'turbolinks'
|
||||||
gem 'uglifier', '>= 1.3.0'
|
gem 'uglifier', '>= 1.3.0'
|
||||||
gem 'will_paginate', '~> 3.0'
|
gem 'will_paginate', '~> 3.0'
|
||||||
|
gem 'tubesock'
|
||||||
|
gem 'faye-websocket'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'better_errors', platform: :ruby
|
gem 'better_errors', platform: :ruby
|
||||||
|
16
Gemfile.lock
16
Gemfile.lock
@ -50,8 +50,6 @@ GEM
|
|||||||
bootstrap-will_paginate (0.0.10)
|
bootstrap-will_paginate (0.0.10)
|
||||||
will_paginate
|
will_paginate
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
byebug (4.0.5)
|
|
||||||
columnize (= 0.9.0)
|
|
||||||
capistrano (3.3.5)
|
capistrano (3.3.5)
|
||||||
capistrano-stats (~> 1.1.0)
|
capistrano-stats (~> 1.1.0)
|
||||||
i18n
|
i18n
|
||||||
@ -96,7 +94,6 @@ GEM
|
|||||||
execjs
|
execjs
|
||||||
coffee-script-source (1.9.1)
|
coffee-script-source (1.9.1)
|
||||||
colorize (0.7.7)
|
colorize (0.7.7)
|
||||||
columnize (0.9.0)
|
|
||||||
concurrent-ruby (0.8.0)
|
concurrent-ruby (0.8.0)
|
||||||
ref (~> 1.0, >= 1.0.5)
|
ref (~> 1.0, >= 1.0.5)
|
||||||
concurrent-ruby (0.8.0-java)
|
concurrent-ruby (0.8.0-java)
|
||||||
@ -110,6 +107,7 @@ GEM
|
|||||||
excon (>= 0.38.0)
|
excon (>= 0.38.0)
|
||||||
json
|
json
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
|
eventmachine (1.0.8)
|
||||||
excon (0.45.2)
|
excon (0.45.2)
|
||||||
execjs (2.5.2)
|
execjs (2.5.2)
|
||||||
factory_girl (4.5.0)
|
factory_girl (4.5.0)
|
||||||
@ -119,6 +117,9 @@ GEM
|
|||||||
railties (>= 3.0.0)
|
railties (>= 3.0.0)
|
||||||
faraday (0.9.1)
|
faraday (0.9.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
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)
|
||||||
ffi (1.9.8-java)
|
ffi (1.9.8-java)
|
||||||
forgery (0.6.0)
|
forgery (0.6.0)
|
||||||
@ -302,6 +303,9 @@ GEM
|
|||||||
thread_safe (0.3.5)
|
thread_safe (0.3.5)
|
||||||
thread_safe (0.3.5-java)
|
thread_safe (0.3.5-java)
|
||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
|
tubesock (0.2.5)
|
||||||
|
rack (>= 1.5.0)
|
||||||
|
websocket (>= 1.1.0)
|
||||||
turbolinks (2.5.3)
|
turbolinks (2.5.3)
|
||||||
coffee-rails
|
coffee-rails
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
@ -315,6 +319,9 @@ GEM
|
|||||||
railties (>= 4.0)
|
railties (>= 4.0)
|
||||||
sprockets-rails (>= 2.0, < 4.0)
|
sprockets-rails (>= 2.0, < 4.0)
|
||||||
websocket (1.2.1)
|
websocket (1.2.1)
|
||||||
|
websocket-driver (0.6.2)
|
||||||
|
websocket-extensions (>= 0.1.0)
|
||||||
|
websocket-extensions (0.1.2)
|
||||||
will_paginate (3.0.7)
|
will_paginate (3.0.7)
|
||||||
xpath (2.0.0)
|
xpath (2.0.0)
|
||||||
nokogiri (~> 1.3)
|
nokogiri (~> 1.3)
|
||||||
@ -330,7 +337,6 @@ DEPENDENCIES
|
|||||||
better_errors
|
better_errors
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
bootstrap-will_paginate
|
bootstrap-will_paginate
|
||||||
byebug
|
|
||||||
capistrano (~> 3.3.0)
|
capistrano (~> 3.3.0)
|
||||||
capistrano-rails
|
capistrano-rails
|
||||||
capistrano-rvm
|
capistrano-rvm
|
||||||
@ -345,6 +351,7 @@ DEPENDENCIES
|
|||||||
database_cleaner
|
database_cleaner
|
||||||
docker-api (~> 1.21.1)
|
docker-api (~> 1.21.1)
|
||||||
factory_girl_rails (~> 4.0)
|
factory_girl_rails (~> 4.0)
|
||||||
|
faye-websocket
|
||||||
forgery
|
forgery
|
||||||
highline
|
highline
|
||||||
ims-lti
|
ims-lti
|
||||||
@ -375,6 +382,7 @@ DEPENDENCIES
|
|||||||
sorcery
|
sorcery
|
||||||
spring
|
spring
|
||||||
thread_safe
|
thread_safe
|
||||||
|
tubesock
|
||||||
turbolinks
|
turbolinks
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
web-console (~> 2.0)
|
web-console (~> 2.0)
|
||||||
|
@ -11,6 +11,7 @@ $(function() {
|
|||||||
var FILENAME_URL_PLACEHOLDER = '{filename}';
|
var FILENAME_URL_PLACEHOLDER = '{filename}';
|
||||||
var SUCCESSFULL_PERCENTAGE = 90;
|
var SUCCESSFULL_PERCENTAGE = 90;
|
||||||
var THEME = 'ace/theme/textmate';
|
var THEME = 'ace/theme/textmate';
|
||||||
|
var REMEMBER_TAB = false;
|
||||||
var AUTOSAVE_INTERVAL = 15 * 1000;
|
var AUTOSAVE_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
var editors = [];
|
var editors = [];
|
||||||
@ -20,6 +21,16 @@ $(function() {
|
|||||||
var qa_api = undefined;
|
var qa_api = undefined;
|
||||||
var output_mode_is_streaming = true;
|
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 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) {
|
var ajax = function(options) {
|
||||||
@ -49,7 +60,7 @@ $(function() {
|
|||||||
|
|
||||||
if (event.type === 'error' || JSON.parse(event.data).code !== 200) {
|
if (event.type === 'error' || JSON.parse(event.data).code !== 200) {
|
||||||
ajaxError();
|
ajaxError();
|
||||||
showTab(1);
|
showTab(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -181,14 +192,15 @@ $(function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var evaluateCodeWithStreamedResponse = function(url, callback) {
|
var evaluateCodeWithStreamedResponse = function(url, callback) {
|
||||||
var event_source = new EventSource(url);
|
initWebsocketConnection(url);
|
||||||
|
|
||||||
event_source.addEventListener('close', closeEventSource);
|
// TODO only init turtle when required
|
||||||
event_source.addEventListener('error', closeEventSource);
|
initTurtle();
|
||||||
|
|
||||||
|
// TODO reimplement via websocket messsages
|
||||||
|
/*var event_source = new EventSource(url);
|
||||||
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);
|
||||||
@ -197,11 +209,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) {
|
||||||
@ -255,13 +263,11 @@ $(function() {
|
|||||||
|
|
||||||
var handleKeyPress = function(event) {
|
var handleKeyPress = function(event) {
|
||||||
if (event.which === ALT_1_KEY_CODE) {
|
if (event.which === ALT_1_KEY_CODE) {
|
||||||
showTab(0);
|
|
||||||
} else if (event.which === ALT_2_KEY_CODE) {
|
|
||||||
showWorkspaceTab(event);
|
showWorkspaceTab(event);
|
||||||
|
} else if (event.which === ALT_2_KEY_CODE) {
|
||||||
|
showTab(1);
|
||||||
} else if (event.which === ALT_3_KEY_CODE) {
|
} else if (event.which === ALT_3_KEY_CODE) {
|
||||||
showTab(2);
|
showTab(2);
|
||||||
} else if (event.which === ALT_4_KEY_CODE) {
|
|
||||||
showTab(3);
|
|
||||||
} else if (event.which === ALT_R_KEY_CODE) {
|
} else if (event.which === ALT_R_KEY_CODE) {
|
||||||
$('#run').trigger('click');
|
$('#run').trigger('click');
|
||||||
} else if (event.which === ALT_S_KEY_CODE) {
|
} else if (event.which === ALT_S_KEY_CODE) {
|
||||||
@ -304,7 +310,7 @@ $(function() {
|
|||||||
}, 0).toFixed(2);
|
}, 0).toFixed(2);
|
||||||
$('#score').data('score', score);
|
$('#score').data('score', score);
|
||||||
renderScore();
|
renderScore();
|
||||||
showTab(3);
|
showTab(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
var stderrOutput = '';
|
var stderrOutput = '';
|
||||||
@ -357,7 +363,7 @@ $(function() {
|
|||||||
qa_api.executeCommand('syncOutput', [response]);
|
qa_api.executeCommand('syncOutput', [response]);
|
||||||
}
|
}
|
||||||
showStatus(response[0]);
|
showStatus(response[0]);
|
||||||
showTab(2);
|
showTab(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
var hideSpinner = function() {
|
var hideSpinner = function() {
|
||||||
@ -631,11 +637,11 @@ $(function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var initializeWorkspaceButtons = function() {
|
var initializeWorkspaceButtons = function() {
|
||||||
$('#assess').on('click', scoreCode);
|
$('#assess').on('click', scoreCode); // todo
|
||||||
$('#dropdown-render, #render').on('click', renderCode);
|
$('#dropdown-render, #render').on('click', renderCode);
|
||||||
$('#dropdown-run, #run').on('click', runCode);
|
$('#dropdown-run, #run').on('click', runCode);
|
||||||
$('#dropdown-stop, #stop').on('click', stopCode);
|
$('#dropdown-stop, #stop').on('click', stopCode); // todo
|
||||||
$('#dropdown-test, #test').on('click', testCode);
|
$('#dropdown-test, #test').on('click', testCode); // todo
|
||||||
$('#save').on('click', saveCode);
|
$('#save').on('click', saveCode);
|
||||||
$('#start-over').on('click', confirmReset);
|
$('#start-over').on('click', confirmReset);
|
||||||
};
|
};
|
||||||
@ -676,6 +682,7 @@ $(function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var isBrowserSupported = function() {
|
var isBrowserSupported = function() {
|
||||||
|
// todo event streams are no longer required with websockets
|
||||||
return window.EventSource !== undefined;
|
return window.EventSource !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -703,12 +710,16 @@ $(function() {
|
|||||||
chunkBuffer.push(output);
|
chunkBuffer.push(output);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
resetOutputTab();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var resetOutputTab = function() {
|
||||||
clearOutput();
|
clearOutput();
|
||||||
$('#hint').fadeOut();
|
$('#hint').fadeOut();
|
||||||
$('#flowrHint').fadeOut();
|
$('#flowrHint').fadeOut();
|
||||||
showTab(2);
|
showTab(1);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
var printOutput = function(output, colorize, index) {
|
var printOutput = function(output, colorize, index) {
|
||||||
var element = findOrCreateOutputElement(index);
|
var element = findOrCreateOutputElement(index);
|
||||||
@ -808,7 +819,7 @@ $(function() {
|
|||||||
stderr: message
|
stderr: message
|
||||||
}, true, 0);
|
}, true, 0);
|
||||||
sendError(message, response.id);
|
sendError(message, response.id);
|
||||||
showTab(2);
|
showTab(1);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -931,16 +942,21 @@ $(function() {
|
|||||||
|
|
||||||
var showOutput = function(event) {
|
var showOutput = function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
showTab(2);
|
showTab(1);
|
||||||
$('#output').scrollTo($(this).attr('href'));
|
$('#output').scrollTo($(this).attr('href'));
|
||||||
};
|
};
|
||||||
|
|
||||||
var showRequestedTab = function() {
|
var showRequestedTab = function() {
|
||||||
var regexp = /tab=(\d+)/;
|
if(REMEMBER_TAB){
|
||||||
if (regexp.test(window.location.search)) {
|
var regexp = /tab=(\d+)/;
|
||||||
var index = regexp.exec(window.location.search)[1] - 1;
|
if (regexp.test(window.location.search)) {
|
||||||
|
var index = regexp.exec(window.location.search)[1] - 1;
|
||||||
|
} else {
|
||||||
|
var index = localStorage.tab;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var index = localStorage.tab;
|
// else start with first tab.
|
||||||
|
var index = 0;
|
||||||
}
|
}
|
||||||
showTab(index);
|
showTab(index);
|
||||||
};
|
};
|
||||||
@ -954,7 +970,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'],
|
||||||
@ -988,29 +1004,46 @@ $(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(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
var stopCode = function(event) {
|
var stopCode = function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if ($('#stop').is(':visible')) {
|
if ($('#stop').is(':visible')) {
|
||||||
var jqxhr = ajax({
|
killWebsocketAndContainer();
|
||||||
data: {
|
|
||||||
container_id: $('#stop').data('container').id
|
|
||||||
},
|
|
||||||
url: $('#stop').data('url')
|
|
||||||
});
|
|
||||||
jqxhr.always(function() {
|
|
||||||
hideSpinner();
|
|
||||||
running = false;
|
|
||||||
toggleButtonStates();
|
|
||||||
});
|
|
||||||
jqxhr.fail(ajaxError);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 storeContainerInformation = function(event) {
|
||||||
var container_information = JSON.parse(event.data);
|
var container_information = JSON.parse(event.data);
|
||||||
$('#stop').data('container', container_information);
|
$('#stop').data('container', container_information);
|
||||||
@ -1065,6 +1098,164 @@ $(function() {
|
|||||||
$('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary());
|
$('#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 requestComments = function(e) {
|
||||||
var user_id = $('#editor').data('user-id')
|
var user_id = $('#editor').data('user-id')
|
||||||
var exercise_id = $('#editor').data('exercise-id')
|
var exercise_id = $('#editor').data('exercise-id')
|
||||||
@ -1123,6 +1314,7 @@ $(function() {
|
|||||||
initializeEventHandlers();
|
initializeEventHandlers();
|
||||||
initializeFileTree();
|
initializeFileTree();
|
||||||
initializeTooltips();
|
initializeTooltips();
|
||||||
|
initPrompt();
|
||||||
renderScore();
|
renderScore();
|
||||||
showFirstFile();
|
showFirstFile();
|
||||||
showRequestedTab();
|
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
|
end
|
||||||
|
|
||||||
def exercise_params
|
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
|
end
|
||||||
private :exercise_params
|
private :exercise_params
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ class SubmissionsController < ApplicationController
|
|||||||
include Lti
|
include Lti
|
||||||
include SubmissionParameters
|
include SubmissionParameters
|
||||||
include SubmissionScoring
|
include SubmissionScoring
|
||||||
|
include Tubesock::Hijack
|
||||||
|
|
||||||
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
|
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
|
||||||
before_action :set_docker_client, only: [:run, :test]
|
before_action :set_docker_client, only: [:run, :test]
|
||||||
@ -70,20 +71,95 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
with_server_sent_events do |server_sent_event|
|
# TODO reimplement SSEs with websocket commands
|
||||||
output = @docker_client.execute_run_command(@submission, params[:filename])
|
# 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({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?
|
||||||
|
# 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
|
||||||
|
|
||||||
unless output[:stderr].nil?
|
hijack do |tubesock|
|
||||||
if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr])
|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||||
server_sent_event.write(hint, event: 'hint')
|
|
||||||
else
|
result = @docker_client.execute_run_command(@submission, params[:filename])
|
||||||
store_error(output[:stderr])
|
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
|
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
|
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
|
#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)
|
div class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') = render('editor_file_tree', files: @files)
|
||||||
#frames.col-sm-9
|
div id='frames' class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9')
|
||||||
- @files.each do |file|
|
- @files.each do |file|
|
||||||
= render('editor_frame', exercise: exercise, file: file)
|
= render('editor_frame', exercise: exercise, file: file)
|
||||||
#autosave-label
|
#autosave-label
|
||||||
|
@ -23,6 +23,10 @@
|
|||||||
label
|
label
|
||||||
= f.check_box(:public)
|
= f.check_box(:public)
|
||||||
= t('activerecord.attributes.exercise.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')
|
h2 = t('activerecord.attributes.exercise.files')
|
||||||
ul#files.list-unstyled
|
ul#files.list-unstyled
|
||||||
= f.fields_for :files do |files_form|
|
= f.fields_for :files do |files_form|
|
||||||
|
@ -13,44 +13,46 @@
|
|||||||
#development-environment
|
#development-environment
|
||||||
ul.nav.nav-justified.nav-tabs role='tablist'
|
ul.nav.nav-justified.nav-tabs role='tablist'
|
||||||
li.active
|
li.active
|
||||||
a data-placement='top' data-toggle='tab' data-tooltip=true href='#instructions' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1')
|
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-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')
|
|
||||||
i.fa.fa-code
|
i.fa.fa-code
|
||||||
= t('.workspace')
|
= t('.workspace')
|
||||||
li
|
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
|
i.fa.fa-terminal
|
||||||
= t('.output')
|
= t('.output')
|
||||||
li
|
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
|
i.fa.fa-line-chart
|
||||||
= t('.progress')
|
= t('.progress')
|
||||||
|
|
||||||
hr
|
hr
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
#instructions.tab-pane.active
|
#workspace.tab-pane.active = render('editor', exercise: @exercise, files: @files, submission: @submission)
|
||||||
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)
|
|
||||||
#outputInformation.tab-pane data-message-no-output=t('.no_output')
|
#outputInformation.tab-pane data-message-no-output=t('.no_output')
|
||||||
#hint
|
#hint
|
||||||
.panel.panel-warning
|
.panel.panel-warning
|
||||||
.panel-heading = t('.hint')
|
.panel-heading = t('.hint')
|
||||||
.panel-body
|
.panel-body
|
||||||
#output
|
.row
|
||||||
pre = t('.no_output_yet')
|
/ #output-col1.col-sm-12
|
||||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
|
#output-col1
|
||||||
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
// todo set to full width if turtle isnt used
|
||||||
.panel-heading = 'Gain more insights here'
|
#prompt.input-group.hidden
|
||||||
.panel-body
|
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
|
#progress.tab-pane
|
||||||
#results
|
#results
|
||||||
h2 = t('.results')
|
h2 = t('.results')
|
||||||
|
@ -14,6 +14,7 @@ h1
|
|||||||
= row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil)
|
= 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.maximum_score', value: @exercise.maximum_score)
|
||||||
= row(label: 'exercise.public', value: @exercise.public?)
|
= row(label: 'exercise.public', value: @exercise.public?)
|
||||||
|
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
|
||||||
= row(label: 'exercise.embedding_parameters') do
|
= row(label: 'exercise.embedding_parameters') do
|
||||||
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@ default: &default
|
|||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
host: tcp://192.168.59.104:2376
|
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:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
@ -28,6 +28,7 @@ de:
|
|||||||
execution_environment: Ausführungsumgebung
|
execution_environment: Ausführungsumgebung
|
||||||
execution_environment_id: Ausführungsumgebung
|
execution_environment_id: Ausführungsumgebung
|
||||||
files: Dateien
|
files: Dateien
|
||||||
|
hide_file_tree: Dateibaum verstecken
|
||||||
instructions: Anweisungen
|
instructions: Anweisungen
|
||||||
maximum_score: Erreichbare Punktzahl
|
maximum_score: Erreichbare Punktzahl
|
||||||
public: Öffentlich
|
public: Öffentlich
|
||||||
|
@ -28,6 +28,7 @@ en:
|
|||||||
execution_environment: Execution Environment
|
execution_environment: Execution Environment
|
||||||
execution_environment_id: Execution Environment
|
execution_environment_id: Execution Environment
|
||||||
files: Files
|
files: Files
|
||||||
|
hide_file_tree: Hide File Tree
|
||||||
instructions: Instructions
|
instructions: Instructions
|
||||||
maximum_score: Maximum Score
|
maximum_score: Maximum Score
|
||||||
public: Public
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -79,6 +79,7 @@ ActiveRecord::Schema.define(version: 20150903152727) do
|
|||||||
t.string "user_type"
|
t.string "user_type"
|
||||||
t.string "token"
|
t.string "token"
|
||||||
t.integer "team_id"
|
t.integer "team_id"
|
||||||
|
t.boolean "hide_file_tree"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "external_users", force: true do |t|
|
create_table "external_users", force: true do |t|
|
||||||
|
@ -11,6 +11,7 @@ class DockerClient
|
|||||||
RETRY_COUNT = 2
|
RETRY_COUNT = 2
|
||||||
|
|
||||||
attr_reader :container
|
attr_reader :container
|
||||||
|
attr_reader :socket
|
||||||
|
|
||||||
def self.check_availability!
|
def self.check_availability!
|
||||||
Timeout.timeout(config[:connection_timeout]) { Docker.version }
|
Timeout.timeout(config[:connection_timeout]) { Docker.version }
|
||||||
@ -41,7 +42,12 @@ class DockerClient
|
|||||||
'Memory' => execution_environment.memory_limit.megabytes,
|
'Memory' => execution_environment.memory_limit.megabytes,
|
||||||
'NetworkDisabled' => !execution_environment.network_enabled?,
|
'NetworkDisabled' => !execution_environment.network_enabled?,
|
||||||
'OpenStdin' => true,
|
'OpenStdin' => true,
|
||||||
'StdinOnce' => true
|
'StdinOnce' => true,
|
||||||
|
# required to expose standard streams over websocket
|
||||||
|
'AttachStdout' => true,
|
||||||
|
'AttachStdin' => true,
|
||||||
|
'AttachStderr' => true,
|
||||||
|
'Tty' => true
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -52,6 +58,29 @@ class DockerClient
|
|||||||
}
|
}
|
||||||
end
|
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 = {})
|
def copy_file_to_workspace(options = {})
|
||||||
FileUtils.cp(options[:file].native_file.path, local_file_path(options))
|
FileUtils.cp(options[:file].native_file.path, local_file_path(options))
|
||||||
end
|
end
|
||||||
@ -118,14 +147,71 @@ class DockerClient
|
|||||||
#(tries += 1) <= RETRY_COUNT ? retry : raise(error)
|
#(tries += 1) <= RETRY_COUNT ? retry : raise(error)
|
||||||
end
|
end
|
||||||
|
|
||||||
[:run, :test].each do |cause|
|
def execute_websocket_command(command, before_execution_block, output_consuming_block)
|
||||||
define_method("execute_#{cause}_command") do |submission, filename, &block|
|
@container = DockerContainerPool.get_container(@execution_environment)
|
||||||
command = submission.execution_environment.send(:"#{cause}_command") % command_substitutions(filename)
|
if @container
|
||||||
create_workspace_files = proc { create_workspace_files(container, submission) }
|
before_execution_block.try(:call)
|
||||||
execute_command(command, create_workspace_files, block)
|
# 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
|
||||||
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)
|
def self.find_image_by_tag(tag)
|
||||||
Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) }
|
Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) }
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
class PyUnitAdapter < TestingFrameworkAdapter
|
class PyUnitAdapter < TestingFrameworkAdapter
|
||||||
COUNT_REGEXP = /Ran (\d+) tests/
|
COUNT_REGEXP = /Ran (\d+) test/
|
||||||
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/
|
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/
|
||||||
|
|
||||||
def self.framework_name
|
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