diff options
-rw-r--r-- | README.txt | 2 | ||||
-rwxr-xr-x | code_server.py (renamed from python_server.py) | 80 | ||||
-rw-r--r-- | docs/sample_questions.py | 15 | ||||
-rw-r--r-- | docs/sample_questions.xml | 2 | ||||
-rw-r--r-- | exam/management/commands/load_questions_xml.py | 4 | ||||
-rw-r--r-- | exam/models.py | 8 | ||||
-rw-r--r-- | exam/views.py | 5 | ||||
-rw-r--r-- | exam/xmlrpc_clients.py | 37 |
8 files changed, 134 insertions, 19 deletions
@@ -21,7 +21,7 @@ To install/deploy this app follow the steps below: 4. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: - $ sudo python python_server.py + $ sudo python code_server.py Put this in the background once it has started since this will not return back the prompt. It is important that the server be running diff --git a/python_server.py b/code_server.py index d33ee47..63f3073 100755 --- a/python_server.py +++ b/code_server.py @@ -5,12 +5,12 @@ and returns the output. It *should* be run as root and will run as the user by editing settings.py to run as many servers as desired. One can also specify the ports on the command line. Here are examples:: - $ sudo ./python_server.py + $ sudo ./code_server.py # Runs servers based on settings.py:SERVER_PORTS one server per port given. or:: - $ sudo ./python_server.py 8001 8002 8003 8004 8005 + $ sudo ./code_server.py 8001 8002 8003 8004 8005 # Runs 5 servers on ports specified. All these servers should be running as nobody. @@ -20,20 +20,26 @@ import traceback from SimpleXMLRPCServer import SimpleXMLRPCServer import pwd import os -from os.path import isdir +import stat +from os.path import isdir, dirname, abspath, join import signal from multiprocessing import Process +import subprocess # Local imports. from settings import SERVER_PORTS, SERVER_TIMEOUT +MY_DIR = abspath(dirname(__file__)) def run_as_nobody(): """Runs the current process as nobody.""" - # Set the effective uid to that of nobody. + # Set the uid and to that of nobody. nobody = pwd.getpwnam('nobody') - os.setegid(nobody.pw_gid) - os.seteuid(nobody.pw_uid) + os.setgid(nobody.pw_gid) + os.setuid(nobody.pw_uid) + +################################################################################ +# Python related code. # Raised when the code times-out. # c.f. http://pguides.net/python/timeout-a-function @@ -44,8 +50,7 @@ def timeout_handler(signum, frame): """A handler for the ALARM signal.""" raise TimeoutException('Code took too long to run.') - -def run_code(answer, test_code, in_dir=None): +def run_python_code(answer, test_code, in_dir=None): """Tests given Python function (`answer`) with the `test_code` supplied. If the optional `in_dir` keyword argument is supplied it changes the directory to that directory (it does not change it back to the original when @@ -97,9 +102,66 @@ def run_code(answer, test_code, in_dir=None): return success, err +################################################################################ +# Run code for Bash. +def run_bash_code(answer, test_code, in_dir=None): + + """Tests given Bash code (`answer`) with the `test_code` supplied. It + assumes that there are two parts to the test_code separated by '#++++++'. + + If the optional `in_dir` keyword argument is supplied it changes the + directory to that directory (it does not change it back to the original when + done). + + Returns + ------- + + A tuple: (success, error message). + + """ + if in_dir is not None and isdir(in_dir): + os.chdir(in_dir) + + def _set_exec(fname): + os.chmod(ref_fname, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR + |stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP + |stat.S_IROTH|stat.S_IWOTH|stat.S_IXOTH) + + # XXX: fix this to not hardcode it to 6 +'s! + reference, args = test_code.split('#++++++') + ref_f = open('reference.sh', 'w') + ref_f.write(reference); ref_f.close() + ref_fname = abspath(ref_f.name) + _set_exec(ref_fname) + args_f = open('reference.args', 'w') + args_f.write(args); args_f.close() + _set_exec(args_f.name) + submit_f = open('submit.sh', 'w') + submit_f.write(answer); submit_f.close() + submit_fname = submit_f.name + _set_exec(submit_fname) + + tester = join(MY_DIR, 'shell_script_tester.sh') + + # Run the shell code in a subprocess. + try: + output = subprocess.check_output([tester, ref_fname, submit_fname], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError, exc: + success = False + err = 'Error: exist status: %d, message: %s'%(exc.returncode, + exc.output) + else: + success = True + err = 'Correct answer' + + return success, err + + def run_server(port): server = SimpleXMLRPCServer(("localhost", port)) - server.register_function(run_code) + server.register_function(run_python_code) + server.register_function(run_bash_code) server.serve_forever() def main(): diff --git a/docs/sample_questions.py b/docs/sample_questions.py index 2a9eaf8..eac9479 100644 --- a/docs/sample_questions.py +++ b/docs/sample_questions.py @@ -4,6 +4,7 @@ questions = [ Question( summary='Factorial', points=2, + language="python", description=''' Write a function called <code>fact</code> which takes a single integer argument (say <code>n</code>) and returns the factorial of the number. @@ -18,6 +19,7 @@ assert fact(5) == 120 Question( summary='Simple function', points=1, + language="python", description='''Create a simple function called <code>sqr</code> which takes a single argument and returns the square of the argument. For example: <br/> <code>sqr(3) -> 9</code>.''', @@ -26,6 +28,19 @@ import math assert sqr(3) == 9 assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14 '''), +Question( + summary='Bash addition', + points=2, + language="bash", + description='''Write a shell script which takes two arguments on the + command line and prints the sum of the two on the output.''', + test='''\ +#!/bin/bash +[[ $# -eq 2 ]] && echo $(( $1 + $2 )) && exit $(( $1 + $2 )) +#++++++ +1 2 +2 1 +'''), ] quiz = Quiz(start_date=date.today(), diff --git a/docs/sample_questions.xml b/docs/sample_questions.xml index 104ea32..cad205b 100644 --- a/docs/sample_questions.xml +++ b/docs/sample_questions.xml @@ -10,6 +10,7 @@ and returns the factorial of the number. For example fact(3) -> 6 </description> <points>2</points> +<language>python</language> <test> assert fact(0) == 1 assert fact(5) == 120 @@ -26,6 +27,7 @@ returns the square of the argument For example sqr(3) -> 9. </description> <points>1</points> +<language>python</language> <test> import math assert sqr(3) == 9 diff --git a/exam/management/commands/load_questions_xml.py b/exam/management/commands/load_questions_xml.py index aa403dd..b4151ae 100644 --- a/exam/management/commands/load_questions_xml.py +++ b/exam/management/commands/load_questions_xml.py @@ -35,6 +35,9 @@ def load_questions_xml(filename): desc_node = question.getElementsByTagName("description")[0] description = (desc_node.childNodes[0].data).strip() + lang_node = question.getElementsByTagName("language")[0] + language = (lang_node.childNodes[0].data).strip() + points_node = question.getElementsByTagName("points")[0] points = int((points_node.childNodes[0].data).strip()) \ if points_node else 1 @@ -45,6 +48,7 @@ def load_questions_xml(filename): new_question = Question(summary=summary, description=description, points=points, + language=language, test=test) new_question.save() diff --git a/exam/models.py b/exam/models.py index d433c7c..ef4312f 100644 --- a/exam/models.py +++ b/exam/models.py @@ -12,6 +12,11 @@ class Profile(models.Model): position = models.CharField(max_length=64) +LANGUAGE_CHOICES = ( + ("python", "Python"), + ("bash", "Bash"), + ) + ################################################################################ class Question(models.Model): """A question in the database.""" @@ -29,6 +34,9 @@ class Question(models.Model): # This is simple Python code. test = models.TextField() + # The language being tested. + language = models.CharField(max_length=10, choices=LANGUAGE_CHOICES) + # Is this question active or not. If it is inactive it will not be used # when creating a QuestionPaper. active = models.BooleanField(default=True) diff --git a/exam/views.py b/exam/views.py index bafd0be..1f92553 100644 --- a/exam/views.py +++ b/exam/views.py @@ -13,7 +13,7 @@ from django.http import Http404 # Local imports. from exam.models import Quiz, Question, QuestionPaper, Profile, Answer, User from exam.forms import UserRegisterForm, UserLoginForm -from exam.xmlrpc_clients import python_server +from exam.xmlrpc_clients import code_server from settings import URL_ROOT # The directory where user data can be saved. @@ -203,7 +203,8 @@ def check(request, q_id): # with the code executed safely in a separate process (the python_server.py) # running as nobody. user_dir = get_user_dir(user) - success, err_msg = python_server.run_code(answer, question.test, user_dir) + success, err_msg = code_server.run_code(answer, question.test, + user_dir, question.language) new_answer.error = err_msg if success: diff --git a/exam/xmlrpc_clients.py b/exam/xmlrpc_clients.py index 115ee6e..01172d7 100644 --- a/exam/xmlrpc_clients.py +++ b/exam/xmlrpc_clients.py @@ -4,7 +4,7 @@ import random import socket -class PythonServer(object): +class CodeServer(object): """A class that manages accesing the farm of Python servers and making calls to them such that no one XMLRPC server is overloaded. """ @@ -12,19 +12,41 @@ class PythonServer(object): servers = [ServerProxy('http://localhost:%d'%(x)) for x in SERVER_PORTS] self.servers = servers self.indices = range(len(SERVER_PORTS)) + self.methods = {"python": 'run_python_code', + "bash": 'run_bash_code'} - def run_code(self, answer, test_code, user_dir): - """See the documentation of the method of the same name in - python_server.py. + def run_code(self, answer, test_code, user_dir, language): + """Tests given code (`answer`) with the `test_code` supplied. If the + optional `in_dir` keyword argument is supplied it changes the directory + to that directory (it does not change it back to the original when + done). The parameter language specifies which language to use for the + tests. + + Parameters + ---------- + answer : str + The user's answer for the question. + test_code : str + The test code to check the user code with. + user_dir : str (directory) + The directory to run the tests inside. + language : str + The programming language to use. + + Returns + ------- + A tuple: (success, error message). """ + method_name = self.methods[language] done = False - result = [False, 'Unable to connect to any Python servers!'] + result = [False, 'Unable to connect to any code servers!'] # Try to connect a few times if not, quit. count = 5 while (not done) and (count > 0): try: server = self._get_server() - result = server.run_code(answer, test_code, user_dir) + method = getattr(server, method_name) + result = method(answer, test_code, user_dir) except socket.error: count -= 1 else: @@ -38,4 +60,5 @@ class PythonServer(object): # views.py calls this Python server which forwards the request to one # of the running servers. -python_server = PythonServer() +code_server = CodeServer() + |