easyctf-iv-platform/judge/executor.py
Michael Zhang 4225cc4dde
Initial.
2018-02-20 22:37:10 -06:00

300 lines
11 KiB
Python

import json
import logging
import os
import shutil
import subprocess
import tempfile
import time
from functools import wraps
from typing import Iterator
import config
from languages import Language
from models import ExecutionResult, Job, JobVerdict, Problem
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logging.info('Starting up')
verdict_map = {
'InternalError': JobVerdict.judge_error,
'RuntimeError': JobVerdict.runtime_error,
'TimeLimitExceeded': JobVerdict.time_limit_exceeded,
'MemoryLimitExceeded': JobVerdict.memory_limit_exceeded,
'IllegalSyscall': JobVerdict.illegal_syscall,
'IllegalOpen': JobVerdict.illegal_syscall,
'IllegalWrite': JobVerdict.illegal_syscall,
}
class ExecutionReport:
def __init__(self, execution_ok: bool, execution_error_code: JobVerdict, exitcode: int, realtime: float,
cputime: float,
memory: int):
self.execution_ok = execution_ok
self.execution_error_code = execution_error_code
self.exitcode = exitcode
self.realtime = realtime
self.cputime = cputime
self.memory = memory
@classmethod
def error_report(cls):
return cls(
execution_ok=False,
execution_error_code=JobVerdict.judge_error,
exitcode=-1,
realtime=0,
cputime=0,
memory=0,
)
@classmethod
def from_json(cls, json_string: str):
try:
obj = json.loads(json_string)
return cls(
execution_ok=obj['execution_ok'],
execution_error_code=verdict_map[obj['execution_error_code']['code']] if obj['execution_error_code'] else None,
exitcode=obj['exitcode'],
realtime=obj['realtime'],
cputime=obj['cputime'],
memory=obj['memory'],
)
except (json.JSONDecodeError, KeyError):
logger.error('Failed to load execution report from json!')
return cls.error_report()
class ExecutionProfile:
def __init__(self, confine_path: str, problem: Problem, language: Language, workdir: str,
input_file='input', output_file='output', error_file='error', report_file='report'):
self.confine_path = confine_path
self.language = language
self.workdir = workdir
self.time_limit = problem.time_limit
self.memory_limit = problem.memory_limit
self.input_file = os.path.join(workdir, input_file)
self.output_file = os.path.join(workdir, output_file)
self.error_file = os.path.join(workdir, error_file)
self.report_file = os.path.join(workdir, report_file)
def as_json(self, executable_name: str):
return json.dumps({
'cputime_limit': self.time_limit,
'realtime_limit': self.time_limit * 1000,
'allowed_files': self.language.get_allowed_files(self.workdir, executable_name),
'allowed_prefixes': self.language.get_allowed_file_prefixes(self.workdir, executable_name),
'stdin_file': self.input_file,
'stdout_file': self.output_file,
'stderr_file': self.error_file,
'json_report_file': self.report_file,
})
def execute(self, executable_name: str) -> ExecutionReport:
return Executor(self).execute(executable_name)
class Executor:
def __init__(self, profile: ExecutionProfile):
self.profile = profile
def execute(self, executable_name: str) -> ExecutionReport:
command = self.profile.language.get_command(self.profile.workdir, executable_name)
config_file_path = os.path.join(self.profile.workdir, 'confine.json')
with open(config_file_path, 'w') as config_file:
config_file.write(self.profile.as_json(executable_name))
try:
subprocess.check_call([self.profile.confine_path, '-c', config_file_path, '--', *command],
timeout=self.profile.time_limit * 2)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
return ExecutionReport.error_report()
with open(os.path.join(self.profile.workdir, self.profile.report_file)) as report_file:
execution_report = ExecutionReport.from_json(report_file.read())
return execution_report
def use_tempdir(func):
@wraps(func)
def wrapper(*args, **kwargs):
before_dir = os.getcwd()
tempdir = tempfile.mkdtemp(prefix='jury-')
os.chdir(tempdir)
result = func(*args, tempdir=tempdir, **kwargs)
os.chdir(before_dir)
# shutil.rmtree(tempdir)
return result
return wrapper
# @use_tempdir
def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]:
result = ExecutionResult(
job=job,
verdict=JobVerdict.judge_error,
last_ran_case=0,
execution_time=0,
execution_memory=0,
)
if job.problem.source_verifier_code and job.problem.source_verifier_language:
source_verifier_executable = job.problem.source_verifier_language.compile(job.problem.source_verifier_code, tempdir,
'source_verifier')
if not source_verifier_executable:
logger.error('Source verifier failed to compile for problem %d' % job.problem.id)
result.verdict = JobVerdict.judge_error
yield result
return
with open(os.path.join(tempdir, 'source'), 'wb') as source_file:
source_file.write(job.code.encode('utf-8'))
execution_profile = ExecutionProfile(
confine_path=str(config.CONFINE_PATH),
problem=job.problem,
language=job.problem.source_verifier_language,
workdir=tempdir,
input_file='source',
output_file='source_verifier_result',
)
execution_result = execution_profile.execute(source_verifier_executable)
if not execution_result.execution_ok:
result.verdict = JobVerdict.judge_error
yield result
return
with open(os.path.join(tempdir, 'source_verifier_result')) as source_verifier_result_file:
if source_verifier_result_file.read().strip() != 'OK':
result.verdict = JobVerdict.invalid_source
yield result
return
program_executable = job.language.compile(job.code, tempdir, 'program')
if not program_executable:
result.verdict = JobVerdict.compilation_error
yield result
return
generator_executable = job.problem.generator_language.compile(job.problem.generator_code, tempdir, 'generator')
if not generator_executable:
logger.error('Generator failed to compile for problem %d' % job.problem.id)
result.verdict = JobVerdict.judge_error
yield result
return
grader_executable = job.problem.grader_language.compile(job.problem.grader_code, tempdir, 'grader')
if not grader_executable:
logger.error('Grader failed to compile for problem %d' % job.problem.id)
result.verdict = JobVerdict.judge_error
yield result
return
result.verdict = None
last_submitted_time = time.time()
last_submitted_case = 0
for case_number in range(1, job.problem.test_cases + 1):
result.last_ran_case = case_number
case_result = run_test_case(job, case_number, tempdir, program_executable, generator_executable, grader_executable)
if case_result.verdict != JobVerdict.accepted:
result = case_result
break
result.execution_time = max(result.execution_time, case_result.execution_time)
result.execution_memory = max(result.execution_memory, case_result.execution_memory)
# Yield result if over threshold and is not last case
# If verdict calculation takes time, result should be changed to yield even if is last case.
if (time.time() - last_submitted_time > config.PARTIAL_JOB_SUBMIT_TIME_THRESHOLD or
case_number - last_submitted_case > config.PARTIAL_JOB_SUBMIT_CASES_THRESHOLD) and \
case_number != job.problem.test_cases + 1:
yield result
# We want to let the programs run for `threshold` time before another potential pause
last_submitted_time = time.time()
last_submitted_case = case_number
if not result.verdict:
result.verdict = JobVerdict.accepted
yield result
def run_test_case(job: Job, case_number: int, workdir: str, program_executable: str, generator_executable: str,
grader_executable: str) -> ExecutionResult:
result = ExecutionResult(
job=job,
verdict=JobVerdict.judge_error,
last_ran_case=case_number,
execution_time=0,
execution_memory=0,
)
with open(os.path.join(workdir, 'case_number'), 'wb') as case_number_file:
case_number_file.write(str(case_number).encode('utf-8'))
generator_execution_profile = ExecutionProfile(
confine_path=str(config.CONFINE_PATH),
problem=job.problem,
language=job.problem.generator_language,
workdir=workdir,
input_file='case_number',
output_file='input',
)
generator_result = generator_execution_profile.execute(generator_executable)
if not generator_result.execution_ok:
logger.error('Generator failed for test case %d of problem %d with error %s' %
(case_number, job.problem.id, generator_result.execution_error_code))
return result
program_execution_profile = ExecutionProfile(
confine_path=str(config.CONFINE_PATH),
problem=job.problem,
language=job.language,
workdir=workdir,
input_file='input',
output_file='program_output',
error_file='program_error',
)
execution_result = program_execution_profile.execute(program_executable)
result.execution_time = execution_result.realtime
result.execution_memory = execution_result.memory
if not execution_result.execution_ok:
result.verdict = execution_result.execution_error_code
return result
grader_execution_profile = ExecutionProfile(
confine_path=str(config.CONFINE_PATH),
problem=job.problem,
language=job.problem.grader_language,
workdir=workdir,
input_file='input',
output_file='grader_output',
error_file='grader_error',
)
grader_result = grader_execution_profile.execute(grader_executable)
if not grader_result.execution_ok:
logger.error('Grader failed for test case %d of problem %d with error %s' %
(case_number, job.problem.id, grader_result.execution_error_code))
result.verdict = JobVerdict.judge_error
return result
with open(os.path.join(workdir, 'program_output'), 'rb') as program_output, \
open(os.path.join(workdir, 'grader_output'), 'rb') as grader_output:
if program_output.read().strip() == grader_output.read().strip():
final_verdict = JobVerdict.accepted
else:
final_verdict = JobVerdict.wrong_answer
result.verdict = final_verdict
return result