Skip to content

Commit 33aad96

Browse files
committed
Add a new "interactive" task type.
1 parent 727005d commit 33aad96

36 files changed

+1216
-111
lines changed

cms/grading/ParameterTypes.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ 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+
<select name="{{ prefix ~ parameter.short_name }}">
142+
<option value="true" {% if previous_value %}selected{% endif %}>
143+
true
144+
</option>
145+
<option value="false" {% if not previous_value %}selected{% endif %}>
146+
false
147+
</option>
148+
</select>
149+
""")
150+
151+
def validate(self, value):
152+
if not isinstance(value, bool):
153+
raise ValueError("Invalid value for bool parameter %s" % self.name)
154+
155+
def parse_string(self, value):
156+
return value.lower() == "true"
157+
158+
137159
class ParameterTypeInt(ParameterType):
138160
"""Type for an integer parameter."""
139161

@@ -151,6 +173,23 @@ def parse_string(self, value):
151173
return int(value)
152174

153175

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

cms/grading/Sandbox.py

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

@@ -269,15 +269,16 @@ def __init__(
269269
self.inherit_env: list[str] = [] # -E
270270
self.set_env: dict[str, str] = {} # -E
271271
self.fsize: int | None = None # -f
272-
self.stdin_file: str | None = None # -i
273-
self.stdout_file: str | None = None # -o
274-
self.stderr_file: str | None = None # -r
272+
self.stdin_file: str | int | None = None # -i
273+
self.stdout_file: str | int | None = None # -o
274+
self.stderr_file: str | int | None = None # -r
275275
self.stack_space: int | None = None # -k
276276
self.address_space: int | None = None # -m
277277
self.timeout: float | None = None # -t
278278
self.verbosity: int = 0 # -v
279279
self.wallclock_timeout: float | None = None # -w
280280
self.extra_timeout: float | None = None # -x
281+
self.close_fds = True
281282

282283
self.max_processes: int = 1
283284

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

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

735738
# Tell isolate to cleanup the sandbox.
736739
subprocess.check_call(
737-
exe + ["--cleanup"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
740+
exe + ["--cleanup"],
741+
stdout=subprocess.DEVNULL,
738742
)
739743

740744
if delete:
@@ -877,21 +881,21 @@ def build_box_options(self) -> list[str]:
877881
if self.fsize is not None:
878882
# Isolate wants file size as KiB.
879883
res += ["--fsize=%d" % (self.fsize // 1024)]
880-
if self.stdin_file is not None:
884+
if isinstance(self.stdin_file, str):
881885
res += ["--stdin=%s" % self.inner_absolute_path(self.stdin_file)]
882886
if self.stack_space is not None:
883887
# Isolate wants stack size as KiB.
884888
res += ["--stack=%d" % (self.stack_space // 1024)]
885889
if self.address_space is not None:
886890
# Isolate wants memory size as KiB.
887891
res += ["--cg-mem=%d" % (self.address_space // 1024)]
888-
if self.stdout_file is not None:
892+
if isinstance(self.stdout_file, str):
889893
res += ["--stdout=%s" % self.inner_absolute_path(self.stdout_file)]
890894
if self.max_processes is not None:
891895
res += ["--processes=%d" % self.max_processes]
892896
else:
893897
res += ["--processes"]
894-
if self.stderr_file is not None:
898+
if isinstance(self.stderr_file, str):
895899
res += ["--stderr=%s" % self.inner_absolute_path(self.stderr_file)]
896900
if self.timeout is not None:
897901
res += ["--time=%g" % self.timeout]
@@ -900,6 +904,8 @@ def build_box_options(self) -> list[str]:
900904
res += ["--wall-time=%g" % self.wallclock_timeout]
901905
if self.extra_timeout is not None:
902906
res += ["--extra-time=%g" % self.extra_timeout]
907+
if not self.close_fds:
908+
res += ["--inherit-fds", "--open-files=0"]
903909
res += ["--meta=%s" % ("%s.%d" % (self.info_basename, self.exec_num))]
904910
res += ["--run"]
905911
return res
@@ -957,7 +963,6 @@ def _popen(
957963
stdin: int | None = None,
958964
stdout: int | None = None,
959965
stderr: int | None = None,
960-
close_fds: bool = True,
961966
) -> subprocess.Popen:
962967
"""Execute the given command in the sandbox using
963968
subprocess.Popen, assigning the corresponding standard file
@@ -967,7 +972,6 @@ def _popen(
967972
stdin: a file descriptor.
968973
stdout: a file descriptor.
969974
stderr: a file descriptor.
970-
close_fds: close all file descriptor before executing.
971975
972976
return: popen object.
973977
@@ -988,7 +992,11 @@ def _popen(
988992
os.chmod(self._home, prev_permissions)
989993
try:
990994
p = subprocess.Popen(
991-
args, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=close_fds
995+
args,
996+
stdin=stdin,
997+
stdout=stdout,
998+
stderr=stderr,
999+
close_fds=self.close_fds,
9921000
)
9931001
except OSError:
9941002
logger.critical(
@@ -1004,6 +1012,6 @@ def initialize_isolate(self):
10041012
"""Initialize isolate's box."""
10051013
init_cmd = ["isolate", "--box-id=%d" % self.box_id, "--cg", "--init"]
10061014
try:
1007-
subprocess.check_call(init_cmd)
1015+
subprocess.check_call(init_cmd, stdout=subprocess.DEVNULL)
10081016
except subprocess.CalledProcessError as e:
10091017
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)