Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions cms/grading/Sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,20 +970,19 @@ def __init__(self, file_cacher, name=None, temp_dir=None):
# between sandboxes.
self.dirs.append((None, "/dev/shm", "tmp"))

# Set common environment variables.
# Set common configuration that is relevant for multiple
# languages.

self.set_env["PATH"] = "/usr/local/bin:/usr/bin:/bin"

# Specifically needed by Python, that searches the home for
# packages.
self.set_env["HOME"] = self._home_dest

# Needed on Ubuntu by PHP (and more), since /usr/bin only contains a
# symlink to one out of many alternatives.
# Needed on Ubuntu by PHP, Java, Pascal etc, since /usr/bin
# only contains a symlink to one out of many alternatives.
self.maybe_add_mapped_directory("/etc/alternatives")

# Likewise, needed by C# programs. The Mono runtime looks in
# /etc/mono/config to obtain the default DllMap, which includes, in
# particular, the System.Native assembly.
self.maybe_add_mapped_directory("/etc/mono", options="noexec")

# Tell isolate to get the sandbox ready. We do our best to cleanup
# after ourselves, but we might have missed something if a previous
# worker was interrupted in the middle of an execution, so we issue an
Expand Down
15 changes: 15 additions & 0 deletions cms/grading/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import os
from abc import ABCMeta, abstractmethod
from cms.grading.Sandbox import Sandbox


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -135,6 +136,13 @@ def get_compilation_commands(
"""
pass

def configure_compilation_sandbox(self, sandbox: Sandbox):
"""
Set sandbox parameters necessary for running the compilation
commands.
"""
pass

@abstractmethod
def get_evaluation_commands(
self,
Expand All @@ -156,6 +164,13 @@ def get_evaluation_commands(
"""
pass

def configure_evaluation_sandbox(self, sandbox: Sandbox):
"""
Set sandbox parameters necessary for running the evaluation
commands.
"""
pass

# It's sometimes handy to use Language objects in sets or as dict
# keys. Since they have no state (they are just collections of
# constants and static methods) and are designed to be used as
Expand Down
9 changes: 9 additions & 0 deletions cms/grading/languages/csharp_mono.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,12 @@ def get_evaluation_commands(
self, executable_filename, main=None, args=None):
"""See Language.get_evaluation_commands."""
return [["/usr/bin/mono", executable_filename]]

def configure_compilation_sandbox(self, sandbox):
# The Mono runtime looks in /etc/mono/config to obtain the
# default DllMap, which includes, in particular, the
# System.Native assembly.
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")

def configure_evaluation_sandbox(self, sandbox):
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")
7 changes: 7 additions & 0 deletions cms/grading/languages/haskell_ghc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ def get_compilation_commands(self,
executable_filename, source_filenames[0]])
return commands

def configure_compilation_sandbox(self, sandbox):
# Directory required to be visible during a compilation with GHC.
# GHC looks for the Haskell's package database in
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
# but it is a symlink to "/var/lib/ghc/package.conf.d")
sandbox.maybe_add_mapped_directory("/var/lib/ghc")

@staticmethod
def _capitalize(string: str):
dirname, basename = os.path.split(string)
Expand Down
8 changes: 8 additions & 0 deletions cms/grading/languages/java_jdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

"""

import os
from shlex import quote as shell_quote

from cms.grading import Language
Expand Down Expand Up @@ -91,3 +92,10 @@ def get_evaluation_commands(
command = ["/usr/bin/java", "-Deval=true", "-Xmx512M", "-Xss64M",
main] + args
return [unzip_command, command]

def configure_compilation_sandbox(self, sandbox):
# the jvm conf directory is often symlinked to /etc in
# distributions, but the location of it in /etc is inconsistent.
for path in os.listdir("/etc"):
if path == "java" or path.startswith("java-"):
sandbox.add_mapped_directory(f"/etc/{path}")
4 changes: 4 additions & 0 deletions cms/grading/languages/pascal_fpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ def get_compilation_commands(self,
command += ["-O2", "-XSs", "-o%s" % executable_filename]
command += [source_filenames[0]]
return [command]

def configure_compilation_sandbox(self, sandbox):
# Needed for /etc/fpc.cfg.
sandbox.maybe_add_mapped_directory("/etc")
8 changes: 8 additions & 0 deletions cms/grading/languages/python3_pypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ def get_evaluation_commands(
"""See Language.get_evaluation_commands."""
args = args if args is not None else []
return [["/usr/bin/pypy3", executable_filename] + args]

def configure_compilation_sandbox(self, sandbox):
# Needed on Arch, where /usr/bin/pypy3 is a symlink into
# /opt/pypy3.
sandbox.maybe_add_mapped_directory("/opt/pypy3")

def configure_evaluation_sandbox(self, sandbox):
sandbox.maybe_add_mapped_directory("/opt/pypy3")
14 changes: 6 additions & 8 deletions cms/grading/steps/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from cms import config
from cms.grading.Sandbox import Sandbox
from cms.grading.language import Language
from cms.grading.steps.stats import StatsDict
from .messages import HumanMessage, MessageCollection
from .utils import generic_step
Expand Down Expand Up @@ -66,7 +67,7 @@ def N_(message: str):


def compilation_step(
sandbox: Sandbox, commands: list[list[str]]
sandbox: Sandbox, commands: list[list[str]], language: Language
) -> tuple[bool, bool | None, list[str] | None, StatsDict | None]:
"""Execute some compilation commands in the sandbox.

Expand All @@ -79,6 +80,7 @@ def compilation_step(

sandbox: the sandbox we consider, already created.
commands: compilation commands to execute.
language: language of the submission

return: a tuple with four items:
* success: True if the sandbox did not fail, in any command;
Expand All @@ -93,18 +95,14 @@ def compilation_step(

"""
# Set sandbox parameters suitable for compilation.
sandbox.add_mapped_directory("/etc")
# Directory required to be visible during a compilation with GHC.
# GHC looks for the Haskell's package database in
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
# but it is a symlink to "/var/lib/ghc/package.conf.d"
sandbox.maybe_add_mapped_directory("/var/lib/ghc")
sandbox.preserve_env = True
sandbox.max_processes = config.sandbox.compilation_sandbox_max_processes
sandbox.timeout = config.sandbox.compilation_sandbox_max_time_s
sandbox.wallclock_timeout = 2 * sandbox.timeout + 1
sandbox.address_space = config.sandbox.compilation_sandbox_max_memory_kib * 1024

# Set per-language sandbox parameters.
language.configure_compilation_sandbox(sandbox)

# Run the compilation commands, copying stdout and stderr to stats.
stats = generic_step(sandbox, commands, "compilation", collect_output=True)
if stats is None:
Expand Down
11 changes: 10 additions & 1 deletion cms/grading/steps/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from cms import config
from cms.grading.Sandbox import Sandbox
from cms.grading.language import Language
from .messages import HumanMessage, MessageCollection
from .stats import StatsDict, execution_stats

Expand Down Expand Up @@ -83,6 +84,7 @@ def N_(message: str):
def evaluation_step(
sandbox: Sandbox,
commands: list[list[str]],
language: Language | None,
time_limit: float | None = None,
memory_limit: int | None = None,
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
Expand All @@ -101,6 +103,8 @@ def evaluation_step(

sandbox: the sandbox we consider, already created.
commands: evaluation commands to execute.
language: language of the submission (or None if the commands to
execute are not from a Language's get_evaluation_commands).
time_limit: time limit in seconds (applied to each command);
if None, no time limit is enforced.
memory_limit: memory limit in bytes (applied to each command);
Expand Down Expand Up @@ -135,7 +139,7 @@ def evaluation_step(
"""
for command in commands:
success = evaluation_step_before_run(
sandbox, command, time_limit, memory_limit,
sandbox, command, language, time_limit, memory_limit,
dirs_map, writable_files, stdin_redirect, stdout_redirect,
multiprocess, wait=True)
if not success:
Expand All @@ -152,6 +156,7 @@ def evaluation_step(
def evaluation_step_before_run(
sandbox: Sandbox,
command: list[str],
language: Language | None,
time_limit: float | None = None,
memory_limit: int | None = None,
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
Expand Down Expand Up @@ -216,6 +221,10 @@ def evaluation_step_before_run(

sandbox.set_multiprocess(multiprocess)

# Configure per-language sandbox parameters.
if language:
language.configure_evaluation_sandbox(sandbox)

# Actually run the evaluation command.
logger.debug("Starting execution step.")
return sandbox.execute_without_std(command, wait=wait)
Expand Down
7 changes: 5 additions & 2 deletions cms/grading/tasktypes/Batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import os

from cms.db import Executable
from cms.db.filecacher import FileCacher
from cms.grading.Job import EvaluationJob
from cms.grading.ParameterTypes import ParameterTypeCollection, \
ParameterTypeChoice, ParameterTypeString
from cms.grading.language import Language
Expand Down Expand Up @@ -242,7 +244,7 @@ def _do_compile(self, job, file_cacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables.
job.success = box_success
Expand All @@ -266,7 +268,7 @@ def compile(self, job, file_cacher):

self._do_compile(job, file_cacher)

def _execution_step(self, job, file_cacher):
def _execution_step(self, job: EvaluationJob, file_cacher: FileCacher):
# Prepare the execution
executable_filename = next(iter(job.executables.keys()))
language = get_language(job.language)
Expand Down Expand Up @@ -308,6 +310,7 @@ def _execution_step(self, job, file_cacher):
box_success, evaluation_success, stats = evaluation_step(
sandbox,
commands,
language,
job.time_limit,
job.memory_limit,
writable_files=files_allowing_write,
Expand Down
5 changes: 4 additions & 1 deletion cms/grading/tasktypes/Communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def compile(self, job, file_cacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables.
job.success = box_success
Expand Down Expand Up @@ -320,6 +320,7 @@ def evaluate(self, job, file_cacher):
manager_ = evaluation_step_before_run(
sandbox_mgr,
manager_command,
None,
manager_time_limit,
config.sandbox.trusted_sandbox_max_memory_kib * 1024,
dirs_map=dict((fifo_dir[i], (sandbox_fifo_dir[i], "rw")) for i in indices),
Expand Down Expand Up @@ -355,11 +356,13 @@ def evaluate(self, job, file_cacher):
# Assumes that the actual execution of the user solution is the
# last command in commands, and that the previous are "setup"
# that don't need tight control.
# TODO: why can't this use normal evaluation step??
if len(commands) > 1:
trusted_step(sandbox_user[i], commands[:-1])
the_process = evaluation_step_before_run(
sandbox_user[i],
commands[-1],
language,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir[i]: (sandbox_fifo_dir[i], "rw")},
Expand Down
4 changes: 3 additions & 1 deletion cms/grading/tasktypes/TwoSteps.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def compile(self, job, file_cacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables
job.success = box_success
Expand Down Expand Up @@ -249,6 +249,7 @@ def evaluate(self, job, file_cacher):
first = evaluation_step_before_run(
first_sandbox,
first_command,
None,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir: ("/fifo", "rw")},
Expand All @@ -272,6 +273,7 @@ def evaluate(self, job, file_cacher):
second = evaluation_step_before_run(
second_sandbox,
second_command,
None,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir: ("/fifo", "rw")},
Expand Down
15 changes: 8 additions & 7 deletions cmstestsuite/unit_tests/grading/steps/compilation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from cmstestsuite.unit_tests.grading.steps.fakeisolatesandbox \
import FakeIsolateSandbox
from cmstestsuite.unit_tests.grading.steps.stats_test import get_stats
from cmstestsuite.unit_tests.grading.tasktypes.tasktypetestutils import LANG_1


ONE_COMMAND = [["test", "command"]]
Expand Down Expand Up @@ -54,7 +55,7 @@ def test_single_command_success(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats) as mock_generic_step:
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

mock_generic_step.assert_called_once_with(
self.sandbox, ONE_COMMAND, "compilation", collect_output=True)
Expand All @@ -73,7 +74,7 @@ def test_single_command_compilation_failed_nonzero_return(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats):
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

# User's fault, no error needs to be logged.
self.assertLoggedError(False)
Expand All @@ -91,7 +92,7 @@ def test_single_command_compilation_failed_timeout(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats):
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

# User's fault, no error needs to be logged.
self.assertLoggedError(False)
Expand All @@ -109,7 +110,7 @@ def test_single_command_compilation_failed_timeout_wall(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats):
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

# User's fault, no error needs to be logged.
self.assertLoggedError(False)
Expand All @@ -127,7 +128,7 @@ def test_single_command_compilation_failed_signal(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats):
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

# User's fault, no error needs to be logged.
self.assertLoggedError(False)
Expand All @@ -141,7 +142,7 @@ def test_single_command_sandbox_failed(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=None):
success, compilation_success, text, stats = compilation_step(
self.sandbox, ONE_COMMAND)
self.sandbox, ONE_COMMAND, LANG_1)

# Sandbox should never fail. If it does, should notify the admin.
self.assertLoggedError()
Expand All @@ -156,7 +157,7 @@ def test_multiple_commands_success(self):
with patch("cms.grading.steps.compilation.generic_step",
return_value=expected_stats) as mock_generic_step:
success, compilation_success, text, stats = compilation_step(
self.sandbox, TWO_COMMANDS)
self.sandbox, TWO_COMMANDS, LANG_1)

mock_generic_step.assert_called_once_with(
self.sandbox, TWO_COMMANDS, "compilation", collect_output=True)
Expand Down
Loading