diff --git a/Checker/urls.py b/Checker/urls.py index bbf4279..8af7bed 100644 --- a/Checker/urls.py +++ b/Checker/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path("status", views.status), path("available", views.available), path("get_dynamic", views.get_dynamic), - path("set_result", views.set_result), - path("current_test", views.current_test), + path("save_solution", views.save_solution), + path("save_progress", views.save_progress), + path("notify", views.notify) ] diff --git a/Checker/views.py b/Checker/views.py index c697731..9cfaa71 100644 --- a/Checker/views.py +++ b/Checker/views.py @@ -9,8 +9,9 @@ from django.utils import timezone from Checker.models import Checker from FileStorage.sync import synchronized_method -from Main.models import Solution, SolutionFile, ExtraFile +from Main.models import Solution, SolutionFile, ExtraFile, Progress from SprintLib.utils import generate_token +from SprintLib.queue import notify as notification def get_dynamic(request): @@ -50,7 +51,7 @@ def available(request): with TemporaryDirectory() as tempdir: with ZipFile(join(tempdir, "package.zip"), 'w') as zip_file: for sf in SolutionFile.objects.filter(solution=solution): - zip_file.writestr(sf.path, sf.bytes) + zip_file.writestr(join('solution', sf.path), sf.bytes) for ef in ExtraFile.objects.filter(task=solution.task): zip_file.writestr(ef.filename, ef.bytes) response = HttpResponse(open(join(tempdir, 'package.zip'), 'rb').read(), content_type='application/octet-stream', status=201) @@ -62,33 +63,61 @@ def available(request): return JsonResponse({"status": "incorrect token"}, status=403) -def set_result(request): +def save_solution(request): try: checker = Checker.objects.get(dynamic_token=request.GET['token']) solution = Solution.objects.get(id=request.GET['solution_id']) result = request.GET['result'] + test = request.GET.get("test") + extras = request.GET.get('extras') if checker.set != solution.set: return JsonResponse({"status": "incorrect solution"}, status=403) solution.result = result + solution.test = test + solution.extras = extras if result == 'OK': solution.test = None solution.save() - checker.testing_solution = None - checker.save() + if not result.startswith('Testing'): + checker.testing_solution = None + checker.save() return JsonResponse({"status": True}) except ObjectDoesNotExist: return JsonResponse({"status": "incorrect token"}, status=403) -def current_test(request): +def notify(request): try: checker = Checker.objects.get(dynamic_token=request.GET['token']) solution = Solution.objects.get(id=request.GET['solution_id']) if checker.set != solution.set: return JsonResponse({"status": "incorrect solution"}, status=403) - test = int(request.GET['test']) - solution.test = test - solution.save() + notification( + solution.user, + "solution_result", + f"Задача: {solution.task.name}\n" + f"Результат: {solution.result}\n" + f"Очки решения: {Progress.by_solution(solution).score}\n" + f"Текущий рейтинг: {solution.user.userinfo.rating}") + return JsonResponse({"status": True}) + except ObjectDoesNotExist: + return JsonResponse({"status": "incorrect token"}, status=403) + + +def save_progress(request): + try: + checker = Checker.objects.get(dynamic_token=request.GET['token']) + solution = Solution.objects.get(id=request.GET['solution_id']) + if checker.set != solution.set: + return JsonResponse({"status": "incorrect solution"}, status=403) + progress = Progress.objects.get( + user=solution.user, task=solution.task + ) + if progress.finished_time is None: + progress.finished_time = solution.time_sent + progress.finished = True + progress.save() + progress.increment_rating() return JsonResponse({"status": True}) except ObjectDoesNotExist: return JsonResponse({"status": "incorrect token"}, status=403) diff --git a/CheckerExecutor/Dockerfile b/CheckerExecutor/Dockerfile deleted file mode 100644 index 24557ac..0000000 --- a/CheckerExecutor/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM docker:dind - -RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python -RUN python3 -m ensurepip -RUN apk update && apk add postgresql-dev gcc python3-dev musl-dev jpeg-dev zlib-dev libjpeg -RUN pip3 install --no-cache --upgrade pip setuptools -RUN addgroup -S docker - -ENV PYTHONUNBUFFERED 1 -RUN mkdir -p /usr/src/app/ -WORKDIR /usr/src/app/ - -RUN pip install requests - -COPY . /usr/src/app/ - -CMD ["python", "main.py"] diff --git a/CheckerExecutor/__init__.py b/CheckerExecutor/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/CheckerExecutor/language.py b/CheckerExecutor/language.py deleted file mode 100644 index 28d3145..0000000 --- a/CheckerExecutor/language.py +++ /dev/null @@ -1,64 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class Language: - id: int - name: str - work_name: str - file_type: str - logo_url: str - image: str - highlight: str - - def __str__(self): - return self.name - - -languages = [ - Language( - id=0, - name="Python3", - work_name="Python3", - file_type="py", - logo_url="https://entredatos.es/wp-content/uploads/2021/05/1200px-Python-logo-notext.svg.png", - image="python:3.6", - highlight="python", - ), - Language( - id=1, - name="Kotlin", - work_name="Kotlin", - file_type="kt", - logo_url="https://upload.wikimedia.org/wikipedia/commons/0/06/Kotlin_Icon.svg", - image="zenika/kotlin", - highlight="kotlin", - ), - Language( - id=2, - name="C++", - work_name="Cpp", - file_type="cpp", - logo_url="https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/ISO_C%2B%2B_Logo.svg/1822px-ISO_C%2B%2B_Logo.svg.png", - image="gcc", - highlight="cpp", - ), - Language( - id=3, - name="Java", - work_name="Java", - file_type="java", - logo_url="https://upload.wikimedia.org/wikipedia/ru/thumb/3/39/Java_logo.svg/1200px-Java_logo.svg.png", - image="openjdk", - highlight="java", - ), - Language( - id=4, - name="C#", - work_name="CSharp", - file_type="cs", - logo_url="https://cdn.worldvectorlogo.com/logos/c--4.svg", - image="mono", - highlight="csharp", - ), -] diff --git a/CheckerExecutor/main.py b/CheckerExecutor/main.py deleted file mode 100644 index dc5074e..0000000 --- a/CheckerExecutor/main.py +++ /dev/null @@ -1,84 +0,0 @@ -from multiprocessing import Process -from os import getenv, mkdir -from os.path import join, exists -from shutil import rmtree -from tempfile import TemporaryDirectory -from time import sleep -from zipfile import ZipFile - -from requests import get - -from language import languages -from testers import * - -host = "http://dev.sprinthub.ru/" - - -def process_solution(path, data, language_id, solution_id, timeout, token, host): - with open(join(path, "package.zip"), 'wb') as fs: - fs.write(data) - with ZipFile(join(path, "package.zip"), 'r') as zip_ref: - zip_ref.extractall(path) - language = languages[language_id] - try: - result = eval(language.work_name + "Tester")(path, solution_id, language_id, timeout, token, host).execute() - except Exception as e: - print(str(e)) - result = "TE" - return result - - -def poll(token): - correct_token = True - while correct_token: - code = get(f"{host}checker/status", params={"token": token}).status_code - if code != 200: - correct_token = False - else: - sleep(2) - - -def main(): - request = get(f"{host}checker/get_dynamic", params={"token": getenv("TOKEN")}) - if request.status_code != 200: - print("Error happened: " + request.json()['status']) - exit(1) - dynamic_token = request.json()['token'] - p = Process(target=poll, args=(dynamic_token,)) - p.start() - while True: - data = get(f"{host}checker/available", params={"token": dynamic_token}) - if data.status_code == 200: - sleep(2) - continue - elif data.status_code == 201: - tempdir = "/var/tmp/solution/" - try: - mkdir(tempdir) - result = process_solution( - tempdir, - data.content, - int(data.headers['language_id']), - int(data.headers['solution_id']), - int(data.headers['timeout']), - dynamic_token, - host - ) - get(f"{host}checker/set_result", params={ - "token": dynamic_token, - "solution_id": data.headers['solution_id'], - "result": result - }) - finally: - if exists(tempdir): - rmtree(tempdir) - elif data.status_code == 403: - print("token removed") - exit(1) - else: - print("unknown status") - exit(1) - - -if __name__ == '__main__': - main() diff --git a/CheckerExecutor/testers/BaseTester.py b/CheckerExecutor/testers/BaseTester.py deleted file mode 100644 index 8a273f0..0000000 --- a/CheckerExecutor/testers/BaseTester.py +++ /dev/null @@ -1,120 +0,0 @@ -from os import listdir -from os.path import join, exists -from subprocess import call, TimeoutExpired - -from SprintLib.language import * -from requests import get - - -class TestException(Exception): - pass - - -class BaseTester: - working_directory = "app" - checker_code = None - - def exec_command(self, command, working_directory="app", timeout=None): - return call( - f'docker exec -i solution sh -c "cd {working_directory} && {command}"', - shell=True, - timeout=timeout, - ) - - def before_test(self): - files = [ - file - for file in listdir(self.path) - if file.endswith("." + self.language.file_type) - ] - code = self.exec_command( - f'{self.build_command} {" ".join(files)}', - working_directory=self.working_directory, - ) - if code != 0: - raise TestException("CE") - - def test(self, filename): - print('testing ' + filename) - code = self.exec_command( - f"< {filename} {self.command} > output.txt", - timeout=self.timeout / 1000, - ) - if code != 0: - raise TestException("RE") - result = open(join(self.path, "output.txt"), "r").read().strip().replace('\r\n', '\n') - print("got result", result) - if self.checker_code is not None: - print('using checker') - with open(join(self.path, 'expected.txt'), 'w') as fs: - fs.write(self.predicted) - with open(join(self.path, 'checker.py'), 'w') as fs: - fs.write(self.checker_code) - code = call(f'docker exec -i checker sh -c "cd app && python checker.py"', shell=True, timeout=1) - if code != 0: - raise TestException("WA") - else: - print('using simple check') - if result != self.predicted: - print('incorrect') - raise TestException("WA") - print('correct') - - def after_test(self): - pass - - @property - def command(self): - return "./executable.exe" - - @property - def build_command(self): - return "" - - @property - def path(self): - return self._path - - @property - def language(self): - return languages[self.language_id] - - def __init__(self, path, solution_id, language_id, timeout, token, host): - self.solution_id = solution_id - self._path = path - self.language_id = language_id - self.timeout = timeout - self.token = token - self.host = host - - def execute(self): - docker_command = f"docker run --name solution --volume={self.path}:/{self.working_directory} -t -d {self.language.image}" - print(docker_command) - call(docker_command, shell=True) - checker = join(self.path, 'checker.py') - if exists(checker): - self.checker_code = open(checker, 'r').read() - call(f"docker run --name checker --volume={self.path}:/app -t -d python:3.6", shell=True) - print("Container created") - result = None - try: - self.before_test() - print("before test finished") - for file in listdir(self.path): - if not file.endswith(".a") and exists(join(self.path, file + '.a')): - self.predicted = open(join(self.path, file + '.a'), 'r').read().strip().replace('\r\n', '\n') - print('predicted:', self.predicted) - get(f"{self.host}checker/current_test", params={"token": self.token, 'test': file, 'solution_id': self.solution_id}) - self.test(file) - self.after_test() - result = "OK" - except TestException as e: - result = str(e) - except TimeoutExpired: - result = "TL" - except Exception as e: - print(str(e)) - result = "TE" - call(f"docker rm --force solution", shell=True) - call(f"docker rm --force checker", shell=True) - return result diff --git a/CheckerExecutor/testers/CSharpTester.py b/CheckerExecutor/testers/CSharpTester.py deleted file mode 100644 index cff4465..0000000 --- a/CheckerExecutor/testers/CSharpTester.py +++ /dev/null @@ -1,11 +0,0 @@ -from .BaseTester import BaseTester - - -class CSharpTester(BaseTester): - @property - def build_command(self): - return "csc /out:executable.exe" - - @property - def command(self): - return "mono executable.exe" diff --git a/CheckerExecutor/testers/CppTester.py b/CheckerExecutor/testers/CppTester.py deleted file mode 100644 index d6b627c..0000000 --- a/CheckerExecutor/testers/CppTester.py +++ /dev/null @@ -1,7 +0,0 @@ -from .BaseTester import BaseTester - - -class CppTester(BaseTester): - @property - def build_command(self): - return "g++ -o executable.exe" diff --git a/CheckerExecutor/testers/GoTester.py b/CheckerExecutor/testers/GoTester.py deleted file mode 100644 index ae697ae..0000000 --- a/CheckerExecutor/testers/GoTester.py +++ /dev/null @@ -1,8 +0,0 @@ -from .BaseTester import BaseTester - - -class GoTester(BaseTester): - working_directory = "../app" - - def build_command(self): - return "go build -o executable.exe" diff --git a/CheckerExecutor/testers/JavaTester.py b/CheckerExecutor/testers/JavaTester.py deleted file mode 100644 index 64b2fda..0000000 --- a/CheckerExecutor/testers/JavaTester.py +++ /dev/null @@ -1,27 +0,0 @@ -from os import listdir - -from .BaseTester import BaseTester, TestException - - -class JavaTester(BaseTester): - _executable = None - - def before_test(self): - files = [ - file - for file in listdir(self.path) - if file.endswith(".java") - ] - code = self.exec_command(f"javac {' '.join(files)}") - if code != 0: - raise TestException("CE") - for file in listdir(self.path): - if file.endswith(".class"): - self._executable = file.rstrip(".class") - break - if self._executable is None: - raise TestException("TE") - - @property - def command(self): - return f"java -classpath . {self._executable}" diff --git a/CheckerExecutor/testers/KotlinTester.py b/CheckerExecutor/testers/KotlinTester.py deleted file mode 100644 index 4921379..0000000 --- a/CheckerExecutor/testers/KotlinTester.py +++ /dev/null @@ -1,21 +0,0 @@ -from os import listdir - -from .BaseTester import BaseTester, TestException - - -class KotlinTester(BaseTester): - def before_test(self): - files = [ - file - for file in listdir(self.path) - if file.endswith(".kt") - ] - code = self.exec_command( - f'kotlinc {" ".join(files)} -include-runtime -d solution.jar' - ) - if code != 0: - raise TestException("CE") - - @property - def command(self): - return "java -jar solution.jar" diff --git a/CheckerExecutor/testers/Python3Tester.py b/CheckerExecutor/testers/Python3Tester.py deleted file mode 100644 index ec2b9f1..0000000 --- a/CheckerExecutor/testers/Python3Tester.py +++ /dev/null @@ -1,19 +0,0 @@ -from os import listdir - -from .BaseTester import BaseTester, TestException - - -class Python3Tester(BaseTester): - file = None - - def before_test(self): - for file in listdir(self.path): - if file.endswith(".py") and file != 'checker.py': - self.file = file - break - if self.file is None: - raise TestException("TE") - - @property - def command(self): - return f"python3 {self.file}" diff --git a/CheckerExecutor/testers/__init__.py b/CheckerExecutor/testers/__init__.py deleted file mode 100644 index cd44294..0000000 --- a/CheckerExecutor/testers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .BaseTester import BaseTester -from .Python3Tester import Python3Tester -from .CppTester import CppTester -from .GoTester import GoTester -from .JavaTester import JavaTester -from .CSharpTester import CSharpTester -from .KotlinTester import KotlinTester diff --git a/Dockerfile b/Dockerfile index f3d1209..a8cf3b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,3 +14,5 @@ WORKDIR /usr/src/app/ COPY . /usr/src/app/ RUN pip3 install -r requirements.txt + +CMD ["./manage.py", "checker"] diff --git a/Main/models/mixins.py b/Main/models/mixins.py index d877b26..0bacdbf 100644 --- a/Main/models/mixins.py +++ b/Main/models/mixins.py @@ -5,10 +5,18 @@ from SprintLib.utils import get_bytes, write_bytes, delete_file class FileStorageMixin: - @cached_property + _bytes = None + + @property def bytes(self): + if self._bytes is not None: + return self._bytes return get_bytes(self.fs_id) + @bytes.setter + def bytes(self, value): + self._bytes = value + @cached_property def text(self): try: diff --git a/Main/models/solution.py b/Main/models/solution.py index e14421b..69ddd4d 100644 --- a/Main/models/solution.py +++ b/Main/models/solution.py @@ -22,6 +22,8 @@ class Solution(models.Model): set = models.ForeignKey(Set, null=True, blank=True, on_delete=models.SET_NULL) extras = models.JSONField(default=dict) + _solutionfiles = None + class Meta: indexes = [ models.Index(fields=['task', 'user', '-time_sent']), @@ -29,6 +31,16 @@ class Solution(models.Model): models.Index(fields=['set', '-time_sent']), ] + @property + def solutionfiles(self): + if self._solutionfiles is not None: + return self._solutionfiles + return SolutionFile.objects.filter(solution=self) + + @solutionfiles.setter + def solutionfiles(self, value): + self._solutionfiles = value + @cached_property def settask(self): return SetTask.objects.filter(set=self.set, task=self.task).first() @@ -83,10 +95,6 @@ class Solution(models.Model): return "info" return "danger" - @property - def volume_directory(self): - return "/sprint-data/worker/" + str(self.id) - def exec_command(self, command, working_directory="app", timeout=None): return call( f'docker exec -i solution_{self.id} sh -c "cd {working_directory} && {command}"', diff --git a/Main/models/task.py b/Main/models/task.py index dcf6c3a..baf58fa 100644 --- a/Main/models/task.py +++ b/Main/models/task.py @@ -1,8 +1,8 @@ +from cached_property import cached_property from django.contrib.postgres.fields import ArrayField from django.db import models from django.contrib.auth.models import User from django.db.models import JSONField -from django.utils import timezone from Main.models.dump import Dump from Main.models.extrafile import ExtraFile @@ -22,6 +22,8 @@ class Task(models.Model): allow_sharing = models.BooleanField(default=False) changes = JSONField(default=list) + _extrafiles = None + def __str__(self): return self.name @@ -35,7 +37,32 @@ class Task(models.Model): @property def tests(self): - return ExtraFile.objects.filter(task=self, is_test=True).order_by('filename') + for file in sorted(self.extrafiles, key=lambda x: x.filename): + if file.filename.isnumeric() or file.filename.endswith('.a') and file.filename[:-2].isnumeric(): + yield file + + @property + def extrafiles(self): + if self._extrafiles is not None: + return self._extrafiles + return ExtraFile.objects.filter(task=self) + + @extrafiles.setter + def extrafiles(self, value): + self._extrafiles = value + + @property + def dockerfiles(self): + for file in self.extrafiles: + if file.filename.startswith('Dockerfile_'): + yield file + + @cached_property + def checkerfile(self): + for file in self.extrafiles: + if file.filename == 'extrafile.py': + return file + return None @property def tests_count(self): @@ -44,7 +71,7 @@ class Task(models.Model): @property def samples(self): data = [] - for test in self.tests.order_by("test_number"): + for test in self.tests: if test.is_sample and test.readable: data.append({"input": test.text, "output": test.answer.text}) count = 1 diff --git a/Main/views/SendCodeView.py b/Main/views/SendCodeView.py index 92138a4..5365b5e 100644 --- a/Main/views/SendCodeView.py +++ b/Main/views/SendCodeView.py @@ -20,7 +20,7 @@ class SendCodeView(BaseView): "message": "Пользователя с таким именем не существует", } code = randrange(10000, 100000) - print(code) + print(f"Отправлен код для {username}", code) user.userinfo.code = code user.userinfo.save() notify(user, "any", "Код для входа в сервис: " + str(code)) diff --git a/SprintLib/testers/BaseTester.py b/SprintLib/testers/BaseTester.py index 05a7014..06e7fca 100644 --- a/SprintLib/testers/BaseTester.py +++ b/SprintLib/testers/BaseTester.py @@ -3,11 +3,10 @@ from os.path import join, exists from subprocess import call, TimeoutExpired from tempfile import TemporaryDirectory -from SprintLib.queue import notify, send_to_queue -from Main.models import ExtraFile, SolutionFile from Main.models.progress import Progress from Sprint.settings import CONSTS -from SprintLib.utils import get_bytes, Timer +from SprintLib.queue import notify, send_to_queue +from SprintLib.utils import Timer class TestException(Exception): @@ -70,21 +69,21 @@ class BaseTester: return "" def call(self, command): - return call(f'cd {self.path} && {command}', shell=True) + print(f"Executing command: {command}") + if exists(self.path): + return call(f'cd {self.path} && {command}', shell=True) + else: + return call(command, shell=True) def __init__(self, solution): self.solution = solution - def set_test(self, num): - self.solution.result = CONSTS["testing_status"] + f"({num})" + def save_solution(self): self.solution.save() def _setup_networking(self): - self.dockerfiles = sorted( - list(ExtraFile.objects.filter(filename__startswith="Dockerfile_", readable=True, task=self.solution.task)), - key=lambda x: x.filename) self.call(f"docker network create solution_network_{self.solution.id}") - for file in self.dockerfiles: + for file in self.solution.task.dockerfiles: add_name = file.filename[11:] with open(join(self.path, 'Dockerfile'), 'w') as fs: fs.write(file.text) @@ -97,11 +96,41 @@ class BaseTester: print('run command', run_command) self.call(run_command) + def notify(self): + self.solution.user.userinfo.refresh_from_db() + notify( + self.solution.user, + "solution_result", + f"Задача: {self.solution.task.name}\n" + f"Результат: {self.solution.result}\n" + f"Очки решения: {Progress.by_solution(self.solution).score}\n" + f"Текущий рейтинг: {self.solution.user.userinfo.rating}") + + def cleanup(self): + self.solution.save() + send_to_queue("cleaner", {"type": "container", "name": f"solution_{self.solution.id}"}) + send_to_queue("cleaner", {"type": "container", "name": f"solution_{self.solution.id}_checker"}) + for file in self.solution.task.dockerfiles: + add_name = file.filename[11:] + send_to_queue("cleaner", {"type": "container", "name": f"solution_container_{self.solution.id}_{add_name}"}) + send_to_queue("cleaner", {"type": "image", "name": f"solution_image_{self.solution.id}_{add_name}"}) + send_to_queue("cleaner", {"type": "network", "name": f"solution_network_{self.solution.id}"}) + + def save_progress(self): + progress = Progress.objects.get( + user=self.solution.user, task=self.solution.task + ) + if progress.finished_time is None: + progress.finished_time = self.solution.time_sent + progress.finished = True + progress.save() + progress.increment_rating() + def execute(self): self.solution.result = CONSTS["testing_status"] - self.solution.save() + self.save_solution() with TemporaryDirectory(dir='/tmp') as self.path: - for file in SolutionFile.objects.filter(solution=self.solution): + for file in self.solution.solutionfiles: dirs = file.path.split("/") for i in range(len(dirs) - 1): name = join( @@ -112,19 +141,19 @@ class BaseTester: with open( join(self.path, file.path), "wb" ) as fs: - fs.write(get_bytes(file.fs_id).replace(b"\r\n", b"\n")) - for file in ExtraFile.objects.filter(task=self.solution.task): + fs.write(file.bytes.replace(b"\r\n", b"\n")) + for file in self.solution.task.extrafiles: with open( join(self.path, file.filename), 'wb' ) as fs: - bts = get_bytes(file.fs_id) + bts = file.bytes fs.write(bts) print("Files copied") self._setup_networking() docker_command = f"docker run --network solution_network_{self.solution.id} --name solution_{self.solution.id} --volume={self.path}:/{self.working_directory} -t -d {self.solution.language.image}" print(docker_command) call(docker_command, shell=True) - checker = ExtraFile.objects.filter(task=self.solution.task, filename='checker.py').first() + checker = self.solution.task.checkerfile if checker is not None: self.checker_code = checker.text call(f"docker run --network solution_network_{self.solution.id} --name solution_{self.solution.id}_checker --volume={self.path}:/app -t -d python:3.6", shell=True) @@ -134,13 +163,11 @@ class BaseTester: print("before test finished") for test in self.solution.task.tests: if not test.filename.endswith(".a"): - self.predicted = ExtraFile.objects.get( - task=self.solution.task, filename=test.filename + ".a" - ).text.strip().replace('\r\n', '\n') + self.predicted = open(join(self.path, test.filename + '.a'), 'r').read().strip().replace('\r\n', '\n') print('predicted:', self.predicted) self.solution.test = int(test.filename) self.solution.extras[test.filename] = {'predicted': self.predicted, 'output': ''} - self.solution.save() + self.save_solution() try: self.test(test.filename) finally: @@ -149,17 +176,12 @@ class BaseTester: self.solution.extras[test.filename]['output'] = open(join(self.path, 'output.txt'), 'r').read() except UnicodeDecodeError: self.solution.extras[test.filename]['output'] = '' + self.save_solution() self.after_test() self.solution.result = CONSTS["ok_status"] self.solution.test = None - progress = Progress.objects.get( - user=self.solution.user, task=self.solution.task - ) - if progress.finished_time is None: - progress.finished_time = self.solution.time_sent - progress.finished = True - progress.save() - progress.increment_rating() + self.save_solution() + self.save_progress() except TestException as e: self.solution.result = str(e) except TimeoutExpired: @@ -167,19 +189,6 @@ class BaseTester: except Exception as e: self.solution.result = "TE" print(e) - self.solution.save() - send_to_queue("cleaner", {"type": "container", "name": f"solution_{self.solution.id}"}) - send_to_queue("cleaner", {"type": "container", "name": f"solution_{self.solution.id}_checker"}) - for file in self.dockerfiles: - add_name = file.filename[11:] - send_to_queue("cleaner", {"type": "container", "name": f"solution_container_{self.solution.id}_{add_name}"}) - send_to_queue("cleaner", {"type": "image", "name": f"solution_image_{self.solution.id}_{add_name}"}) - send_to_queue("cleaner", {"type": "network", "name": f"solution_network_{self.solution.id}"}) - self.solution.user.userinfo.refresh_from_db() - notify( - self.solution.user, - "solution_result", - f"Задача: {self.solution.task.name}\n" - f"Результат: {self.solution.result}\n" - f"Очки решения: {Progress.by_solution(self.solution).score}\n" - f"Текущий рейтинг: {self.solution.user.userinfo.rating}") + self.save_solution() + self.cleanup() + self.notify() diff --git a/SprintLib/testers/DistantTester.py b/SprintLib/testers/DistantTester.py new file mode 100644 index 0000000..0659384 --- /dev/null +++ b/SprintLib/testers/DistantTester.py @@ -0,0 +1,41 @@ +import json + +from requests import get + +from SprintLib.testers import BaseTester + + +class DistantTester(BaseTester): + host = "" + token = "" + + def request(self, method, params=None): + if params is None: + params = {} + return get(f'{self.host}checker/{method}', params={**{ + "token": self.token, + "solution_id": self.solution.id, + }, **params}) + + def save_solution(self): + self.request("save_solution", { + "test": self.solution.test, + "result": self.solution.result, + "extras": json.dumps(self.solution.extras) + }) + + def notify(self): + self.request("notify") + + def cleanup(self): + self.save_solution() + self.call(f"docker rm --force solution_{self.solution.id}") + self.call(f"docker rm --force solution_{self.solution.id}_checker") + for file in self.solution.task.dockerfiles: + add_name = file.filename[11:] + self.call(f"docker rm --force solution_container_{self.solution.id}_{add_name}") + self.call(f"docker image rm solution_image_{self.solution.id}_{add_name}") + self.call(f"docker network rm solution_network_{self.solution.id}") + + def save_progress(self): + self.request("save_progress") diff --git a/SprintLib/testers/Python3Tester.py b/SprintLib/testers/Python3Tester.py index ac15f91..4698ff1 100644 --- a/SprintLib/testers/Python3Tester.py +++ b/SprintLib/testers/Python3Tester.py @@ -7,9 +7,8 @@ class Python3Tester(BaseTester): file = None def before_test(self): - no_files = [file.filename for file in self.solution.task.files] for file in listdir(self.path): - if file.endswith(".py") and file not in no_files: + if file == 'solution.py': self.file = file break if self.file is None: diff --git a/SprintLib/testers/__init__.py b/SprintLib/testers/__init__.py index bc67c92..9d245dd 100644 --- a/SprintLib/testers/__init__.py +++ b/SprintLib/testers/__init__.py @@ -6,3 +6,4 @@ from .JavaTester import JavaTester from .CSharpTester import CSharpTester from .KotlinTester import KotlinTester from .SwiftTester import SwiftTester +from .DistantTester import DistantTester diff --git a/daemons/management/commands/checker.py b/daemons/management/commands/checker.py new file mode 100644 index 0000000..f11c717 --- /dev/null +++ b/daemons/management/commands/checker.py @@ -0,0 +1,102 @@ +from os import getenv, remove, listdir, walk +from os.path import join, isfile +from tempfile import TemporaryDirectory +from threading import Thread +from time import sleep +from zipfile import ZipFile + +from django.core.management import BaseCommand +from requests import get + +from Main.models import Solution, Task, ExtraFile, SolutionFile +from SprintLib.language import languages +from SprintLib.testers import * + + +host = 'http://192.168.0.146:8000/' + + +class Command(BaseCommand): + help = "Tests solution" + + def poll(self, token): + correct_token = True + while correct_token: + code = get(f"{host}checker/status", params={"token": token}).status_code + if code != 200: + correct_token = False + else: + sleep(2) + + def handle(self, *args, **options): + print("Starting checker") + request = get(f"{host}checker/get_dynamic", params={"token": getenv("TOKEN")}) + if request.status_code != 200: + print("Error happened: " + request.json()['status']) + exit(1) + print("Got dynamic token") + dynamic_token = request.json()['token'] + p = Thread(target=self.poll, args=(dynamic_token,)) + p.start() + while True: + data = get(f"{host}checker/available", params={"token": dynamic_token}) + if data.status_code == 200: + sleep(2) + continue + elif data.status_code == 201: + solution = self.create_solution(data) + print("handled solution", solution.id) + tester_class = eval(solution.language.work_name + "Tester") + + class LocalTester(DistantTester, tester_class): + ... + + tester = LocalTester(solution) + tester.host = host + tester.token = dynamic_token + try: + tester.execute() + except Exception as e: + print(e) + solution.result = "TE" + tester.save_solution() + elif data.status_code == 403: + print("token removed") + exit(1) + else: + print("unknown status") + exit(1) + + def create_solution(self, data): + with TemporaryDirectory(dir='/tmp') as path: + with open(join(path, "package.zip"), 'wb') as fs: + fs.write(data.content) + with ZipFile(join(path, "package.zip"), 'r') as zip_ref: + zip_ref.extractall(path) + remove(join(path, "package.zip")) + + solution = Solution( + id=int(data.headers['solution_id']), + language_id=int(data.headers['language_id']), + task=Task( + time_limit=int(data.headers['timeout']), + ) + ) + + solution.task.extrafiles = [ExtraFile( + filename=file, + task=solution.task + ) for file in listdir(path) if isfile(join(path, file))] + for file in solution.task.extrafiles: + file.bytes = open(join(path, file.filename), 'rb').read() + solution.solutionfiles = [ + SolutionFile( + path=join(directory, file)[len(join(path, 'solution')) + 1:], + solution=solution, + ) + for directory, _, files in walk(join(path, 'solution')) for file in files + ] + for file in solution.solutionfiles: + file.bytes = open(join(path, 'solution', file.path), 'rb').read() + + return solution diff --git a/daemons/management/commands/receive.py b/daemons/management/commands/receive.py index bea0a8d..20aa437 100644 --- a/daemons/management/commands/receive.py +++ b/daemons/management/commands/receive.py @@ -1,6 +1,3 @@ -from os.path import join, exists -from shutil import rmtree - from Main.models import Solution from SprintLib.queue import MessagingSupport from SprintLib.testers import * @@ -14,8 +11,9 @@ class Command(MessagingSupport): id = payload['id'] print(f"Received id {id}") solution = Solution.objects.get(id=id) + tester = eval(solution.language.work_name + "Tester")(solution) try: - eval(solution.language.work_name + "Tester")(solution).execute() + tester.execute() except Exception as e: print(e) solution.result = "TE"