summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPrabhu Ramachandran2011-11-24 02:11:40 +0530
committerPrabhu Ramachandran2011-11-24 02:11:40 +0530
commit11a2eaefaba6d2b547d35afbee3e85b18520afd2 (patch)
treef05aef4423f613c4d38232569df77a88c66978e7
parent30f56443790841901f15b5ab435f97fba1c81d85 (diff)
downloadonline_test-11a2eaefaba6d2b547d35afbee3e85b18520afd2.tar.gz
online_test-11a2eaefaba6d2b547d35afbee3e85b18520afd2.tar.bz2
online_test-11a2eaefaba6d2b547d35afbee3e85b18520afd2.zip
ENH/TMP: Preliminary support for bash scripts.
- Changing the Question model to add a language attribute. - Moving python_server.py -> code_server.py. - Adding functionality to test for Shell scripts. This is still incomplete since the shell code checker seems to have some problems. - Modified the xmlrpc_clients to support multiple languages and right now two. - Using setgid/setuid instead of setegid/seteuid in the code_server.py.. - Adding a bash example to the sample_questions.py. The shell script support doesn't quite work yet but this is really a code_server/checking issue.
-rw-r--r--README.txt2
-rwxr-xr-xcode_server.py (renamed from python_server.py)80
-rw-r--r--docs/sample_questions.py15
-rw-r--r--docs/sample_questions.xml2
-rw-r--r--exam/management/commands/load_questions_xml.py4
-rw-r--r--exam/models.py8
-rw-r--r--exam/views.py5
-rw-r--r--exam/xmlrpc_clients.py37
8 files changed, 134 insertions, 19 deletions
diff --git a/README.txt b/README.txt
index 8c1c9c1..1d7821b 100644
--- a/README.txt
+++ b/README.txt
@@ -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()
+