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
|
|-- <id>value-to-be-returned</id>"""
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()