300 lines
11 KiB
Python
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
|