summaryrefslogtreecommitdiff
path: root/code_server.py
diff options
context:
space:
mode:
authorparth2011-12-09 04:02:19 +0530
committerparth2011-12-09 04:02:19 +0530
commit861d2cc5e36835f60bace61a919e73b4bd27274b (patch)
treed08d776f58167903781c371d147f5b9e1f83e168 /code_server.py
parent7104f495d01fb934af11c8dfd09da087174c1b12 (diff)
downloadonline_test-861d2cc5e36835f60bace61a919e73b4bd27274b.tar.gz
online_test-861d2cc5e36835f60bace61a919e73b4bd27274b.tar.bz2
online_test-861d2cc5e36835f60bace61a919e73b4bd27274b.zip
Moved all the apps to testapp folder
Diffstat (limited to 'code_server.py')
-rwxr-xr-xcode_server.py360
1 files changed, 0 insertions, 360 deletions
diff --git a/code_server.py b/code_server.py
deleted file mode 100755
index 1276c76..0000000
--- a/code_server.py
+++ /dev/null
@@ -1,360 +0,0 @@
-#!/usr/bin/env python
-"""This server runs an XMLRPC server that can be submitted code and tests
-and returns the output. It *should* be run as root and will run as the user
-'nobody' so as to minimize any damange by errant code. This can be configured
-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 ./code_server.py
- # Runs servers based on settings.py:SERVER_PORTS one server per port given.
-
-or::
-
- $ sudo ./code_server.py 8001 8002 8003 8004 8005
- # Runs 5 servers on ports specified.
-
-All these servers should be running as nobody. This will also start a server
-pool that defaults to port 50000 and is configurable in
-settings.py:SERVER_POOL_PORT. This port exposes a `get_server_port` function
-that returns an available server.
-"""
-import sys
-import traceback
-from SimpleXMLRPCServer import SimpleXMLRPCServer
-import pwd
-import os
-import stat
-from os.path import isdir, dirname, abspath, join, isfile
-import signal
-from multiprocessing import Process, Queue
-import subprocess
-
-# Local imports.
-from settings import SERVER_PORTS, SERVER_TIMEOUT, SERVER_POOL_PORT
-
-MY_DIR = abspath(dirname(__file__))
-
-def run_as_nobody():
- """Runs the current process as nobody."""
- # Set the effective uid and to that of nobody.
- nobody = pwd.getpwnam('nobody')
- os.setegid(nobody.pw_gid)
- os.seteuid(nobody.pw_uid)
-
-
-# Raised when the code times-out.
-# c.f. http://pguides.net/python/timeout-a-function
-class TimeoutException(Exception):
- pass
-
-def timeout_handler(signum, frame):
- """A handler for the ALARM signal."""
- raise TimeoutException('Code took too long to run.')
-
-
-################################################################################
-# `CodeServer` class.
-################################################################################
-class CodeServer(object):
- """A code server that executes user submitted test code, tests it and
- reports if the code was correct or not.
- """
- def __init__(self, port, queue):
- self.port = port
- self.queue = queue
- msg = 'Code took more than %s seconds to run. You probably '\
- 'have an infinite loop in your code.'%SERVER_TIMEOUT
- self.timeout_msg = msg
-
- def run_python_code(self, 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
- done). This function also timesout when the function takes more than
- SERVER_TIMEOUT seconds to run to prevent runaway code.
-
- Returns
- -------
-
- A tuple: (success, error message).
-
- """
- if in_dir is not None and isdir(in_dir):
- os.chdir(in_dir)
-
- # Add a new signal handler for the execution of this code.
- old_handler = signal.signal(signal.SIGALRM, timeout_handler)
- signal.alarm(SERVER_TIMEOUT)
-
- success = False
- tb = None
- try:
- submitted = compile(answer, '<string>', mode='exec')
- g = {}
- exec submitted in g
- _tests = compile(test_code, '<string>', mode='exec')
- exec _tests in g
- except TimeoutException:
- err = self.timeout_msg
- except AssertionError:
- type, value, tb = sys.exc_info()
- info = traceback.extract_tb(tb)
- fname, lineno, func, text = info[-1]
- text = str(test_code).splitlines()[lineno-1]
- err = "{0} {1} in: {2}".format(type.__name__, str(value), text)
- except:
- type, value = sys.exc_info()[:2]
- err = "Error: {0}".format(repr(value))
- else:
- success = True
- err = 'Correct answer'
- finally:
- del tb
- # Set back any original signal handler.
- signal.signal(signal.SIGALRM, old_handler)
-
- # Cancel the signal if any, see signal.alarm documentation.
- signal.alarm(0)
-
- # Put us back into the server pool queue since we are free now.
- self.queue.put(self.port)
-
- return success, err
-
- def run_bash_code(self, answer, test_code, in_dir=None):
- """Tests given Bash code (`answer`) with the `test_code` supplied.
-
- The testcode should typically contain two lines, the first is a path to
- the reference script we are to compare against. The second is a path
- to the arguments to be supplied to the reference and submitted script.
- The output of these will be compared for correctness.
-
- If the path's start with a "/" then we assume they are absolute paths.
- If not, we assume they are relative paths w.r.t. the location of this
- code_server script.
-
- 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(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)
- submit_f = open('submit.sh', 'w')
- submit_f.write(answer.lstrip()); submit_f.close()
- submit_path = abspath(submit_f.name)
- _set_exec(submit_path)
-
- ref_path, test_case_path = test_code.strip().splitlines()
- if not ref_path.startswith('/'):
- ref_path = join(MY_DIR, ref_path)
- if not test_case_path.startswith('/'):
- test_case_path = join(MY_DIR, test_case_path)
-
- # Add a new signal handler for the execution of this code.
- old_handler = signal.signal(signal.SIGALRM, timeout_handler)
- signal.alarm(SERVER_TIMEOUT)
-
- # Do whatever testing needed.
- success = False
- try:
- success, err = self.check_bash_script(ref_path, submit_path, test_case_path)
- except TimeoutException:
- err = self.timeout_msg
- except:
- type, value = sys.exc_info()[:2]
- err = "Error: {0}".format(repr(value))
- finally:
- # Set back any original signal handler.
- signal.signal(signal.SIGALRM, old_handler)
-
- # Delete the created file.
- os.remove(submit_path)
-
- # Cancel the signal if any, see signal.alarm documentation.
- signal.alarm(0)
-
- # Put us back into the server pool queue since we are free now.
- self.queue.put(self.port)
-
- return success, err
-
- def _run_command(self, cmd_args, *args, **kw):
- """Run a command in a subprocess while blocking, the process is killed
- if it takes more than 2 seconds to run. Return the Popen object, the
- stdout and stderr.
- """
- try:
- proc = subprocess.Popen(cmd_args, *args, **kw)
- stdout, stderr = proc.communicate()
- except TimeoutException:
- # Runaway code, so kill it.
- proc.kill()
- # Re-raise exception.
- raise
- return proc, stdout, stderr
-
- def check_bash_script(self, ref_script_path, submit_script_path,
- test_case_path=None):
- """ Function validates student script using instructor script as
- reference. Test cases can optionally be provided. The first argument
- ref_script_path, is the path to instructor script, it is assumed to
- have executable permission. The second argument submit_script_path, is
- the path to the student script, it is assumed to have executable
- permission. The Third optional argument is the path to test the
- scripts. Each line in this file is a test case and each test case is
- passed to the script as standard arguments.
-
- Returns
- --------
-
- returns (True, "Correct answer") : If the student script passes all test
- cases/have same output, when compared to the instructor script
-
- returns (False, error_msg): If
- the student script fails a single test/have dissimilar output, when
- compared to the instructor script.
-
- Returns (False, error_msg): If mandatory arguments are not files or if the
- required permissions are not given to the file(s).
-
- """
- if not isfile(ref_script_path):
- return False, "No file at %s"%ref_script_path
- if not isfile(submit_script_path):
- return False, 'No file at %s'%submit_script_path
- if not os.access(ref_script_path, os.X_OK):
- return False, 'Script %s is not executable'%ref_script_path
- if not os.access(submit_script_path, os.X_OK):
- return False, 'Script %s is not executable'%submit_script_path
-
- if test_case_path is None:
- ret = self._run_command(ref_script_path, stdin=None,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- proc, inst_stdout, inst_stderr = ret
- ret = self._run_command(submit_script_path, stdin=None,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- proc, stdnt_stdout, stdnt_stderr = ret
- if inst_stdout == stdnt_stdout:
- return True, 'Correct answer'
- else:
- err = "Error: expected %s, got %s"%(inst_stderr, stdnt_stderr)
- return False, err
- else:
- if not isfile(test_case_path):
- return False, "No test case at %s"%test_case_path
- if not os.access(ref_script_path, os.R_OK):
- return False, "Test script %s, not readable"%test_case_path
- valid_answer = True # We initially make it one, so that we can stop
- # once a test case fails
- loop_count = 0 # Loop count has to be greater than or equal to one.
- # Useful for caching things like empty test files,etc.
- test_cases = open(test_case_path).readlines()
- num_lines = len(test_cases)
- for test_case in test_cases:
- loop_count += 1
- if valid_answer:
- args = [ ref_script_path ] + [x for x in test_case.split()]
- ret = self._run_command(args, stdin=None,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- proc, inst_stdout, inst_stderr = ret
- args = [ submit_script_path ] + [x for x in test_case.split()]
- ret = self._run_command(args, stdin=None,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- proc, stdnt_stdout, stdnt_stderr = ret
- valid_answer = inst_stdout == stdnt_stdout
- if valid_answer and (num_lines == loop_count):
- return True, "Correct answer"
- else:
- err = "Error: expected %s, got %s"%(inst_stdout+inst_stderr,
- stdnt_stdout+stdnt_stderr)
- return False, err
-
-
- def run(self):
- """Run XMLRPC server, serving our methods.
- """
- server = SimpleXMLRPCServer(("localhost", self.port))
- self.server = server
- server.register_instance(self)
- self.queue.put(self.port)
- server.serve_forever()
-
-
-################################################################################
-# `ServerPool` class.
-################################################################################
-class ServerPool(object):
- """Manages a pool of CodeServer objects."""
- def __init__(self, ports, pool_port=50000):
- """Create a pool of servers. Uses a shared Queue to get available
- servers.
-
- Parameters
- ----------
-
- ports : list(int)
- List of ports at which the CodeServer's should run.
-
- pool_port : int
- Port at which the server pool should serve.
- """
- self.my_port = pool_port
- self.ports = ports
- queue = Queue(maxsize=len(ports))
- self.queue = queue
- servers = []
- for port in ports:
- server = CodeServer(port, queue)
- servers.append(server)
- p = Process(target=server.run)
- p.start()
- self.servers = servers
-
- def get_server_port(self):
- """Get available server port from ones in the pool. This will block
- till it gets an available server.
- """
- q = self.queue
- was_waiting = True if q.empty() else False
- port = q.get()
- if was_waiting:
- print '*'*80
- print "No available servers, was waiting but got server later at %d."%port
- print '*'*80
- sys.stdout.flush()
- return port
-
- def run(self):
- """Run server which returns an available server port where code
- can be executed.
- """
- server = SimpleXMLRPCServer(("localhost", self.my_port))
- self.server = server
- server.register_instance(self)
- server.serve_forever()
-
-
-################################################################################
-def main():
- run_as_nobody()
- if len(sys.argv) == 1:
- ports = SERVER_PORTS
- else:
- ports = [int(x) for x in sys.argv[1:]]
-
- server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT)
- server_pool.run()
-
-if __name__ == '__main__':
- main()