From 0feded8b125577142590f6f93165c1ee10d85b01 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 10 Sep 2024 19:26:08 -0400 Subject: [PATCH 01/15] Bugfixes to get a working dev env --- docs/source/contributing/setup.rst | 36 +++++++++++-- pyproject.toml | 4 ++ .../create_debug_users.py | 0 scripts/create_wrappers.py | 54 +++++++++++++++++++ scripts/sample_grader.py | 18 +++++++ tin/apps/submissions/tasks.py | 11 ++-- 6 files changed, 114 insertions(+), 9 deletions(-) rename create_debug_users.py => scripts/create_debug_users.py (100%) create mode 100755 scripts/create_wrappers.py create mode 100644 scripts/sample_grader.py diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index b3324b34..5f533630 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -3,13 +3,16 @@ Setting up a development environment ------------------------------------ +Basic Setup +~~~~~~~~~~~ + First, you will need to install the following: * ``python`` * ``pipenv`` * ``git`` -You will also need a Github account. +You will also need a GitHub account. First, `fork `_ tin. Then you can clone tin onto your computer with @@ -30,15 +33,40 @@ After that, install dependencies and follow standard django procedures .. code-block:: bash pipenv install --dev - python3 manage.py migrate - python3 create_debug_users.py + pipenv run python3 manage.py migrate + pipenv run python3 scripts/create_debug_users.py Now you're all set! Try running the development server .. code-block:: bash - python3 manage.py runserver + pipenv run python3 manage.py runserver Head on over to `http://127.0.0.1:8000 `_, and login as ``admin`` and the password you just entered. + +Submissions +~~~~~~~~~~~ + +In order to actually create a submission, there are some more steps. First, +you'll need to install `redis `_. This is platform dependent: +for example, on macOS you can use `homebrew `_ and run ``brew install redis``, +but on archlinux you'll need to run ``pacman -Syu redis``. + +You'll also need to run some scripts to emulate the sandboxing process that goes on in production. +Run the following script:: + + pipenv run python3 scripts/create_wrappers.py + +After that, you'll want to start up the development server and create a course, +and an assignment in the course. After saving the assignment, you can hit "Upload grader" +to add a grader - the simplest example of a grader is located in ``scripts/sample_grader.py``. + +Finally, before creating a submission, you'll need to start the celery worker. This can be done +by running the following command in a separate terminal:: + + pipenv run celery -A tin worker --loglevel=info + +Now you can try making a submission, and as long as your submission doesn't throw an error you +should get a 100%! Congrats on your brand new 5.0 GPA! diff --git a/pyproject.toml b/pyproject.toml index c89edb08..e70e8175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,6 +174,10 @@ extend-ignore-names = [ "FBT", ] +"scripts/*" = [ + "INP001", +] + [tool.ruff.format] docstring-code-format = true line-ending = "lf" diff --git a/create_debug_users.py b/scripts/create_debug_users.py similarity index 100% rename from create_debug_users.py rename to scripts/create_debug_users.py diff --git a/scripts/create_wrappers.py b/scripts/create_wrappers.py new file mode 100755 index 00000000..8ad2feee --- /dev/null +++ b/scripts/create_wrappers.py @@ -0,0 +1,54 @@ +#!/bin/env python3 +from __future__ import annotations + +import argparse +from pathlib import Path + +TIN_ROOT = Path(__file__).parent.parent / "tin" + +PYTHON_WRAPPER = """ +import subprocess +import sys + +submission = "{submission_path}" +subprocess.run([sys.executable, submission]) +""" + +# TODO +JAVA_WRAPPER = """""" + + +def create_wrappers(file_name: str, wrapper_text: str) -> None: + """Create sample Tin wrapper scripts. + + These are supposed to be used for sandboxing, but + for debug purposes we can just do nothing! + """ + wrappers = TIN_ROOT / "sandboxing" / "wrappers" + # we need both because in some cases bwrap exists on the parent system + for dir in ["sandboxed", "testing"]: + wrapper = wrappers / dir + wrapper.mkdir(parents=True, exist_ok=True) + + path = wrapper.joinpath(f"{file_name}.txt") + # prevent possible overwriting + if path.exists() and not args.force: + print(f"Skipping file {path}") + else: + if args.force: + print(f"Overwriting file {path}") + path.write_text(wrapper_text) + + +def cli() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create sample wrapper scripts to run Tin submissions." + ) + parser.add_argument("--force", action="store_true", help="overwrite existing wrapper files.") + return parser.parse_args() + + +if __name__ == "__main__": + args = cli() + create_wrappers("P", PYTHON_WRAPPER) + create_wrappers("J", JAVA_WRAPPER) diff --git a/scripts/sample_grader.py b/scripts/sample_grader.py new file mode 100644 index 00000000..e57f4afc --- /dev/null +++ b/scripts/sample_grader.py @@ -0,0 +1,18 @@ +"""The (second) simplest grader. + +This runs the student submission, and without checking the output +gives them a 100%. It also errors if the student submission crashes. +""" + +from __future__ import annotations + +import subprocess +import sys + +# this will error if the student submission errors +process = subprocess.run( + [sys.executable, sys.argv[1]], + check=True, +) + +print("Score: 100%") diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index 50f56156..ecfe5526 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -62,11 +62,12 @@ def run_submission(submission_id): logger.error("Cannot run processes: %s", e) raise FileNotFoundError from e - python_exe = ( - os.path.join(submission.assignment.venv.path, "bin", "python") - if submission.assignment.venv_fully_created - else "/usr/bin/python3.10" - ) + if submission.assignment.venv_fully_created: + python_exe = os.path.join(submission.assignment.venv.path, "bin", "python") + elif settings.DEBUG: + python_exe = shutil.which("python") or shutil.which("python3") + else: + python_exe = "/usr/bin/python3.10" if not settings.DEBUG or shutil.which("bwrap") is not None: folder_name = "sandboxed" From 29470ca352458a2b5e5e99b22e064bb5b0924bba Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 10 Sep 2024 19:29:17 -0400 Subject: [PATCH 02/15] Remove platform-dependent yap --- docs/source/contributing/setup.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 5f533630..7075b0e7 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -50,9 +50,7 @@ Submissions ~~~~~~~~~~~ In order to actually create a submission, there are some more steps. First, -you'll need to install `redis `_. This is platform dependent: -for example, on macOS you can use `homebrew `_ and run ``brew install redis``, -but on archlinux you'll need to run ``pacman -Syu redis``. +you'll need to install `redis `_. You'll also need to run some scripts to emulate the sandboxing process that goes on in production. Run the following script:: From 17654d720e37834f0a25ca378d7270d07d05d718 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 10 Sep 2024 19:31:14 -0400 Subject: [PATCH 03/15] pathlibify --- scripts/create_wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_wrappers.py b/scripts/create_wrappers.py index 8ad2feee..a01a2f5a 100755 --- a/scripts/create_wrappers.py +++ b/scripts/create_wrappers.py @@ -30,7 +30,7 @@ def create_wrappers(file_name: str, wrapper_text: str) -> None: wrapper = wrappers / dir wrapper.mkdir(parents=True, exist_ok=True) - path = wrapper.joinpath(f"{file_name}.txt") + path = wrapper / f"{file_name}.txt" # prevent possible overwriting if path.exists() and not args.force: print(f"Skipping file {path}") From 66647a5da29afb34451b8a262bf2a12938987364 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 10 Sep 2024 20:43:44 -0400 Subject: [PATCH 04/15] use sys.executable instead of or-ring shutil.which --- tin/apps/submissions/tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index ecfe5526..2a26b9f9 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -7,6 +7,7 @@ import shutil import signal import subprocess +import sys import time import traceback from decimal import Decimal @@ -65,8 +66,8 @@ def run_submission(submission_id): if submission.assignment.venv_fully_created: python_exe = os.path.join(submission.assignment.venv.path, "bin", "python") elif settings.DEBUG: - python_exe = shutil.which("python") or shutil.which("python3") - else: + python_exe = sys.executable + else: # pragma: no cover python_exe = "/usr/bin/python3.10" if not settings.DEBUG or shutil.which("bwrap") is not None: From 0d2428f2604b6d349d1bf15b956e06eaee59aad4 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Wed, 11 Sep 2024 11:34:28 -0400 Subject: [PATCH 05/15] Use formatting python instead of sys.executable --- scripts/create_wrappers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/create_wrappers.py b/scripts/create_wrappers.py index a01a2f5a..c72695a6 100755 --- a/scripts/create_wrappers.py +++ b/scripts/create_wrappers.py @@ -8,10 +8,9 @@ PYTHON_WRAPPER = """ import subprocess -import sys submission = "{submission_path}" -subprocess.run([sys.executable, submission]) +subprocess.run(["{python}", submission]) """ # TODO From cae2d8480e9da56958e35f57216b161c76e10777 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Wed, 11 Sep 2024 14:56:18 -0400 Subject: [PATCH 06/15] Make stuff more robust --- docs/source/contributing/setup.rst | 6 ++++++ scripts/create_wrappers.py | 17 +++++++++++++++-- scripts/sample_grader.py | 16 ++++++++-------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 7075b0e7..61d42810 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -68,3 +68,9 @@ by running the following command in a separate terminal:: Now you can try making a submission, and as long as your submission doesn't throw an error you should get a 100%! Congrats on your brand new 5.0 GPA! + +.. tip:: + + If you're on a Unix-like system, you can use the ``&`` operator to run the celery worker in the background:: + + pipenv run celery -A tin worker --loglevel=info & pipenv run python3 manage.py runserver diff --git a/scripts/create_wrappers.py b/scripts/create_wrappers.py index c72695a6..5d04fd9c 100755 --- a/scripts/create_wrappers.py +++ b/scripts/create_wrappers.py @@ -8,9 +8,22 @@ PYTHON_WRAPPER = """ import subprocess +import sys -submission = "{submission_path}" -subprocess.run(["{python}", submission]) +def main(): + output = subprocess.run( + ["{python}", "{submission_path}"], + check=False, + capture_output=True, + text=True, + ) + print(output.stdout) + print(output.stderr, file=sys.stderr) + return output.returncode + + +if __name__ == "__main__": + sys.exit(main()) """ # TODO diff --git a/scripts/sample_grader.py b/scripts/sample_grader.py index e57f4afc..50f0de51 100644 --- a/scripts/sample_grader.py +++ b/scripts/sample_grader.py @@ -1,7 +1,7 @@ """The (second) simplest grader. This runs the student submission, and without checking the output -gives them a 100%. It also errors if the student submission crashes. +gives them a 100%. However, if the submission crashes the student will get a 0% """ from __future__ import annotations @@ -9,10 +9,10 @@ import subprocess import sys -# this will error if the student submission errors -process = subprocess.run( - [sys.executable, sys.argv[1]], - check=True, -) - -print("Score: 100%") +process = subprocess.run([sys.executable, sys.argv[1]], check=False) +if process.returncode != 0: + print("Encountered exception for test case 0:") + print(process.stderr) + print("Score: 0%") +else: + print("Score: 100%") From 46db5f025c04de9826ada0fe40c814fcefa10a5b Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Thu, 12 Sep 2024 09:44:24 -0400 Subject: [PATCH 07/15] Update implementation of create_debug_users --- scripts/create_debug_users.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/scripts/create_debug_users.py b/scripts/create_debug_users.py index d61cb124..aeb25d3c 100755 --- a/scripts/create_debug_users.py +++ b/scripts/create_debug_users.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 + +from __future__ import annotations + import os -import subprocess -import sys +from getpass import getpass + +import django -if not __file__.endswith("shell.py"): - subprocess.call( - [ - sys.executable, - os.path.join(os.path.dirname(__file__), "manage.py"), - "shell", - "-c", - open(__file__).read(), - ] - ) - exit() +import tin.tests.create_users as users -from tin.tests.create_users import add_users_to_database +def main(): + # hide user password from showing on terminal + password = getpass("Enter password for all users: ") + users.add_users_to_database(password=password, verbose=True) -password = input("Enter password for all users: ") -add_users_to_database(password=password, verbose=True) +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tin.settings") + django.setup() + main() From bd017e0f79c0c1c6c68c104bab731abab780eb7e Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 8 Oct 2024 14:36:44 -0400 Subject: [PATCH 08/15] Move create_debug_users into a management command --- docs/source/contributing/setup.rst | 2 +- scripts/create_debug_users.py | 22 --------------- .../management/commands/create_debug_users.py | 28 +++++++++++++++++++ tin/tests/create_users.py | 4 +-- 4 files changed, 31 insertions(+), 25 deletions(-) delete mode 100755 scripts/create_debug_users.py create mode 100644 tin/apps/users/management/commands/create_debug_users.py diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 61d42810..36192406 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -34,7 +34,7 @@ After that, install dependencies and follow standard django procedures pipenv install --dev pipenv run python3 manage.py migrate - pipenv run python3 scripts/create_debug_users.py + pipenv run python3 manage.py create_debug_users Now you're all set! Try running the development server diff --git a/scripts/create_debug_users.py b/scripts/create_debug_users.py deleted file mode 100755 index aeb25d3c..00000000 --- a/scripts/create_debug_users.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import os -from getpass import getpass - -import django - -import tin.tests.create_users as users - - -def main(): - # hide user password from showing on terminal - password = getpass("Enter password for all users: ") - users.add_users_to_database(password=password, verbose=True) - - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tin.settings") - django.setup() - main() diff --git a/tin/apps/users/management/commands/create_debug_users.py b/tin/apps/users/management/commands/create_debug_users.py new file mode 100644 index 00000000..9f5b6f75 --- /dev/null +++ b/tin/apps/users/management/commands/create_debug_users.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import contextlib +from getpass import getpass + +from django.core.management.base import BaseCommand + +import tin.tests.create_users as users + + +class Command(BaseCommand): + help = "Create users for debugging" + + def add_arguments(self, parser): + parser.add_argument("--noinput", action="store_true", help="Do not ask for password") + parser.add_argument("--force", action="store_true", help="Force creation of users") + + def handle(self, *args, **options): + if not options["noinput"]: + pwd = getpass("Enter password for all users: ") + else: + pwd = "jasongrace" + + with ( + contextlib.redirect_stdout(self.stdout), # type: ignore[misc] + contextlib.redirect_stderr(self.stderr), # type: ignore[misc] + ): + users.add_users_to_database(password=pwd, verbose=True, force=options["force"]) diff --git a/tin/tests/create_users.py b/tin/tests/create_users.py index 35ed5bb7..831ae40c 100644 --- a/tin/tests/create_users.py +++ b/tin/tests/create_users.py @@ -14,7 +14,7 @@ # fmt: on -def add_users_to_database(password: str, *, verbose: bool = True) -> None: +def add_users_to_database(password: str, *, force: bool = False, verbose: bool = True) -> None: User = get_user_model() for ( @@ -26,7 +26,7 @@ def add_users_to_database(password: str, *, verbose: bool = True) -> None: ) in user_data: user, created = User.objects.get_or_create(username=username) - if not created: + if not created and not force: if verbose: print(f"User {username} already exists, skipping...") continue From dbb0226f659947bc7472b6672a4975883c0b6766 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Fri, 11 Oct 2024 22:34:31 -0400 Subject: [PATCH 09/15] refactor(run_submission): use a custom wrapper This wrapper will only take effect if DEBUG is False and the sandboxing submodule isn't cloned. --- docs/source/contributing/setup.rst | 5 --- pyproject.toml | 4 ++ scripts/create_wrappers.py | 66 --------------------------- scripts/grader_wrapper.py | 55 +++++++++++++++++++++++ tin/apps/submissions/tasks.py | 72 +++++++++++++++--------------- tin/settings/__init__.py | 10 +++++ 6 files changed, 106 insertions(+), 106 deletions(-) delete mode 100755 scripts/create_wrappers.py create mode 100644 scripts/grader_wrapper.py diff --git a/docs/source/contributing/setup.rst b/docs/source/contributing/setup.rst index 36192406..a0b65afa 100644 --- a/docs/source/contributing/setup.rst +++ b/docs/source/contributing/setup.rst @@ -52,11 +52,6 @@ Submissions In order to actually create a submission, there are some more steps. First, you'll need to install `redis `_. -You'll also need to run some scripts to emulate the sandboxing process that goes on in production. -Run the following script:: - - pipenv run python3 scripts/create_wrappers.py - After that, you'll want to start up the development server and create a course, and an assignment in the course. After saving the assignment, you can hit "Upload grader" to add a grader - the simplest example of a grader is located in ``scripts/sample_grader.py``. diff --git a/pyproject.toml b/pyproject.toml index e70e8175..4a638871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,6 +178,10 @@ extend-ignore-names = [ "INP001", ] +"**/management/*" = [ + "INP001", +] + [tool.ruff.format] docstring-code-format = true line-ending = "lf" diff --git a/scripts/create_wrappers.py b/scripts/create_wrappers.py deleted file mode 100755 index 5d04fd9c..00000000 --- a/scripts/create_wrappers.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/env python3 -from __future__ import annotations - -import argparse -from pathlib import Path - -TIN_ROOT = Path(__file__).parent.parent / "tin" - -PYTHON_WRAPPER = """ -import subprocess -import sys - -def main(): - output = subprocess.run( - ["{python}", "{submission_path}"], - check=False, - capture_output=True, - text=True, - ) - print(output.stdout) - print(output.stderr, file=sys.stderr) - return output.returncode - - -if __name__ == "__main__": - sys.exit(main()) -""" - -# TODO -JAVA_WRAPPER = """""" - - -def create_wrappers(file_name: str, wrapper_text: str) -> None: - """Create sample Tin wrapper scripts. - - These are supposed to be used for sandboxing, but - for debug purposes we can just do nothing! - """ - wrappers = TIN_ROOT / "sandboxing" / "wrappers" - # we need both because in some cases bwrap exists on the parent system - for dir in ["sandboxed", "testing"]: - wrapper = wrappers / dir - wrapper.mkdir(parents=True, exist_ok=True) - - path = wrapper / f"{file_name}.txt" - # prevent possible overwriting - if path.exists() and not args.force: - print(f"Skipping file {path}") - else: - if args.force: - print(f"Overwriting file {path}") - path.write_text(wrapper_text) - - -def cli() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Create sample wrapper scripts to run Tin submissions." - ) - parser.add_argument("--force", action="store_true", help="overwrite existing wrapper files.") - return parser.parse_args() - - -if __name__ == "__main__": - args = cli() - create_wrappers("P", PYTHON_WRAPPER) - create_wrappers("J", JAVA_WRAPPER) diff --git a/scripts/grader_wrapper.py b/scripts/grader_wrapper.py new file mode 100644 index 00000000..a17ad94c --- /dev/null +++ b/scripts/grader_wrapper.py @@ -0,0 +1,55 @@ +"""A sample wrapper script for running python submissions. + +The text in this file is read in :func:`run_submission` and +executed if ``settings.USE_SANDBOXING`` is not ``True``. +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + + +def parse_args() -> list[str]: + parser = argparse.ArgumentParser() + parser.add_argument("--write", action="append") + parser.add_argument("--read", action="append") + # since we're not being sandboxed, we don't need to do anything + # with the grader arguments + _grader_args, submission_args = parser.parse_known_args() + + if submission_args and submission_args[0] == "--": + return submission_args[1:] + return submission_args + + +def find_python() -> str: + venv = Path("{venv_path}") + if venv.name == "None": + return "{python}" + if (python := venv / "bin" / "python").exists(): + return str(python) + return str(venv / "bin" / "python3") + + +def main() -> int: + args = parse_args() + submission_path = Path("{submission_path}") + + if submission_path.suffix != ".py": + raise NotImplementedError("Only python submissions are supported in DEBUG.") + + python = find_python() + output = subprocess.run( + [python, "--", str(submission_path), *args], + stdout=sys.stdout, + stderr=sys.stderr, + check=False, + ) + return output.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index 2a26b9f9..f82d4dec 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -4,13 +4,13 @@ import os import re import select -import shutil import signal import subprocess import sys import time import traceback from decimal import Decimal +from pathlib import Path import psutil from asgiref.sync import async_to_sync @@ -42,12 +42,12 @@ def run_submission(submission_id): ) submission_path = submission.file_path - submission_wrapper_path = submission.wrapper_file_path + submission_wrapper_path = Path(submission.wrapper_file_path) args = get_assignment_sandbox_args( - ["mkdir", "-p", "--", os.path.dirname(submission_wrapper_path)], + ["mkdir", "-p", "--", str(submission_wrapper_path.parent)], network_access=False, - whitelist=[os.path.dirname(os.path.dirname(submission_wrapper_path))], + whitelist=[str(submission_wrapper_path.parent.parent)], ) try: @@ -70,35 +70,38 @@ def run_submission(submission_id): else: # pragma: no cover python_exe = "/usr/bin/python3.10" - if not settings.DEBUG or shutil.which("bwrap") is not None: - folder_name = "sandboxed" - else: - folder_name = "testing" - - with open( - os.path.join( - settings.BASE_DIR, - "sandboxing", - "wrappers", - folder_name, - f"{submission.assignment.language}.txt", + if settings.USE_SANDBOXING: + wrapper_text = ( + Path(settings.BASE_DIR) + .joinpath( + "sandboxing", + "wrappers", + "sandboxed", + f"{submission.assignment.language}.txt", + ) + .read_text("utf-8") ) - ) as wrapper_file: - wrapper_text = wrapper_file.read().format( - has_network_access=bool(submission.assignment.has_network_access), - venv_path=( - submission.assignment.venv.path - if submission.assignment.venv_fully_created - else None - ), - submission_path=submission_path, - python=python_exe, + + elif submission.assignment.language == "P": + wrapper_text = settings.DEBUG_GRADER_WRAPPER_SCRIPT.read_text("utf-8") + else: + raise NotImplementedError( + f"Unsupported language {submission.assignment.language} in DEBUG" ) - with open(submission_wrapper_path, "w", encoding="utf-8") as f_obj: - f_obj.write(wrapper_text) + wrapper_text = wrapper_text.format( + has_network_access=bool(submission.assignment.has_network_access), + venv_path=( + submission.assignment.venv.path + if submission.assignment.venv_fully_created + else None + ), + submission_path=submission_path, + python=python_exe, + ) - os.chmod(submission_wrapper_path, 0o700) + submission_wrapper_path.write_text(wrapper_text, "utf-8") + submission_wrapper_path.chmod(0o700) except OSError: submission.grader_output = ( "An internal error occurred. Please try again.\n" @@ -126,15 +129,15 @@ def run_submission(submission_id): python_exe, "-u", grader_path, - submission_wrapper_path, + str(submission_wrapper_path), submission_path, submission.student.username, grader_log_path, ] - if not settings.DEBUG or shutil.which("firejail") is not None: + if settings.USE_SANDBOXING: whitelist = [os.path.dirname(grader_path)] - read_only = [grader_path, submission_path, os.path.dirname(submission_wrapper_path)] + read_only = [grader_path, submission_path, str(submission_wrapper_path.parent)] if submission.assignment.venv_fully_created: whitelist.append(submission.assignment.venv.path) read_only.append(submission.assignment.venv.path) @@ -147,7 +150,7 @@ def run_submission(submission_id): read_only=read_only, ) - env = dict(os.environ) + env = os.environ.copy() if submission.assignment.venv_fully_created: env.update(submission.assignment.venv.get_activation_env()) @@ -275,5 +278,4 @@ def run_submission(submission_id): submission.channel_group_name, {"type": "submission.updated"} ) - if os.path.exists(submission_wrapper_path): - os.remove(submission_wrapper_path) + submission_wrapper_path.unlink(missing_ok=True) diff --git a/tin/settings/__init__.py b/tin/settings/__init__.py index 56c9450b..526d3b37 100644 --- a/tin/settings/__init__.py +++ b/tin/settings/__init__.py @@ -12,6 +12,7 @@ from __future__ import annotations import os +from pathlib import Path # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -28,6 +29,11 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +USE_SANDBOXING = ( + not DEBUG or Path(BASE_DIR).joinpath("sandboxing", "wrappers", "sandboxed", "P.txt").exists() +) + + ALLOWED_HOSTS = [ "127.0.0.1", "localhost", @@ -305,6 +311,10 @@ VENV_FILE_SIZE_LIMIT = 1 * 1000 * 1000 * 1000 # 1 GB +# The wrapper script to use when running submissions outside of production +# We still need this so that it can handle cli arguments to the wrapper script +DEBUG_GRADER_WRAPPER_SCRIPT = Path(BASE_DIR).parent / "scripts" / "grader_wrapper.py" + # Spaces and special characters may not be handled correctly # Not importing correctly - specified directly in apps/submissions/tasks.py # as of 8/3/2022, 2022ldelwich From 8db46c1b01497f756bf2452b24dcfcd13b1beb60 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 13 Oct 2024 22:45:31 -0400 Subject: [PATCH 10/15] fix sample grader not actually capturing output --- scripts/sample_grader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/sample_grader.py b/scripts/sample_grader.py index 50f0de51..ffa310a5 100644 --- a/scripts/sample_grader.py +++ b/scripts/sample_grader.py @@ -9,9 +9,14 @@ import subprocess import sys -process = subprocess.run([sys.executable, sys.argv[1]], check=False) +process = subprocess.run( + [sys.executable, sys.argv[1]], + capture_output=True, + check=False, +) + if process.returncode != 0: - print("Encountered exception for test case 0:") + # let the student see the error print(process.stderr) print("Score: 0%") else: From c971a4b187dded7a293318844665b608828f9564 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 13 Oct 2024 23:00:29 -0400 Subject: [PATCH 11/15] Better support for django-admin options verbosity --- .../management/commands/create_debug_users.py | 14 +++++++------- tin/apps/users/tests.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 tin/apps/users/tests.py diff --git a/tin/apps/users/management/commands/create_debug_users.py b/tin/apps/users/management/commands/create_debug_users.py index 9f5b6f75..b888e86c 100644 --- a/tin/apps/users/management/commands/create_debug_users.py +++ b/tin/apps/users/management/commands/create_debug_users.py @@ -1,9 +1,8 @@ from __future__ import annotations -import contextlib from getpass import getpass -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, no_translations import tin.tests.create_users as users @@ -15,14 +14,15 @@ def add_arguments(self, parser): parser.add_argument("--noinput", action="store_true", help="Do not ask for password") parser.add_argument("--force", action="store_true", help="Force creation of users") + @no_translations def handle(self, *args, **options): if not options["noinput"]: pwd = getpass("Enter password for all users: ") else: pwd = "jasongrace" - with ( - contextlib.redirect_stdout(self.stdout), # type: ignore[misc] - contextlib.redirect_stderr(self.stderr), # type: ignore[misc] - ): - users.add_users_to_database(password=pwd, verbose=True, force=options["force"]) + users.add_users_to_database( + password=pwd, + verbose=options["verbosity"] > 0, + force=options["force"], + ) diff --git a/tin/apps/users/tests.py b/tin/apps/users/tests.py new file mode 100644 index 00000000..8e6b7071 --- /dev/null +++ b/tin/apps/users/tests.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from django.contrib.auth import get_user_model +from django.core.management import call_command + + +def test_create_debug_users(): + call_command("create_debug_users", noinput=True, verbosity=0) + assert get_user_model().objects.filter(username="admin", is_superuser=True) + assert get_user_model().objects.filter(username="student", is_student=True) + assert get_user_model().objects.filter(username="teacher", is_teacher=True) From 4a31149cc136b34e0b1fc40f132fcdb63a4fa95a Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 10 Nov 2024 09:29:20 -0500 Subject: [PATCH 12/15] pathlib -> os.path --- tin/apps/submissions/tasks.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index f82d4dec..441b863f 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import logging import os import re @@ -42,12 +43,12 @@ def run_submission(submission_id): ) submission_path = submission.file_path - submission_wrapper_path = Path(submission.wrapper_file_path) + submission_wrapper_path = submission.wrapper_file_path args = get_assignment_sandbox_args( - ["mkdir", "-p", "--", str(submission_wrapper_path.parent)], + ["mkdir", "-p", "--", os.path.dirname(submission_wrapper_path)], network_access=False, - whitelist=[str(submission_wrapper_path.parent.parent)], + whitelist=[os.path.dirname(os.path.dirname(submission_wrapper_path))], ) try: @@ -100,8 +101,9 @@ def run_submission(submission_id): python=python_exe, ) - submission_wrapper_path.write_text(wrapper_text, "utf-8") - submission_wrapper_path.chmod(0o700) + with open(submission_wrapper_path, "w", encoding="utf-8") as f: + f.write(wrapper_text) + os.chmod(submission_wrapper_path, 0o700) except OSError: submission.grader_output = ( "An internal error occurred. Please try again.\n" @@ -129,7 +131,7 @@ def run_submission(submission_id): python_exe, "-u", grader_path, - str(submission_wrapper_path), + submission_wrapper_path, submission_path, submission.student.username, grader_log_path, @@ -137,7 +139,7 @@ def run_submission(submission_id): if settings.USE_SANDBOXING: whitelist = [os.path.dirname(grader_path)] - read_only = [grader_path, submission_path, str(submission_wrapper_path.parent)] + read_only = [grader_path, submission_path, os.path.dirname(submission_wrapper_path)] if submission.assignment.venv_fully_created: whitelist.append(submission.assignment.venv.path) read_only.append(submission.assignment.venv.path) @@ -278,4 +280,5 @@ def run_submission(submission_id): submission.channel_group_name, {"type": "submission.updated"} ) - submission_wrapper_path.unlink(missing_ok=True) + with contextlib.suppress(FileNotFoundError): + os.unlink(submission_wrapper_path) From 9b66af0c00b50f811f71301e09f0358ce8808f21 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 10 Nov 2024 09:32:37 -0500 Subject: [PATCH 13/15] Rename to reduce diff --- tin/apps/submissions/tasks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tin/apps/submissions/tasks.py b/tin/apps/submissions/tasks.py index 441b863f..137af23a 100644 --- a/tin/apps/submissions/tasks.py +++ b/tin/apps/submissions/tasks.py @@ -101,8 +101,9 @@ def run_submission(submission_id): python=python_exe, ) - with open(submission_wrapper_path, "w", encoding="utf-8") as f: - f.write(wrapper_text) + with open(submission_wrapper_path, "w", encoding="utf-8") as f_obj: + f_obj.write(wrapper_text) + os.chmod(submission_wrapper_path, 0o700) except OSError: submission.grader_output = ( @@ -281,4 +282,4 @@ def run_submission(submission_id): ) with contextlib.suppress(FileNotFoundError): - os.unlink(submission_wrapper_path) + os.remove(submission_wrapper_path) From 749f76ae92c44f7b22bc1a19990d8e247662152b Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 10 Nov 2024 10:07:06 -0500 Subject: [PATCH 14/15] Fix creation of virtual environments --- scripts/sample_grader.py | 5 ++--- tin/apps/venvs/tasks.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/sample_grader.py b/scripts/sample_grader.py index ffa310a5..2a19e5fd 100644 --- a/scripts/sample_grader.py +++ b/scripts/sample_grader.py @@ -11,13 +11,12 @@ process = subprocess.run( [sys.executable, sys.argv[1]], - capture_output=True, + stdout=sys.stdout, + stderr=subprocess.STDOUT, check=False, ) if process.returncode != 0: - # let the student see the error - print(process.stderr) print("Score: 0%") else: print("Score: 100%") diff --git a/tin/apps/venvs/tasks.py b/tin/apps/venvs/tasks.py index ab7798e3..eab62b1c 100644 --- a/tin/apps/venvs/tasks.py +++ b/tin/apps/venvs/tasks.py @@ -15,6 +15,9 @@ @shared_task def create_venv(venv_id): venv = Venv.objects.get(id=venv_id) + python = settings.SUBMISSION_PYTHON + if settings.DEBUG: + python = sys.executable success = False try: @@ -25,7 +28,7 @@ def create_venv(venv_id): "-m", "virtualenv", "-p", - settings.SUBMISSION_PYTHON, + python, "--", venv.path, ], From 10769db236274c9d10e56d7bea611091295e8a12 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 10 Nov 2024 10:49:21 -0500 Subject: [PATCH 15/15] Ensure that test doesn't autocreate users before --- tin/apps/users/tests.py | 17 ++++++++++++++--- tin/tests/fixtures.py | 6 ++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tin/apps/users/tests.py b/tin/apps/users/tests.py index 8e6b7071..33f65f22 100644 --- a/tin/apps/users/tests.py +++ b/tin/apps/users/tests.py @@ -1,11 +1,22 @@ from __future__ import annotations +import pytest from django.contrib.auth import get_user_model from django.core.management import call_command +@pytest.mark.no_autocreate_users def test_create_debug_users(): + admin = get_user_model().objects.filter(username="admin", is_superuser=True) + student = get_user_model().objects.filter(username="student", is_student=True) + teacher = get_user_model().objects.filter(username="teacher", is_teacher=True) + + assert not admin.exists() + assert not teacher.exists() + assert not student.exists() + call_command("create_debug_users", noinput=True, verbosity=0) - assert get_user_model().objects.filter(username="admin", is_superuser=True) - assert get_user_model().objects.filter(username="student", is_student=True) - assert get_user_model().objects.filter(username="teacher", is_teacher=True) + + assert admin.exists() + assert teacher.exists() + assert student.exists() diff --git a/tin/tests/fixtures.py b/tin/tests/fixtures.py index 2cd66aa3..a89bf6c7 100644 --- a/tin/tests/fixtures.py +++ b/tin/tests/fixtures.py @@ -33,8 +33,10 @@ def tin_setup(settings, worker_id: str, testrun_uid: str): @pytest.fixture(autouse=True) -def create_users(): - users.add_users_to_database(password=PASSWORD, verbose=False) +def create_users(request): + marker = request.node.get_closest_marker("no_autocreate_users") + if marker is None: + users.add_users_to_database(password=PASSWORD, verbose=False) @pytest.fixture