├── judgesite ├── __init__.py ├── config.py ├── service.py └── task.py ├── requirements.txt ├── README.md ├── main.py ├── .gitignore └── LICENSE /judgesite/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'maemual' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pika==0.9.14 2 | requests==2.9.1 3 | ljudge -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OnlineJudgeSite 2 | =============== 3 | 4 | python写的分布式判题节点 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | 6 | from judgesite.service import JudgeSite 7 | 8 | 9 | def main(): 10 | logging.basicConfig(format='%(levelname)s:%(asctime)s %(filename)s %(funcName)s %(lineno)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=logging.INFO) 11 | logging.info("Judge Node starting...") 12 | srv = JudgeSite() 13 | srv.run() 14 | 15 | 16 | if __name__ == "__main__": 17 | main() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | venv/ 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | .idea/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .coverage 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | *.sublime-project 47 | *.sublime-workspace 48 | 49 | # Rope 50 | .ropeproject 51 | 52 | # Django stuff: 53 | *.log 54 | *.pot 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | lastSource 60 | run_dir/ 61 | data_dir/ 62 | Core 63 | *.out 64 | *.in 65 | *.out 66 | *.log 67 | testdata/ 68 | tmp/ 69 | core_log.txt 70 | -------------------------------------------------------------------------------- /judgesite/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from collections import namedtuple 5 | 6 | import requests 7 | 8 | Configure = namedtuple("Configure", [ 9 | 'testdata_path', 10 | 'tmp_path', 11 | 'rmq_host', 12 | 'rmq_port', 13 | 'rmq_user', 14 | 'rmq_password', 15 | 'judge_task_queue', 16 | 'judge_exchange', 17 | 'judge_result_queue' 18 | ]) 19 | rabbitmq_setting = requests.get( 20 | 'http://etcc.in.njoj.org:8009/services/rabbitmq-01/configures/production/').json()['data'] 21 | judge_site_setting = requests.get( 22 | 'http://etcc.in.njoj.org:8009/services/judge-site/configures/default/').json()['data'] 23 | 24 | conf = Configure( 25 | testdata_path="", 26 | tmp_path="", 27 | rmq_host=rabbitmq_setting['HOST'], 28 | rmq_port=rabbitmq_setting['PORT'], 29 | rmq_user=rabbitmq_setting['USER'], 30 | rmq_password=rabbitmq_setting['PASSWORD'], 31 | judge_task_queue=judge_site_setting['judge_task_queue'], 32 | judge_exchange=judge_site_setting['judge_exchange'], 33 | judge_result_queue=judge_site_setting['judge_result_queue'], 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 maemual 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /judgesite/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import logging 6 | import pika 7 | import json 8 | 9 | from config import conf 10 | from task import JudgeTask 11 | 12 | 13 | class JudgeSite(object): 14 | 15 | def __init__(self): 16 | self.connection = pika.BlockingConnection( 17 | pika.ConnectionParameters(host=conf.rmq_host, port=conf.rmq_port)) 18 | self.channel = self.connection.channel() 19 | 20 | # judge_task_queue 21 | self.channel.queue_declare(queue=conf.judge_task_queue, durable=True) 22 | self.channel.queue_bind(queue=conf.judge_task_queue, 23 | exchange=conf.judge_exchange, 24 | routing_key=conf.judge_task_queue) 25 | # judge_result_queue 26 | self.channel.queue_declare(queue=conf.judge_result_queue, durable=True) 27 | self.channel.queue_bind(queue=conf.judge_result_queue, 28 | exchange=conf.judge_exchange, 29 | routing_key=conf.judge_result_queue) 30 | 31 | self.channel.basic_qos(prefetch_count=1) 32 | self.channel.basic_consume(self._consume, queue=conf.judge_task_queue) 33 | 34 | def _consume(self, ch, method, properties, body): 35 | logging.info("GOT A TASK!") 36 | task = JudgeTask(body, self.save_result) 37 | task.run() 38 | self.channel.basic_ack(delivery_tag=method.delivery_tag) 39 | logging.info("TASK IS DONE!") 40 | 41 | def run(self): 42 | self.channel.start_consuming() 43 | 44 | def save_result(self, id, run_time=0, run_memory=0, compiler_output="", status="SystemError"): 45 | def ensure_unicode(s, encoding='utf-8'): 46 | return s.decode(encoding) if isinstance(s, str) else s 47 | compiler_output = ensure_unicode(compiler_output) 48 | status = ensure_unicode(status) 49 | body = { 50 | u'id': id, 51 | u'data': { 52 | u'run_time': run_time, 53 | u'run_memory': run_memory, 54 | u'compiler_output': compiler_output, 55 | u'status': status 56 | } 57 | 58 | } 59 | self.channel.basic_publish( 60 | exchange=conf.judge_exchange, 61 | routing_key=conf.judge_result_queue, 62 | body=json.dumps(body, ensure_ascii=False), # We shouldn't mix unicode with str 63 | properties=pika.BasicProperties( 64 | delivery_mode=2, # make message persistent 65 | )) 66 | -------------------------------------------------------------------------------- /judgesite/task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division 5 | from __future__ import unicode_literals 6 | 7 | import logging 8 | import json 9 | import shutil 10 | import os 11 | 12 | import ljudge 13 | 14 | from config import conf 15 | 16 | 17 | class NoTestDataException(Exception): 18 | pass 19 | 20 | 21 | class NoSpecialJudgeException(Exception): 22 | pass 23 | 24 | 25 | def make_task_result(): 26 | result = { 27 | 'status': '', 28 | 'time': 0, 29 | 'memory': 0, 30 | 'compiler_output': '', 31 | } 32 | return result 33 | 34 | 35 | class JudgeTask(object): 36 | 37 | def __init__(self, message, save_result_callback): 38 | task = json.loads(message) 39 | self.id = task['id'] 40 | self.code = task["code"] 41 | self.language = task["language"] 42 | self.testdata_id = str(task["testdata_id"]) 43 | self.time_limit = str(task["time_limit"]) 44 | self.memory_limit = str(task["memory_limit"]) 45 | self.validator = str(task["validator"]) 46 | 47 | self.result = make_task_result() 48 | 49 | self.save_result_callback = save_result_callback 50 | 51 | logging.info("Task id is: %s" % self.id) 52 | 53 | def run(self): 54 | self._clean_files() 55 | try: 56 | self._prepare_temp_dir() 57 | 58 | self._dump_code_to_file() 59 | 60 | self._prepare_testdata_file() 61 | except NoTestDataException as e: 62 | self.result['status'] = 'NoTestDataError' 63 | except NoSpecialJudgeException as e: 64 | self.result['status'] = 'NoSpecialJudgeException' 65 | except Exception as e: 66 | self.result['status'] = 'System Error' 67 | logging.exception(e) 68 | else: 69 | self._run() 70 | 71 | self._save_result() 72 | 73 | self._clean_files() 74 | 75 | def _prepare_temp_dir(self): 76 | logging.info("Prepare temp dir") 77 | os.mkdir(conf.tmp_path) 78 | 79 | def _dump_code_to_file(self): 80 | logging.info("Dump code to file") 81 | filename = "Main." + self.language 82 | self.code_file = os.path.join(conf.tmp_path, filename) 83 | with open(self.code_file, 'w') as code_file: 84 | code_file.write(self.code.encode('utf-8')) 85 | 86 | def _prepare_testdata_file(self): 87 | logging.info("Prepare testdata") 88 | self.input_file = os.path.join( 89 | conf.testdata_path, self.testdata_id, "in.in") 90 | self.output_file = os.path.join( 91 | conf.testdata_path, self.testdata_id, "out.out") 92 | if not os.path.exists(self.input_file) or\ 93 | not os.path.exists(self.output_file): 94 | raise NoTestDataException 95 | if self.validator == 'Special Validator': 96 | self.spj_code_file = os.path.join( 97 | conf.testdata_path, self.testdata_id, "specialjudge.cpp") 98 | if not os.path.exists(self.spj_code_file): 99 | # 不存在spj程序, 2016/10/22 由于老版SPJ题不存在spj.cpp, 导致judge site崩溃 100 | raise NoSpecialJudgeException() 101 | 102 | @staticmethod 103 | def _parse_ljudge_result(ljudge_res): 104 | result = make_task_result() 105 | if not ljudge_res['compilation']['success']: 106 | result['status'] = 'Compile Error' 107 | result['compiler_output'] = ljudge_res['compilation']['log'] 108 | return result 109 | if not ljudge_res.get('checkerCompilation', dict(success=True)).\ 110 | get('success'): 111 | result['status'] = 'Spj Compile Error' 112 | return result 113 | testcase = ljudge_res['testcases'][0] # oj 单case 114 | status_map = { 115 | 'ACCEPTED': 'Accepted', 116 | 'PRESENTATION_ERROR': 'Presentation Error', 117 | 'WRONG_ANSWER': 'Wrong Answer', 118 | 'NON_ZERO_EXIT_CODE': 'Non Zero Exit Code', 119 | 'MEMORY_LIMIT_EXCEEDED': 'Memory Limit Exceeded', 120 | 'TIME_LIMIT_EXCEEDED': 'Time Limit Exceeded', 121 | 'OUTPUT_LIMIT_EXCEEDED': 'Output Limit Exceeded', 122 | 'FLOAT_POINT_EXCEPTION': 'Float Point Error', 123 | 'SEGMENTATION_FAULT': 'Segmentation Fault', 124 | 'RUNTIME_ERROR': 'Runtime Error', 125 | 'INTERNAL_ERROR': 'System Error', 126 | } 127 | result['status'] = status_map.get(testcase['result'], 'System Error') 128 | result['time'] = int(testcase.get('time', 0)*1000) # s to ms 129 | result['memory'] = int(testcase.get('memory', 0)/1024) # B to KB 130 | return result 131 | 132 | def _run(self): 133 | logging.info("GO!GO!GO!") 134 | opts = { 135 | 'max-cpu-time': int(self.time_limit) / 1000, 136 | 'max-real-time': 20.0, # 最多运行20s 137 | 'max-memory': '{0}K'.format(self.memory_limit), 138 | 'user-code': self.code_file, 139 | 'max-compiler-real-time': 10, 140 | 'max-compiler-memory': '256M', 141 | 'testcase': { 142 | 'input': self.input_file, 143 | 'output': self.output_file, 144 | } 145 | } 146 | if self.validator == 'Special Validator': 147 | opts['checker-code'] = self.spj_code_file 148 | self.result = self._parse_ljudge_result(ljudge.run(opts)) 149 | 150 | def _save_result(self): 151 | logging.info("Save result, result is %s" % self.result['status']) 152 | self.save_result_callback( 153 | id=self.id, 154 | run_time=self.result['time'], 155 | run_memory=self.result['memory'], 156 | compiler_output=self.result['compiler_output'], 157 | status=self.result['status']) 158 | 159 | def _clean_files(self): 160 | logging.info("Clean files") 161 | if os.path.exists(conf.tmp_path): 162 | shutil.rmtree(conf.tmp_path) 163 | --------------------------------------------------------------------------------