mirror of https://github.com/EasyCTF/librectf
361 lines
12 KiB
Python
361 lines
12 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
|