Skip to content

Commit 3ed43ef

Browse files
committed
Add a new "interactive" task type.
1 parent 9f6f2d9 commit 3ed43ef

36 files changed

+1200
-111
lines changed

cms/grading/ParameterTypes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ def parse_string(self, value):
134134
return value
135135

136136

137+
class ParameterTypeBool(ParameterType):
138+
"""Type for a boolean parameter."""
139+
140+
TEMPLATE = GLOBAL_ENVIRONMENT.from_string("""
141+
<input type="checkbox"
142+
name="{{ prefix ~ parameter.short_name }}"
143+
{% if previous_value %}checked{% endif %} />
144+
""")
145+
146+
def validate(self, value):
147+
if not isinstance(value, bool):
148+
raise ValueError("Invalid value for bool parameter %s" % self.name)
149+
150+
def parse_handler(self, handler, prefix):
151+
return handler.get_argument(prefix + self.short_name, None) is not None
152+
153+
def parse_string(self, value):
154+
return value.lower() == "true"
155+
156+
137157
class ParameterTypeInt(ParameterType):
138158
"""Type for an integer parameter."""
139159

@@ -151,6 +171,23 @@ def parse_string(self, value):
151171
return int(value)
152172

153173

174+
class ParameterTypeFloat(ParameterType):
175+
"""Type for a float parameter."""
176+
177+
TEMPLATE = GLOBAL_ENVIRONMENT.from_string("""
178+
<input type="text"
179+
name="{{ prefix ~ parameter.short_name }}"
180+
value="{{ previous_value }}" />
181+
""")
182+
183+
def validate(self, value):
184+
if not isinstance(value, float) and not isinstance(value, int):
185+
raise ValueError("Invalid value for float parameter %s" % self.name)
186+
187+
def parse_string(self, value):
188+
return float(value)
189+
190+
154191
class ParameterTypeChoice(ParameterType):
155192
"""Type for a parameter giving a choice among a finite number of items."""
156193

cms/grading/Sandbox.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,10 @@ def __init__(
241241
# we need to ensure that they can read and write to the directory.
242242
# But we don't want everybody on the system to, which is why the
243243
# outer directory exists with no read permissions.
244-
self._outer_dir = tempfile.mkdtemp(
244+
self._outer_dir: str = tempfile.mkdtemp(
245245
dir=self.temp_dir, prefix="cms-%s-" % (self.name)
246246
)
247-
self._home = os.path.join(self._outer_dir, "home")
247+
self._home: str = os.path.join(self._outer_dir, "home")
248248
self._home_dest = "/tmp"
249249
os.mkdir(self._home)
250250

@@ -266,15 +266,16 @@ def __init__(
266266
self.inherit_env: list[str] = [] # -E
267267
self.set_env: dict[str, str] = {} # -E
268268
self.fsize: int | None = None # -f
269-
self.stdin_file: str | None = None # -i
270-
self.stdout_file: str | None = None # -o
271-
self.stderr_file: str | None = None # -r
269+
self.stdin_file: str | int | None = None # -i
270+
self.stdout_file: str | int | None = None # -o
271+
self.stderr_file: str | int | None = None # -r
272272
self.stack_space: int | None = None # -k
273273
self.address_space: int | None = None # -m
274274
self.timeout: float | None = None # -t
275275
self.verbosity: int = 0 # -v
276276
self.wallclock_timeout: float | None = None # -w
277277
self.extra_timeout: float | None = None # -x
278+
self.close_fds = True
278279

279280
self.max_processes: int = 1
280281

@@ -656,13 +657,15 @@ def execute_without_std(
656657
return the Popen object from subprocess.
657658
658659
"""
659-
popen = self._popen(
660-
command,
661-
stdin=subprocess.PIPE,
662-
stdout=subprocess.PIPE,
663-
stderr=subprocess.PIPE,
664-
close_fds=True,
660+
stdin = self.stdin_file if isinstance(self.stdin_file, int) else subprocess.PIPE
661+
stdout = (
662+
self.stdout_file if isinstance(self.stdout_file, int) else subprocess.PIPE
665663
)
664+
stderr = (
665+
self.stderr_file if isinstance(self.stderr_file, int) else subprocess.PIPE
666+
)
667+
668+
popen = self._popen(command, stdin=stdin, stdout=stdout, stderr=stderr)
666669

667670
# If the caller wants us to wait for completion, we also avoid
668671
# std*** to interfere with command. Otherwise we let the
@@ -730,12 +733,13 @@ def cleanup(self, delete: bool = False):
730733
self._home_dest,
731734
],
732735
stdout=subprocess.DEVNULL,
733-
stderr=subprocess.STDOUT,
736+
stderr=subprocess.DEVNULL,
734737
)
735738

736739
# Tell isolate to cleanup the sandbox.
737740
subprocess.check_call(
738-
exe + ["--cleanup"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
741+
exe + ["--cleanup"],
742+
stdout=subprocess.DEVNULL,
739743
)
740744

741745
if delete:
@@ -878,21 +882,21 @@ def build_box_options(self) -> list[str]:
878882
if self.fsize is not None:
879883
# Isolate wants file size as KiB.
880884
res += ["--fsize=%d" % (self.fsize // 1024)]
881-
if self.stdin_file is not None:
885+
if isinstance(self.stdin_file, str):
882886
res += ["--stdin=%s" % self.inner_absolute_path(self.stdin_file)]
883887
if self.stack_space is not None:
884888
# Isolate wants stack size as KiB.
885889
res += ["--stack=%d" % (self.stack_space // 1024)]
886890
if self.address_space is not None:
887891
# Isolate wants memory size as KiB.
888892
res += ["--cg-mem=%d" % (self.address_space // 1024)]
889-
if self.stdout_file is not None:
893+
if isinstance(self.stdout_file, str):
890894
res += ["--stdout=%s" % self.inner_absolute_path(self.stdout_file)]
891895
if self.max_processes is not None:
892896
res += ["--processes=%d" % self.max_processes]
893897
else:
894898
res += ["--processes"]
895-
if self.stderr_file is not None:
899+
if isinstance(self.stderr_file, str):
896900
res += ["--stderr=%s" % self.inner_absolute_path(self.stderr_file)]
897901
if self.timeout is not None:
898902
res += ["--time=%g" % self.timeout]
@@ -901,6 +905,8 @@ def build_box_options(self) -> list[str]:
901905
res += ["--wall-time=%g" % self.wallclock_timeout]
902906
if self.extra_timeout is not None:
903907
res += ["--extra-time=%g" % self.extra_timeout]
908+
if not self.close_fds:
909+
res += ["--inherit-fds", "--open-files=0"]
904910
res += ["--meta=%s" % ("%s.%d" % (self.info_basename, self.exec_num))]
905911
res += ["--run"]
906912
return res
@@ -958,7 +964,6 @@ def _popen(
958964
stdin: int | None = None,
959965
stdout: int | None = None,
960966
stderr: int | None = None,
961-
close_fds: bool = True,
962967
) -> subprocess.Popen:
963968
"""Execute the given command in the sandbox using
964969
subprocess.Popen, assigning the corresponding standard file
@@ -968,7 +973,6 @@ def _popen(
968973
stdin: a file descriptor.
969974
stdout: a file descriptor.
970975
stderr: a file descriptor.
971-
close_fds: close all file descriptor before executing.
972976
973977
return: popen object.
974978
@@ -989,7 +993,11 @@ def _popen(
989993
os.chmod(self._home, prev_permissions)
990994
try:
991995
p = subprocess.Popen(
992-
args, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=close_fds
996+
args,
997+
stdin=stdin,
998+
stdout=stdout,
999+
stderr=stderr,
1000+
close_fds=self.close_fds,
9931001
)
9941002
except OSError:
9951003
logger.critical(
@@ -1005,6 +1013,6 @@ def initialize_isolate(self):
10051013
"""Initialize isolate's box."""
10061014
init_cmd = ["isolate", "--box-id=%d" % self.box_id, "--cg", "--init"]
10071015
try:
1008-
subprocess.check_call(init_cmd)
1016+
subprocess.check_call(init_cmd, stdout=subprocess.DEVNULL)
10091017
except subprocess.CalledProcessError as e:
10101018
raise SandboxInterfaceException("Failed to initialize sandbox") from e

cms/grading/steps/evaluation.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def evaluation_step(
136136
for command in commands:
137137
success = evaluation_step_before_run(
138138
sandbox, command, time_limit, memory_limit,
139-
dirs_map, writable_files, stdin_redirect, stdout_redirect,
140-
multiprocess, wait=True)
139+
None, dirs_map, writable_files, stdin_redirect,
140+
stdout_redirect, multiprocess, wait=True)
141141
if not success:
142142
logger.debug("Job failed in evaluation_step_before_run.")
143143
return False, None, None
@@ -154,11 +154,13 @@ def evaluation_step_before_run(
154154
command: list[str],
155155
time_limit: float | None = None,
156156
memory_limit: int | None = None,
157+
wall_limit: float | None = None,
157158
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
158159
writable_files: list[str] | None = None,
159-
stdin_redirect: str | None = None,
160-
stdout_redirect: str | None = None,
160+
stdin_redirect: str | int | None = None,
161+
stdout_redirect: str | int | None = "stdout.txt",
161162
multiprocess: bool = False,
163+
close_fds: bool = True,
162164
wait: bool = False,
163165
) -> bool | subprocess.Popen:
164166
"""First part of an evaluation step, up to the execution, included.
@@ -175,6 +177,8 @@ def evaluation_step_before_run(
175177
# Ensure parameters are appropriate.
176178
if time_limit is not None and time_limit <= 0:
177179
raise ValueError("Time limit must be positive, is %s" % time_limit)
180+
if wall_limit is not None and wall_limit <= 0:
181+
raise ValueError("Wall limit must be positive, is %s" % wall_limit)
178182
if memory_limit is not None and memory_limit <= 0:
179183
raise ValueError(
180184
"Memory limit must be positive, is %s" % memory_limit)
@@ -184,8 +188,6 @@ def evaluation_step_before_run(
184188
dirs_map = {}
185189
if writable_files is None:
186190
writable_files = []
187-
if stdout_redirect is None:
188-
stdout_redirect = "stdout.txt"
189191

190192
# Set sandbox parameters suitable for evaluation.
191193
if time_limit is not None:
@@ -195,6 +197,9 @@ def evaluation_step_before_run(
195197
sandbox.timeout = None
196198
sandbox.wallclock_timeout = None
197199

200+
if wall_limit is not None:
201+
sandbox.wallclock_timeout = wall_limit
202+
198203
if memory_limit is not None:
199204
sandbox.address_space = memory_limit
200205
else:
@@ -210,11 +215,12 @@ def evaluation_step_before_run(
210215
for src, (dest, options) in dirs_map.items():
211216
sandbox.add_mapped_directory(src, dest=dest, options=options)
212217
for name in [sandbox.stderr_file, sandbox.stdout_file]:
213-
if name is not None:
218+
if isinstance(name, str):
214219
writable_files.append(name)
215220
sandbox.allow_writing_only(writable_files)
216221

217222
sandbox.set_multiprocess(multiprocess)
223+
sandbox.close_fds = close_fds
218224

219225
# Actually run the evaluation command.
220226
logger.debug("Starting execution step.")

0 commit comments

Comments
 (0)