import sys import os import time import commands import threading import subprocess import tempfile import signal import time import pickle import cookielib import urllib2 import resource import types import StringIO import logging import xml.dom.minidom as minidom from settings import * import rules class EvaluatorError(Exception): def __init__(self, error, value=''): self.value = value self.error = error def __str__(self): return 'Error: '+self.error+'\tValue: '+ self.value class XMLParser: """This base class will contain all the functions needed for the XML file to be parsed NOTE: This class is not to be instantiated """ logger = logging.getLogger('XML') def __init__(self): raise NotImplementedError('XMLParser class is not to be instantiated') def dos2unix(self, data): """Converts text from DOS CRLF to UNIX LF""" return data.replace('\r\n', '\n') def get_val_by_id(self, root, id): """This function will get the value of the `id' child node of `root' node. `root' should be of type DOM Element. e.g. root | |-- value-to-be-returned""" child_node = root.getElementsByTagName(id) if not child_node: self.logger.critical('Invalid XML file: \n'+root.toxml()) raise EvaluatorError('Invalid XML file') return self.dos2unix(child_node[0].firstChild.nodeValue) def add_node(self, doc, root, child, value): """ Used to add a text node 'child' with the value of 'value'(duh..) """ node = doc.createElement(child) node.appendChild(doc.createTextNode(value)) root.appendChild(node) class Question(XMLParser): """Defines the Characteristics of each question in the contest""" logger = logging.getLogger('XML.Question') def __init__(self, qn, qid): input_data = self.get_val_by_id(qn, 'input-data') self.input_path = self.save_input_to_disk(input_data, qid) self.time_limit = float(self.get_val_by_id(qn, 'time-limit')) self.mem_limit = int(self.get_val_by_id(qn, 'mem-limit')) self.eval_path = self.save_eval_to_disk(qn, qid) self.logger.info('Received New Question: Input Data - %s, Time Limit -' \ ' %f, Memory Limit - %d, Evaluator - %s' % (self.input_path, self.time_limit, self.mem_limit, self.eval_path)) def save_input_to_disk(self, input_data, qid): """Saves the input file to disk""" inp_path = os.path.join(INPUT_PATH, qid) inp_file = open(inp_path, 'w') input_data = str(input_data) inp_file.write(input_data) inp_file.close() return inp_path def save_eval_to_disk(self, qn, qid): """This function will unpickle the evaluator, sent from web server and save it into a file in the directory `evaluators' in the name of the question id""" # Save the pickled Evaluator binary to disk evaluator = qn.getElementsByTagName('evaluator')[0].firstChild.nodeValue eval_file_path = os.path.join(EVALUATOR_PATH, qid) ev = StringIO.StringIO() ev.write(evaluator) ev.seek(0) eval_file = open(eval_file_path, 'w') eval_file.close() os.chmod(eval_file_path, 0700) # set executable permission for # code checker return eval_file_path class Questions(XMLParser): """Set of all questions in the contest""" def __init__(self, xml_file): xml = minidom.parseString(xml_file) qn_set = xml.getElementsByTagName('question-set')[0] if not qn_set: pass # TODO: return error here self.questions = {} for qn in qn_set.getElementsByTagName('question'): qid = str(qn.getAttribute('id').strip()) self.questions[qid] = Question(qn, qid) class Attempt(XMLParser): """Each Attempt XML file is parsed by this class""" logger = logging.getLogger('XML.Attempt') def __init__(self, xml_file): xml = minidom.parseString(xml_file) attempt = xml.getElementsByTagName('attempt') if not attempt: pass # TODO: return error here attempt = attempt[0] self.aid = self.get_val_by_id(attempt, 'aid') self.qid = self.get_val_by_id(attempt, 'qid') self.code = self.get_val_by_id(attempt, 'code') self.lang = self.get_val_by_id(attempt, 'lang') self.file_name = self.get_val_by_id(attempt, 'file-name') self.logger.info('Obtained an attempt: AID - %s, QID - %s, LANG - %s,'\ ' FILENAME - %s' % (self.aid, self.qid, self.lang, self.file_name)) def convert_to_result(self, result, msg): """Converts an attempt into a corresponding XML file to notify result""" doc = minidom.Document() root = doc.createElementNS('http://code.google.com/p/hackzor', 'attempt') doc.appendChild(root) self.add_node(doc, root, 'aid', self.aid) self.add_node(doc, root, 'result', str(result)) if int(result) in rules.rules.keys(): msg = rules.rules[int(result)] self.add_node(doc, root, 'error', msg) self.logger.info('Attempt %s evaluated to \'%s\' with return value %s' % (self.aid, msg, result)) print msg return doc.toxml() class Evaluator: """Provides the base functions for evaluating an attempt. NOTE: This class is not to be instantiated""" logger = logging.getLogger('XML.Attempt') def compile(self, code_file, input_file): raise NotImplementedError('Must be Overridden') def get_run_cmd(self, exec_file): raise NotImplementedError('Must be Overridden') def is_compiled(self): raise NotImplementedError('Must be Overridden') def get_additional_time(self): """In the case that a particular language is to be awarded extra execution time over ride this function. The return value in seconds will be the amount of extra execution time awarded to the program """ return 0 # Defaults to no additional time def get_additional_mem(self): """In the case that a particular language is to be awarded extra memory over ride this function. The return value in bytes will be the amount of extra execution time awarded to the program """ return 0 # Defaults to no additional memory def run(self, cmd, quest): """Execute the command `cmd' and returns the execution output file descriptor in a tempfile.NamedTemporaryFile() object. """ input_file = quest.input_path output_file = tempfile.NamedTemporaryFile() # subprocess will not accept StringIO objects inp = open (input_file, 'r') kws = {'shell':True, 'stdin':inp, 'stdout':output_file.file} start_time = time.time() mem_limit = quest.mem_limit + self.get_additional_mem() time_limit = quest.time_limit + self.get_additional_time() # TODO: Convert all these print statements in logging files print 'Running: ', cmd print 'Memory limit: ', mem_limit print 'Time limit: ', time_limit self.logger.info('Executing command: ' + cmd) # TODO: pass the input file as a parameter to exec.py too p = subprocess.Popen('./exec.py '+str(quest.mem_limit)+' '+cmd, **kws) while True: if time.time() - start_time >= quest.time_limit: inp.close() # TODO: Try to execute the KILL code internall with no # dependancies self.logger.info('pgreping for '+os.path.basename(cmd)) status, psid = commands.getstatusoutput('pgrep -f '+os.path.basename(cmd)) psid = psid.splitlines() if os.getpid() in psid: psid.remove(os.getpid()) # do not kill evaluator itself in # case of a python TLE if psid == '': print 'oO Problems of problems. Kill manually' self.logger.critical('No Process ID to be killed. Look into '\ 'this manually') psid = [p.pid] for proc in psid: self.logger.info('Killing psid '+proc) try: os.kill (int(proc), signal.SIGKILL) except OSError: pass # process does not exist self.logger.info('Killed Process Tree: %d' % p.pid) raise EvaluatorError('Time Limit Expired') elif p.poll() != None: break time.sleep(PS_POLL_INTVL) if p.returncode == 139: raise EvaluatorError('Run-Time Error. Received SIGSEGV') elif p.returncode == 137: raise EvaluatorError('Run-Time Error. Received SIGTERM') elif p.returncode == 143: raise EvaluatorError('Run-Time Error. Received SIGKILL') elif p.returncode != 0 : raise EvaluatorError('Run-Time Error. Unknown Error') else: output_file.file.flush() output_file.file.seek(0) return output_file def save_file(self, file_path, contents): """ Save the contents in the file given by file_path relative inside the WORK_DIR directory""" if not os.path.exists(WORK_DIR): os.mkdir(WORK_DIR) file_path = os.path.join(WORK_DIR, file_path) open_file = open(file_path, 'w') open_file.write(contents) open_file.close() return file_path def evaluate(self, attempt, quest): """Evaluate the attempt and serve the ruling""" # Save the File save_loc = attempt.aid + '-' + attempt.qid + '-' + attempt.file_name code_file = self.save_file(save_loc, attempt.code) # Java has this dirty requirement that the file name be the same as the # main class name. So having a workaround. The attempts are saved #(for archival purposes only) and java files are also saved in a # temporary directory called java if attempt.lang.lower() == 'java': save_loc = os.path.join(JAVA_TEMP_DIR, attempt.file_name) self.logger.info('Java Submission. Saving file with original name '\ 'to ' + save_loc) code_file = self.save_file(save_loc, attempt.code) # Compile the File self.logger.info('Compiling Attempt ' + attempt.aid) exec_file = self.compile(code_file) cmd = self.get_run_cmd(exec_file) # Execute the file for preset input try: output = self.run(cmd, quest) except: if self.is_compiled(): self.logger.info('Removing Object File: ' + exec_file) os.remove(exec_file) raise if self.is_compiled(): self.logger.info('Removing Object File: ' + exec_file) os.remove(exec_file) self.logger.info('Running Output Checker') return self.check(attempt, output, quest.eval_path) def check(self, attempt, output, eval_path): """This receives a tempfile.NamedTemporaryFile object which contains the output of the user program """ output.seek(0) kws = {'shell':True, 'stdin':output.file} p = subprocess.Popen(eval_path, **kws) p.wait() output.close() return str(p.returncode) class C_Evaluator(Evaluator): def __init__(self): self.compile_cmd = C_COMPILE_STR def __str__(self): return 'C Evaluator' def get_run_cmd(self, exec_file): return os.path.join('.', exec_file) def is_compiled(self): return True def compile(self, code_file): output_file = code_file # Change this value to change output file name # replace the code with the object file code_file = code_file.replace('\'','\\\'').replace('\"','\\\"') cmd = self.compile_cmd.replace('%i',code_file).replace('%o',output_file) (status, output) = commands.getstatusoutput(cmd) if status != 0: raise EvaluatorError('Compiler Error', output) else: return output_file class CPP_Evaluator(C_Evaluator): def __init__(self): self.compile_cmd = CPP_COMPILE_STR def __str__(self): return 'C++ Evaluator' class CSharp_Evaluator(C_Evaluator): """NOTE: No of processes should not be limited to 0 for the CSharp Eval to work. It usually forks about 15-20 times on initialising """ def __init__(self): self.compile_cmd = CSHARP_COMPILE_STR def __str__(self): return 'CSharp Evaluator' def get_additional_mem(self): return 15*1024*1024 def compile(self, code_file): return C_Evaluator.compile(self, code_file)+'.exe' class Java_Evaluator(Evaluator): def __str__(self): return 'Java Evaluator' def __init__(self): self.compile_cmd = JAVA_COMPILE_STR self.link_cmd = JAVA_LINK_STR def get_run_cmd(self, exec_file): return exec_file def is_compiled(self): return True def get_additional_mem(self): return 20*1024*1024 def get_additional_time(self): return 2.0 def compile(self, code_file): """When we use GCJ to convert the java file to native binary, it goes through two phases, the compiling phase and the linking phase """ output_dir, file_name = os.path.split(code_file) code_file = code_file.replace('\'','\\\'') cmd = self.compile_cmd.replace('%i',code_file) if file_name [-5:] != '.java': raise EvaluatorError('Compiler Error', 'Not a Java File') file_name = file_name [:-5] (status, output) = commands.getstatusoutput(cmd) if status != 0: raise EvaluatorError('Compiler Error', output) out = os.path.join(WORK_DIR, file_name) cmd = self.link_cmd.replace('%i', file_name).replace('%o', out) (status, output) = commands.getstatusoutput(cmd) if status != 0: raise EvaluatorError('Compiler Error', output) else: return out class Interpreted_Language(Evaluator): def compile(self, code_file): """ Nothing to Compile. Aha *Magic*!""" os.chmod(code_file,0700) return code_file def get_run_cmd(self, exec_file): return os.path.join('.', exec_file) # SHEBANG line is very very important for this to work.! # python = /usr/bin/env python # ruby = /usr/bin/ruby # perl = /usr/bin/perl # PHP = /usr/bin/php def is_compiled(self): return False def get_additional_mem(self): return 20*1024*1024 def get_additional_time(self): return 2.0 class Python_Evaluator(Interpreted_Language): def __str__(self): return 'Python Evaluator' class Ruby_Evaluator(Interpreted_Language): def __str__(self): return 'Ruby Evaluator' class Perl_Evaluator(Interpreted_Language): def __str__(self): return 'Perl Evaluator' def get_additional_mem(self): return 10*1024*1024 # Perl is much more light weight compared to the # other interpreted languages class PHP_Evaluator(Interpreted_Language): def __str__(self): return 'PHP Evaluator' class Client: """ The Evaluator will evaluate and update the status """ evaluators = {'c':C_Evaluator, 'c++':CPP_Evaluator, 'java':Java_Evaluator, 'python':Python_Evaluator, 'ruby':Ruby_Evaluator, 'php':PHP_Evaluator, 'perl':Perl_Evaluator, 'csharp':CSharp_Evaluator} def __init__(self): from settings import EVALUATOR_KEYID root_url = CONTEST_URL + '/opc/evaluator/'+EVALUATOR_KEYID self.get_attempt_url = root_url + '/getattempt/' self.submit_attempt_url = root_url + '/submitattempt/' self.get_qns = root_url + '/getquestionset/' self.get_pub_key = root_url + '/getpubkey/' self.question_set = Questions(self.read_page(self.get_qns)) def __del__(self): logging.shutdown() def read_page(self, website): cj = cookielib.CookieJar() cookie_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) urllib2.install_opener(cookie_opener) req = urllib2.Request(website, None) data = urllib2.urlopen(req) data = data.read() return data def get_attempt(self): """ Keep polling the server until an attempt to be evaluated is obtained """ req = urllib2.Request(self.get_attempt_url, None) req = urllib2.urlopen(req) return Attempt(req.read()) def evaluate(self, attempt, quest): """ Evaluate the attempt and return the ruling :-) attempt : An instance of the Attempt Class return value : Boolean, the result of the evaluation. """ lang = attempt.lang.lower() # first list special case languages whose names cannot be used for # function names in python try: evaluator = self.evaluators[lang]() return evaluator.evaluate(attempt, quest) except KeyError: raise NotImplementedError('Language '+lang+' not supported') except EvaluatorError: raise def submit_attempt(self, attempt_xml): """This will submit the XML file containing the result of the attempt back to the server """ host = CONTEST_URL selector = self.submit_attempt_url #attempt_xml = self.obj.sign(attempt_xml).data headers = {'Content-Type': 'application/xml', 'Content-Length': str(len(attempt_xml))} r = urllib2.Request(self.submit_attempt_url, data=attempt_xml, headers=headers) return urllib2.urlopen(r).read() def start(self): logger = logging.getLogger('Client') logger.info('Evaluator Started') logger.info('Waiting for attempts from '+CONTEST_URL) print 'Evaluator Started' while True: try: logger.info('Waiting for Attempt') attempt = self.get_attempt() except urllib2.HTTPError: attempt = None # No attempts in web server queue to evaluate time.sleep(TIME_INTERVAL) continue # Evaluate the attempt logger.info('Obtained an Attempt. Starting Evaluation') try: return_value = self.evaluate(attempt, self.question_set.questions[str(attempt.qid)]) msg = '' except EvaluatorError: print 'EvaluatorError: ' msg = sys.exc_info()[1].error return_value = 3 logger.exception('Exception Occured. Return Value is 3') print 'Final Result: ', return_value, msg print self.submit_attempt(attempt.convert_to_result(return_value, msg)) return return_value if __name__ == '__main__': if len(sys.argv) >= 2: log_file = sys.argv[1] else: log_file = 'hackzor.log' logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', filename=log_file, filemode='w') Client().start()