Merge remote-tracking branch 'origin/master' into statistics

This commit is contained in:
Maximilian Grundke
2015-10-29 10:24:30 +01:00
17 changed files with 65 additions and 4531 deletions

View File

@ -50,6 +50,7 @@ GEM
bootstrap-will_paginate (0.0.10)
will_paginate
builder (3.2.2)
byebug (6.0.2)
capistrano (3.3.5)
capistrano-stats (~> 1.1.0)
i18n
@ -108,6 +109,7 @@ GEM
json
erubis (2.7.0)
eventmachine (1.0.8)
eventmachine (1.0.8-java)
excon (0.45.2)
execjs (2.5.2)
factory_girl (4.5.0)
@ -321,6 +323,8 @@ GEM
websocket (1.2.1)
websocket-driver (0.6.2)
websocket-extensions (>= 0.1.0)
websocket-driver (0.6.2-java)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
will_paginate (3.0.7)
xpath (2.0.0)
@ -337,6 +341,7 @@ DEPENDENCIES
better_errors
binding_of_caller
bootstrap-will_paginate
byebug
capistrano (~> 3.3.0)
capistrano-rails
capistrano-rvm
@ -387,3 +392,6 @@ DEPENDENCIES
uglifier (>= 1.3.0)
web-console (~> 2.0)
will_paginate (~> 3.0)
BUNDLED WITH
1.10.6

View File

@ -28,15 +28,16 @@ Execute: vagrant box add ubuntu/trusty64
Execute: vagrant up
Install docker environments
export DOCKER_HOST=tcp://192.168.23.75:2375
docker pull openhpi/docker_java
docker pull openhpi/docker_ruby
docker pull openhpi/docker_python
(The following images need to be moved to openhpi/docker_[coffee|sqlite|etc.] if they are required at some point.
docker pull jprberlin/ubuntu-coffee
docker pull jprberlin/ubuntu-java
docker pull jprberlin/ubuntu-sqlite
docker pull jprberlin/ubuntu-sinatra
docker pull jprberlin/ubuntu-ruby
docker pull jprberlin/ubuntu-python
docker pull jprberlin/ubuntu-node
docker pull jprberlin/ubuntu-html
docker pull jprberlin/ubuntu-jruby
docker pull jprberlin/ubuntu-jruby)
cd repoPath
bundle install

View File

@ -30,7 +30,7 @@ $(function() {
numMessages = 0,
turtlecanvas = $('#turtlecanvas'),
prompt = $('#prompt'),
commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'status'],
commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'timeout', 'status'],
streams = ['stdin', 'stdout', 'stderr'];
var ENTER_KEY_CODE = 13;
@ -686,8 +686,8 @@ $(function() {
};
var isBrowserSupported = function() {
// todo event streams are no longer required with websockets
return window.EventSource !== undefined;
// eventsource tests for server send events (used for scoring), websockets is used for run
return Modernizr.eventsource && Modernizr.websockets;
};
var populatePanel = function(panel, result, index) {
@ -1004,7 +1004,7 @@ $(function() {
};
var showTimeoutMessage = function() {
$.flash.danger({
$.flash.info({
icon: ['fa', 'fa-clock-o'],
text: $('#editor').data('message-timeout')
});
@ -1059,14 +1059,6 @@ $(function() {
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
@ -1125,7 +1117,7 @@ $(function() {
};
var initWebsocketConnection = function(url) {
websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url);
websocket = new WebSocket('wss://' + 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); };
@ -1160,7 +1152,7 @@ $(function() {
}
switch(msg.cmd) {
case 'input':
showPrompt();
showPrompt(msg);
break;
case 'write':
printWebsocketOutput(msg);
@ -1176,6 +1168,10 @@ $(function() {
case 'exit':
killWebsocketAndContainer();
break;
case 'timeout':
// just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes.
showTimeoutMessage();
break;
case 'status':
showStatus(msg)
break;
@ -1249,11 +1245,13 @@ $(function() {
executeWebsocketCommand(msg);
};
var showPrompt = function() {
if (prompt.isPresent() && prompt.hasClass('hidden')) {
var showPrompt = function(msg) {
var label = $('#prompt .input-group-addon');
label.text(msg.data || label.data('prompt'));
if (prompt.isPresent() && prompt.hasClass('hidden')) {
prompt.removeClass('hidden');
}
prompt.focus();
$('#prompt input').focus();
}
var hidePrompt = function() {

View File

@ -0,0 +1,3 @@
/*! modernizr 3.1.0 (Custom Build) | MIT *
* http://modernizr.com/download/?-eventsource-websockets !*/
!function(e,n,s){function o(e,n){return typeof e===n}function a(e){var n=l.className,s=Modernizr._config.classPrefix||"";if(f&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+s+"no-js(\\s|$)");n=n.replace(o,"$1"+s+"js$2")}Modernizr._config.enableClasses&&(n+=" "+s+e.join(" "+s),f?l.className.baseVal=n:l.className=n)}function t(){var e,n,s,a,t,l,f;for(var r in c){if(e=[],n=c[r],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(s=0;s<n.options.aliases.length;s++)e.push(n.options.aliases[s].toLowerCase());for(a=o(n.fn,"function")?n.fn():n.fn,t=0;t<e.length;t++)l=e[t],f=l.split("."),1===f.length?Modernizr[f[0]]=a:(!Modernizr[f[0]]||Modernizr[f[0]]instanceof Boolean||(Modernizr[f[0]]=new Boolean(Modernizr[f[0]])),Modernizr[f[0]][f[1]]=a),i.push((a?"":"no-")+f.join("-"))}}var i=[],l=n.documentElement,f="svg"===l.nodeName.toLowerCase(),c=[],r={_version:"3.1.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var s=this;setTimeout(function(){n(s[e])},0)},addTest:function(e,n,s){c.push({name:e,fn:n,options:s})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=r,Modernizr=new Modernizr,Modernizr.addTest("websockets","WebSocket"in e&&2===e.WebSocket.CLOSING),Modernizr.addTest("eventsource","EventSource"in e),t(),a(i),delete r.addTest,delete r.addAsyncTest;for(var u=0;u<Modernizr._q.length;u++)Modernizr._q[u]();e.Modernizr=Modernizr}(window,document);

View File

@ -90,6 +90,12 @@ class SubmissionsController < ApplicationController
hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
# socket is the socket into the container, tubesock is the socket to the client
# give the docker_client the tubesock object, so that it can send messages (timeout)
@docker_client.tubesock = tubesock
result = @docker_client.execute_run_command(@submission, params[:filename])
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})

View File

@ -3,6 +3,6 @@ class Comment < ActiveRecord::Base
include Creation
attr_accessor :username
belongs_to :file, class: CodeOcean::File
belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :user, polymorphic: true
end

View File

@ -2,7 +2,7 @@ module Context
extend ActiveSupport::Concern
included do
has_many :files, as: :context, class: CodeOcean::File
has_many :files, as: :context, class_name: 'CodeOcean::File'
accepts_nested_attributes_for :files
end

View File

@ -1,6 +1,6 @@
class RequestForComment < ActiveRecord::Base
belongs_to :exercise
belongs_to :file, class: CodeOcean::File
belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :user, polymorphic: true
before_create :set_requested_timestamp

View File

@ -39,7 +39,7 @@
#output-col1
// todo set to full width if turtle isnt used
#prompt.input-group.hidden
span.input-group-addon = t('exercises.editor.input')
span.input-group-addon data-prompt=t('exercises.editor.input') = t('exercises.editor.input')
input#prompt-input.form-control type='text'
span.input-group-btn
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')

View File

@ -22,6 +22,7 @@ production:
interval: 30
timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243
test:
<<: *default

View File

@ -191,7 +191,7 @@ de:
stop: Stoppen
submit: Code zur Bewertung abgeben
test: Testen
timeout: 'Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
timeout: 'Ausführung gestoppt. Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
tooltips:
save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig.
editor_file_tree:

View File

@ -191,7 +191,7 @@ en:
stop: Stop
submit: Submit Code For Assessment
test: Test
timeout: 'Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
timeout: 'Execution stopped. Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
tooltips:
save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary.
editor_file_tree:

View File

@ -12,6 +12,7 @@ class DockerClient
attr_reader :container
attr_reader :socket
attr_accessor :tubesock
def self.check_availability!
Timeout.timeout(config[:connection_timeout]) { Docker.version }
@ -61,7 +62,7 @@ class DockerClient
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')
query_params = 'logs=0&stream=1&' + (stderr ? 'stderr=1' : 'stdout=1&stdin=1')
# Headers are required by Docker
headers = {'Origin' => 'http://localhost'}
@ -138,8 +139,8 @@ class DockerClient
def execute_command(command, before_execution_block, output_consuming_block)
#tries ||= 0
@container = DockerContainerPool.get_container(@execution_environment)
@container.status = :executing
if @container
@container.status = :executing
before_execution_block.try(:call)
send_command(command, @container, &output_consuming_block)
else
@ -153,8 +154,8 @@ class DockerClient
def execute_websocket_command(command, before_execution_block, output_consuming_block)
@container = DockerContainerPool.get_container(@execution_environment)
@container.status = :executing
if @container
@container.status = :executing
before_execution_block.try(:call)
# todo catch exception if socket could not be created
@socket ||= create_socket(@container)
@ -171,17 +172,25 @@ class DockerClient
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
@thread = Thread.new do
timeout = @execution_environment.permitted_execution_time.to_i # seconds
sleep(timeout)
if container.status != :returned
Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.")
# send timeout to the tubesock socket
if(@tubesock)
@tubesock.send_data JSON.dump({'cmd' => 'timeout'})
end
kill_container(container)
end
end
end
def exit_container(container)
# exit the timeout thread if it is still alive
if(@thread && @thread.alive?)
@thread.exit
end
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
end

View File

@ -29,15 +29,10 @@ DOCKER_OPTS="-H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock"
EOF
service docker restart
fi
# XXX docker pull jprberlin/ubuntu-coffee
docker pull jprberlin/ubuntu-java
# XXX docker pull jprberlin/ubuntu-sqlite
# XXX docker pull jprberlin/ubuntu-sinatra
# XXX docker pull jprberlin/ubuntu-ruby
# XXX docker pull jprberlin/ubuntu-python
docker pull jprberlin/ubuntu-node
# XXX docker pull jprberlin/ubuntu-html
# XXX docker pull jprberlin/ubuntu-jruby
docker pull openhpi/docker_java
docker pull openhpi/docker_ruby
docker pull openhpi/docker_python
# rvm

View File

@ -1,234 +1 @@
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()
# moved to dockerfiles/ubuntu-python

File diff suppressed because it is too large Load Diff

View File

@ -1,178 +1 @@
#!/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'})
# moved to dockerfiles/ubuntu-python/webpython.py