rbx.cp 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. rbx/__init__.py +0 -0
  2. rbx/annotations.py +127 -0
  3. rbx/autoenum.py +333 -0
  4. rbx/box/__init__.py +0 -0
  5. rbx/box/builder.py +77 -0
  6. rbx/box/cd.py +37 -0
  7. rbx/box/checkers.py +134 -0
  8. rbx/box/code.py +185 -0
  9. rbx/box/compile.py +56 -0
  10. rbx/box/conftest.py +42 -0
  11. rbx/box/contest/__init__.py +0 -0
  12. rbx/box/contest/build_contest_statements.py +347 -0
  13. rbx/box/contest/contest_package.py +76 -0
  14. rbx/box/contest/contest_utils.py +20 -0
  15. rbx/box/contest/main.py +179 -0
  16. rbx/box/contest/schema.py +155 -0
  17. rbx/box/contest/statements.py +82 -0
  18. rbx/box/creation.py +72 -0
  19. rbx/box/download.py +64 -0
  20. rbx/box/environment.py +345 -0
  21. rbx/box/extensions.py +26 -0
  22. rbx/box/generators.py +478 -0
  23. rbx/box/generators_test.py +63 -0
  24. rbx/box/main.py +449 -0
  25. rbx/box/package.py +316 -0
  26. rbx/box/packaging/boca/extension.py +27 -0
  27. rbx/box/packaging/boca/packager.py +245 -0
  28. rbx/box/packaging/contest_main.py +82 -0
  29. rbx/box/packaging/main.py +68 -0
  30. rbx/box/packaging/packager.py +117 -0
  31. rbx/box/packaging/polygon/packager.py +320 -0
  32. rbx/box/packaging/polygon/test.py +81 -0
  33. rbx/box/packaging/polygon/xml_schema.py +106 -0
  34. rbx/box/presets/__init__.py +503 -0
  35. rbx/box/presets/fetch.py +70 -0
  36. rbx/box/presets/lock_schema.py +20 -0
  37. rbx/box/presets/schema.py +59 -0
  38. rbx/box/schema.py +394 -0
  39. rbx/box/solutions.py +792 -0
  40. rbx/box/solutions_test.py +41 -0
  41. rbx/box/statements/__init__.py +0 -0
  42. rbx/box/statements/build_statements.py +359 -0
  43. rbx/box/statements/builders.py +375 -0
  44. rbx/box/statements/joiners.py +113 -0
  45. rbx/box/statements/latex.py +47 -0
  46. rbx/box/statements/latex_jinja.py +214 -0
  47. rbx/box/statements/schema.py +138 -0
  48. rbx/box/stresses.py +292 -0
  49. rbx/box/stressing/__init__.py +0 -0
  50. rbx/box/stressing/finder_parser.py +359 -0
  51. rbx/box/stressing/generator_parser.py +258 -0
  52. rbx/box/testcases.py +54 -0
  53. rbx/box/ui/__init__.py +0 -0
  54. rbx/box/ui/captured_log.py +372 -0
  55. rbx/box/ui/css/app.tcss +48 -0
  56. rbx/box/ui/main.py +38 -0
  57. rbx/box/ui/run.py +209 -0
  58. rbx/box/validators.py +245 -0
  59. rbx/box/validators_test.py +15 -0
  60. rbx/checker.py +128 -0
  61. rbx/clone.py +197 -0
  62. rbx/config.py +271 -0
  63. rbx/conftest.py +38 -0
  64. rbx/console.py +27 -0
  65. rbx/create.py +37 -0
  66. rbx/edit.py +24 -0
  67. rbx/grading/__init__.py +0 -0
  68. rbx/grading/caching.py +356 -0
  69. rbx/grading/conftest.py +33 -0
  70. rbx/grading/judge/__init__.py +0 -0
  71. rbx/grading/judge/cacher.py +503 -0
  72. rbx/grading/judge/digester.py +35 -0
  73. rbx/grading/judge/sandbox.py +748 -0
  74. rbx/grading/judge/sandboxes/__init__.py +0 -0
  75. rbx/grading/judge/sandboxes/isolate.py +683 -0
  76. rbx/grading/judge/sandboxes/stupid_sandbox.py +310 -0
  77. rbx/grading/judge/sandboxes/timeit.py +217 -0
  78. rbx/grading/judge/storage.py +284 -0
  79. rbx/grading/judge/test.py +38 -0
  80. rbx/grading/judge/testiso.py +54 -0
  81. rbx/grading/steps.py +522 -0
  82. rbx/grading/steps_with_caching.py +59 -0
  83. rbx/grading/steps_with_caching_run_test.py +429 -0
  84. rbx/grading_utils.py +148 -0
  85. rbx/hydration.py +101 -0
  86. rbx/main.py +122 -0
  87. rbx/metadata.py +105 -0
  88. rbx/providers/__init__.py +43 -0
  89. rbx/providers/codeforces.py +73 -0
  90. rbx/providers/provider.py +26 -0
  91. rbx/resources/checkers/boilerplate.cpp +20 -0
  92. rbx/resources/default_config.json +48 -0
  93. rbx/resources/envs/default.rbx.yml +37 -0
  94. rbx/resources/envs/isolate.rbx.yml +37 -0
  95. rbx/resources/packagers/boca/checker.sh +43 -0
  96. rbx/resources/packagers/boca/compare +53 -0
  97. rbx/resources/packagers/boca/compile/c +172 -0
  98. rbx/resources/packagers/boca/compile/cc +173 -0
  99. rbx/resources/packagers/boca/compile/cpp +172 -0
  100. rbx/resources/packagers/boca/compile/java +194 -0
  101. rbx/resources/packagers/boca/compile/kt +155 -0
  102. rbx/resources/packagers/boca/compile/pas +172 -0
  103. rbx/resources/packagers/boca/compile/py2 +173 -0
  104. rbx/resources/packagers/boca/compile/py3 +173 -0
  105. rbx/resources/packagers/boca/run/c +128 -0
  106. rbx/resources/packagers/boca/run/cc +128 -0
  107. rbx/resources/packagers/boca/run/cpp +128 -0
  108. rbx/resources/packagers/boca/run/java +194 -0
  109. rbx/resources/packagers/boca/run/kt +159 -0
  110. rbx/resources/packagers/boca/run/py2 +166 -0
  111. rbx/resources/packagers/boca/run/py3 +166 -0
  112. rbx/resources/presets/default/contest/contest.rbx.yml +14 -0
  113. rbx/resources/presets/default/contest/statement/contest.rbx.tex +97 -0
  114. rbx/resources/presets/default/contest/statement/olymp.sty +250 -0
  115. rbx/resources/presets/default/contest/statement/template.rbx.tex +42 -0
  116. rbx/resources/presets/default/preset.rbx.yml +12 -0
  117. rbx/resources/presets/default/problem/.gitignore +6 -0
  118. rbx/resources/presets/default/problem/gen.cpp +9 -0
  119. rbx/resources/presets/default/problem/problem.rbx.yml +44 -0
  120. rbx/resources/presets/default/problem/random.py +3 -0
  121. rbx/resources/presets/default/problem/random.txt +2 -0
  122. rbx/resources/presets/default/problem/sols/main.cpp +9 -0
  123. rbx/resources/presets/default/problem/sols/slow.cpp +15 -0
  124. rbx/resources/presets/default/problem/sols/wa.cpp +9 -0
  125. rbx/resources/presets/default/problem/statement/olymp.sty +250 -0
  126. rbx/resources/presets/default/problem/statement/projecao.png +0 -0
  127. rbx/resources/presets/default/problem/statement/statement.rbx.tex +18 -0
  128. rbx/resources/presets/default/problem/statement/template.rbx.tex +89 -0
  129. rbx/resources/presets/default/problem/tests/samples/000.in +1 -0
  130. rbx/resources/presets/default/problem/tests/samples/001.in +1 -0
  131. rbx/resources/presets/default/problem/validator.cpp +16 -0
  132. rbx/resources/presets/default/problem/wcmp.cpp +34 -0
  133. rbx/resources/templates/template.cpp +19 -0
  134. rbx/run.py +45 -0
  135. rbx/schema.py +64 -0
  136. rbx/submit.py +61 -0
  137. rbx/submitors/__init__.py +18 -0
  138. rbx/submitors/codeforces.py +120 -0
  139. rbx/submitors/submitor.py +25 -0
  140. rbx/test.py +347 -0
  141. rbx/testcase.py +70 -0
  142. rbx/testcase_rendering.py +79 -0
  143. rbx/testdata/box1/gen1.cpp +7 -0
  144. rbx/testdata/box1/gen2.cpp +9 -0
  145. rbx/testdata/box1/genScript.py +2 -0
  146. rbx/testdata/box1/hard-tle.sol.cpp +26 -0
  147. rbx/testdata/box1/ole.cpp +17 -0
  148. rbx/testdata/box1/problem.rbx.yml +39 -0
  149. rbx/testdata/box1/re.sol.cpp +23 -0
  150. rbx/testdata/box1/sol.cpp +22 -0
  151. rbx/testdata/box1/tests/1.in +1 -0
  152. rbx/testdata/box1/tle-and-incorrect.sol.cpp +33 -0
  153. rbx/testdata/box1/tle.sol.cpp +35 -0
  154. rbx/testdata/box1/validator.cpp +11 -0
  155. rbx/testdata/box1/wa.sol.cpp +22 -0
  156. rbx/testdata/caching/executable.py +1 -0
  157. rbx/testdata/compatible +0 -0
  158. rbx/testing_utils.py +65 -0
  159. rbx/utils.py +162 -0
  160. rbx_cp-0.5.0.dist-info/LICENSE +201 -0
  161. rbx_cp-0.5.0.dist-info/METADATA +89 -0
  162. rbx_cp-0.5.0.dist-info/RECORD +164 -0
  163. rbx_cp-0.5.0.dist-info/WHEEL +4 -0
  164. rbx_cp-0.5.0.dist-info/entry_points.txt +4 -0
File without changes
@@ -0,0 +1,683 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import pathlib
6
+ import shutil
7
+ import stat
8
+ import subprocess
9
+ import tempfile
10
+ from typing import IO, Any, Dict, List, Optional
11
+
12
+ from rbx.config import get_app_path
13
+ from rbx.grading.judge.cacher import FileCacher
14
+ from rbx.grading.judge.sandbox import (
15
+ SandboxBase,
16
+ SandboxParams,
17
+ wait_without_std,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class IsolateSandbox(SandboxBase):
24
+ """This class creates, deletes and manages the interaction with a
25
+ sandbox. The sandbox doesn't support concurrent operation, not
26
+ even for reading.
27
+
28
+ The Sandbox offers API for retrieving and storing file, as well as
29
+ executing programs in a controlled environment. There are anyway a
30
+ few files reserved for use by the Sandbox itself:
31
+
32
+ * commands.log: a text file with the commands ran into this
33
+ Sandbox, one for each line;
34
+
35
+ * run.log.N: for each N, the log produced by the sandbox when running
36
+ command number N.
37
+
38
+ """
39
+
40
+ next_id = 0
41
+
42
+ # If the command line starts with this command name, we are just
43
+ # going to execute it without sandboxing, and with all permissions
44
+ # on the current directory.
45
+ SECURE_COMMANDS = ['/bin/cp', '/bin/mv', '/usr/bin/zip', '/usr/bin/unzip']
46
+
47
+ log: Optional[Dict[str, Any]]
48
+
49
+ def __init__(
50
+ self,
51
+ file_cacher: Optional[FileCacher] = None,
52
+ name: Optional[str] = None,
53
+ temp_dir: Optional[pathlib.Path] = None,
54
+ params: Optional[SandboxParams] = None,
55
+ debug: bool = False,
56
+ ):
57
+ """Initialization.
58
+
59
+ For arguments documentation, see SandboxBase.__init__.
60
+
61
+ """
62
+ if not temp_dir:
63
+ temp_dir = pathlib.Path(tempfile.gettempdir())
64
+ SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
65
+
66
+ self.box_id = IsolateSandbox.next_id % 10
67
+ IsolateSandbox.next_id += 1
68
+
69
+ # We create a directory "home" inside the outer temporary directory,
70
+ # that will be bind-mounted to "/tmp" inside the sandbox (some
71
+ # compilers need "/tmp" to exist, and this is a cheap shortcut to
72
+ # create it). The sandbox also runs code as a different user, and so
73
+ # we need to ensure that they can read and write to the directory.
74
+ # But we don't want everybody on the system to, which is why the
75
+ # outer directory exists with no read permissions.
76
+ self._outer_dir = pathlib.Path(
77
+ tempfile.mkdtemp(dir=str(self.temp_dir), prefix='cms-%s-' % (self.name))
78
+ )
79
+ self._home = self._outer_dir / 'home'
80
+ self._home_dest = pathlib.PosixPath('/tmp')
81
+ self._home.mkdir(parents=True, exist_ok=True)
82
+ self.allow_writing_all()
83
+
84
+ self.exec_name = 'isolate'
85
+ self.box_exec = self.detect_box_executable()
86
+ # Used for -M - the meta file ends up in the outer directory. The
87
+ # actual filename will be <info_basename>.<execution_number>.
88
+ self.info_basename = self._outer_dir / 'run.log'
89
+ self.log = None
90
+ self.exec_num = -1
91
+ self.cmd_file = self._outer_dir / 'commands.log'
92
+ self.chdir = self._home_dest
93
+ self.debug = debug
94
+ logger.debug(
95
+ "Sandbox in `%s' created, using box `%s'.", self._home, self.box_exec
96
+ )
97
+
98
+ # Ensure we add a few extra things to params.
99
+ self.set_params(params or SandboxParams())
100
+
101
+ # Tell isolate to get the sandbox ready. We do our best to cleanup
102
+ # after ourselves, but we might have missed something if a previous
103
+ # worker was interrupted in the middle of an execution, so we issue an
104
+ # idempotent cleanup.
105
+ self.cleanup()
106
+ self.initialize()
107
+
108
+ def set_params(self, params: SandboxParams):
109
+ """Set the parameters of the sandbox.
110
+
111
+ params (SandboxParams): the parameters to set.
112
+
113
+ """
114
+ super().set_params(params)
115
+ self.add_mapped_directory(self._home, dest=self._home_dest, options='rw')
116
+
117
+ # Set common environment variables.
118
+ # Specifically needed by Python, that searches the home for
119
+ # packages.
120
+ self.params.set_env['HOME'] = str(self._home_dest)
121
+
122
+ def add_mapped_directory(
123
+ self,
124
+ src: pathlib.Path,
125
+ dest: Optional[pathlib.Path] = None,
126
+ options: Optional[str] = None,
127
+ ignore_if_not_existing: bool = False,
128
+ ):
129
+ """Add src to the directory to be mapped inside the sandbox.
130
+
131
+ src (Path): directory to make visible.
132
+ dest (Path|None): if not None, the path where to bind src.
133
+ options (str|None): if not None, isolate's directory rule options.
134
+ ignore_if_not_existing (bool): if True, ignore the mapping when src
135
+ does not exist (instead of having isolate terminate with an
136
+ error).
137
+
138
+ """
139
+ self.params.add_mapped_directory(
140
+ src, dest, options, ignore_if_not_existing=ignore_if_not_existing
141
+ )
142
+
143
+ def maybe_add_mapped_directory(
144
+ self,
145
+ src: pathlib.Path,
146
+ dest: Optional[pathlib.Path] = None,
147
+ options: Optional[str] = None,
148
+ ):
149
+ """Same as add_mapped_directory, with ignore_if_not_existing."""
150
+ return self.add_mapped_directory(
151
+ src, dest, options, ignore_if_not_existing=True
152
+ )
153
+
154
+ def allow_writing_all(self):
155
+ """Set permissions in such a way that any operation is allowed."""
156
+ self._home.chmod(0o777)
157
+ for child in self._home.iterdir():
158
+ child.chmod(0o777)
159
+
160
+ def allow_writing_none(self):
161
+ """Set permissions in such a way that the user cannot write anything."""
162
+ self._home.chmod(0o755)
163
+ for child in self._home.iterdir():
164
+ child.chmod(0o755)
165
+
166
+ def allow_writing_only(self, inner_paths: List[pathlib.Path]):
167
+ """Set permissions in so that the user can write only some paths.
168
+
169
+ By default the user can only write to the home directory. This
170
+ method further restricts permissions so that it can only write
171
+ to some files inside the home directory.
172
+
173
+ inner_paths ([Path]): the only paths that the user is allowed to
174
+ write to; they should be "inner" paths (from the perspective
175
+ of the sandboxed process, not of the host system); they can
176
+ be absolute or relative (in which case they are interpreted
177
+ relative to the home directory); paths that point to a file
178
+ outside the home directory are ignored.
179
+
180
+ """
181
+ outer_paths: List[pathlib.Path] = []
182
+ for inner_path in inner_paths:
183
+ abs_inner_path = (self._home_dest / inner_path).resolve()
184
+ # If an inner path is absolute (e.g., /fifo0/u0_to_m) then
185
+ # it may be outside home and we should ignore it.
186
+ if not abs_inner_path.is_relative_to(self._home_dest.resolve()):
187
+ continue
188
+ rel_inner_path = abs_inner_path.relative_to(self._home_dest)
189
+ outer_path = self._home / rel_inner_path
190
+ outer_paths.append(outer_path)
191
+
192
+ # If one of the specified file do not exists, we touch it to
193
+ # assign the correct permissions.
194
+ for path in outer_paths:
195
+ if not path.exists():
196
+ path.touch()
197
+
198
+ # Close everything, then open only the specified.
199
+ self.allow_writing_none()
200
+ for path in outer_paths:
201
+ path.chmod(0o722)
202
+
203
+ def get_root_path(self) -> pathlib.Path:
204
+ """Return the toplevel path of the sandbox.
205
+
206
+ return (Path): the root path.
207
+
208
+ """
209
+ return self._outer_dir
210
+
211
+ def relative_path(self, path: pathlib.Path) -> pathlib.Path:
212
+ """Translate from a relative path inside the sandbox to a system path.
213
+
214
+ path (Path): relative path of the file inside the sandbox.
215
+
216
+ return (Path): the absolute path.
217
+
218
+ """
219
+ return self._home / path
220
+
221
+ def detect_box_executable(self) -> pathlib.Path:
222
+ """Try to find an isolate executable. It first looks in
223
+ ./isolate/, then the local directory, then in a relative path
224
+ from the file that contains the Sandbox module, then in the
225
+ system paths.
226
+
227
+ return (Path): the path to a valid (hopefully) isolate.
228
+
229
+ """
230
+ paths: List[pathlib.Path] = [
231
+ pathlib.PosixPath('./isolate') / self.exec_name,
232
+ pathlib.PosixPath('.') / self.exec_name,
233
+ get_app_path() / self.exec_name,
234
+ pathlib.PosixPath('/usr/local/bin') / self.exec_name,
235
+ pathlib.PosixPath(self.exec_name),
236
+ ]
237
+ for path in paths:
238
+ # Consider only non-directory, executable files with SUID flag on.
239
+ if path.exists() and not path.is_dir() and os.access(str(path), os.X_OK):
240
+ st = path.stat()
241
+ if st.st_mode & stat.S_ISUID != 0:
242
+ return path
243
+
244
+ # As default, return self.exec_name alone, that means that
245
+ # system path is used.
246
+ return paths[-1]
247
+
248
+ def build_box_options(self) -> List[str]:
249
+ """Translate the options defined in the instance to a string
250
+ that can be postponed to isolate as an arguments list.
251
+
252
+ return ([string]): the arguments list as strings.
253
+
254
+ """
255
+ res = list()
256
+ if self.box_id is not None:
257
+ res += [f'--box-id={self.box_id}']
258
+ if self.params.cgroup:
259
+ res += ['--cg']
260
+ if self.chdir is not None:
261
+ res += [f'--chdir={str(self.chdir)}']
262
+ for dirmount in self.params.dirs:
263
+ s = str(dirmount.dst) + '=' + str(dirmount.src)
264
+ if dirmount.options is not None:
265
+ s += ':' + dirmount.options
266
+ res += [f'--dir={s}']
267
+ if self.params.preserve_env:
268
+ res += ['--full-env']
269
+ for var in self.params.inherit_env:
270
+ res += [f'--env={var}']
271
+ for var, value in self.params.set_env.items():
272
+ res += [f'--env={var}={value}']
273
+ if self.params.fsize is not None:
274
+ # Isolate wants file size as KiB.
275
+ fsize = self.params.fsize
276
+ res += [f'--fsize={fsize}']
277
+ if self.params.stdin_file is not None:
278
+ inner_stdin = self.inner_absolute_path(self.params.stdin_file)
279
+ res += ['--stdin=%s' % str(inner_stdin)]
280
+ if self.params.stack_space is not None:
281
+ # Isolate wants stack size as KiB.
282
+ stack_space = self.params.stack_space * 1024
283
+ res += [f'--stack={stack_space}']
284
+ if self.params.address_space is not None:
285
+ # Isolate wants memory size as KiB.
286
+ address_space = self.params.address_space * 1024
287
+ if self.params.cgroup:
288
+ res += [f'--cg-mem={address_space}']
289
+ else:
290
+ res += [f'--mem={address_space}']
291
+ if self.params.stdout_file is not None:
292
+ inner_stdout = self.inner_absolute_path(self.params.stdout_file)
293
+ res += ['--stdout=%s' % str(inner_stdout)]
294
+ if self.params.max_processes is not None:
295
+ res += [f'--processes={self.params.max_processes}']
296
+ else:
297
+ res += ['--processes']
298
+ if self.params.stderr_file is not None:
299
+ inner_stderr = self.inner_absolute_path(self.params.stderr_file)
300
+ res += ['--stderr=%s' % str(inner_stderr)]
301
+ if self.params.timeout is not None:
302
+ # Isolate wants time in seconds.
303
+ timeout = float(self.params.timeout) / 1000
304
+ res += ['--time=%g' % timeout]
305
+ res += ['--verbose'] * self.params.verbosity
306
+ if self.params.wallclock_timeout is not None:
307
+ wallclock_timeout = float(self.params.wallclock_timeout) / 1000
308
+ res += ['--wall-time=%g' % wallclock_timeout]
309
+ if self.params.extra_timeout is not None:
310
+ extra_timeout = float(self.params.extra_timeout) / 1000
311
+ res += ['--extra-time=%g' % extra_timeout]
312
+ res += ['--meta=%s' % ('%s.%d' % (self.info_basename, self.exec_num))]
313
+ res += ['--run']
314
+ return res
315
+
316
+ def hydrate_logs(self):
317
+ """Read the content of the log file of the sandbox (usually
318
+ run.log.N for some integer N), and set self.log as a dict
319
+ containing the info in the log file (time, memory, status,
320
+ ...).
321
+
322
+ """
323
+ # self.log is a dictionary of lists (usually lists of length
324
+ # one).
325
+ self.log = {}
326
+ info_file = pathlib.Path('%s.%d' % (self.info_basename, self.exec_num))
327
+ try:
328
+ with self.get_file_text(info_file) as log_file:
329
+ for line in log_file:
330
+ key, value = line.strip().split(':', 1)
331
+ if key in self.log:
332
+ self.log[key].append(value)
333
+ else:
334
+ self.log[key] = [value]
335
+ except OSError as error:
336
+ raise OSError(
337
+ 'Error while reading execution log file %s. %r' % (info_file, error)
338
+ ) from error
339
+
340
+ def get_execution_time(self) -> Optional[float]:
341
+ """Return the time spent in the sandbox, reading the logs if
342
+ necessary.
343
+
344
+ return (float): time spent in the sandbox.
345
+
346
+ """
347
+ assert self.log is not None
348
+ if 'time' in self.log:
349
+ return float(self.log['time'][0])
350
+ return None
351
+
352
+ def get_execution_wall_clock_time(self) -> Optional[float]:
353
+ """Return the total time from the start of the sandbox to the
354
+ conclusion of the task, reading the logs if necessary.
355
+
356
+ return (float): total time the sandbox was alive.
357
+
358
+ """
359
+ assert self.log is not None
360
+ if 'time-wall' in self.log:
361
+ return float(self.log['time-wall'][0])
362
+ return None
363
+
364
+ def use_soft_timeout(self) -> bool:
365
+ return True
366
+
367
+ def get_memory_used(self) -> Optional[int]:
368
+ """Return the memory used by the sandbox, reading the logs if
369
+ necessary.
370
+
371
+ return (int): memory used by the sandbox (in kbytes).
372
+
373
+ """
374
+ assert self.log is not None
375
+ if 'cg-mem' in self.log:
376
+ # Isolate returns memory measurements in KiB.
377
+ return int(self.log['cg-mem'][0])
378
+ return None
379
+
380
+ def get_killing_signal(self) -> int:
381
+ """Return the signal that killed the sandboxed process,
382
+ reading the logs if necessary.
383
+
384
+ return (int): offending signal, or 0.
385
+
386
+ """
387
+ assert self.log is not None
388
+ if 'exitsig' in self.log:
389
+ return int(self.log['exitsig'][0])
390
+ return 0
391
+
392
+ def get_exit_code(self) -> int:
393
+ """Return the exit code of the sandboxed process, reading the
394
+ logs if necessary.
395
+
396
+ return (int): exitcode, or 0.
397
+
398
+ """
399
+ assert self.log is not None
400
+ if 'exitcode' in self.log:
401
+ return int(self.log['exitcode'][0])
402
+ return 0
403
+
404
+ def get_status_list(self) -> List[str]:
405
+ """Reads the sandbox log file, and set and return the status
406
+ of the sandbox.
407
+
408
+ return (list): list of statuses of the sandbox.
409
+
410
+ """
411
+ assert self.log is not None
412
+ if 'status' in self.log:
413
+ return self.log['status']
414
+ return []
415
+
416
+ def get_exit_status(self) -> str:
417
+ """Get the list of statuses of the sandbox and return the most
418
+ important one.
419
+
420
+ return (string): the main reason why the sandbox terminated.
421
+
422
+ """
423
+ assert self.log is not None
424
+ status_list = self.get_status_list()
425
+ if 'XX' in status_list:
426
+ return self.EXIT_SANDBOX_ERROR
427
+ elif 'TO' in status_list:
428
+ if 'message' in self.log and 'wall' in self.log['message'][0]:
429
+ return self.EXIT_TIMEOUT_WALL
430
+ else:
431
+ return self.EXIT_TIMEOUT
432
+ elif 'SG' in status_list:
433
+ return self.EXIT_SIGNAL
434
+ elif 'RE' in status_list:
435
+ return self.EXIT_NONZERO_RETURN
436
+ # OK status is not reported in the log file, it's implicit.
437
+ return self.EXIT_OK
438
+
439
+ def get_human_exit_description(self) -> str:
440
+ """Get the status of the sandbox and return a human-readable
441
+ string describing it.
442
+
443
+ return (string): human-readable explaination of why the
444
+ sandbox terminated.
445
+
446
+ """
447
+ status = self.get_exit_status()
448
+ if status == self.EXIT_OK:
449
+ return (
450
+ 'Execution successfully finished (with exit code %d)'
451
+ % self.get_exit_code()
452
+ )
453
+ elif status == self.EXIT_SANDBOX_ERROR:
454
+ return 'Execution failed because of sandbox error'
455
+ elif status == self.EXIT_TIMEOUT:
456
+ return 'Execution timed out'
457
+ elif status == self.EXIT_TIMEOUT_WALL:
458
+ return 'Execution timed out (wall clock limit exceeded)'
459
+ elif status == self.EXIT_SIGNAL:
460
+ return 'Execution killed with signal %s' % self.get_killing_signal()
461
+ elif status == self.EXIT_NONZERO_RETURN:
462
+ return 'Execution failed because the return code was nonzero'
463
+ return ''
464
+
465
+ def inner_absolute_path(self, path: pathlib.Path) -> pathlib.Path:
466
+ """Translate from a relative path inside the sandbox to an
467
+ absolute path inside the sandbox.
468
+
469
+ path (string): relative path of the file inside the sandbox.
470
+
471
+ return (string): the absolute path of the file inside the sandbox.
472
+
473
+ """
474
+ return self._home_dest / path
475
+
476
+ def _popen(
477
+ self,
478
+ command: List[str],
479
+ stdin: Optional[IO[bytes] | int] = None,
480
+ stdout: Optional[IO[bytes] | int] = None,
481
+ stderr: Optional[IO[bytes] | int] = None,
482
+ close_fds: bool = True,
483
+ ) -> subprocess.Popen:
484
+ """Execute the given command in the sandbox using
485
+ subprocess.Popen, assigning the corresponding standard file
486
+ descriptors.
487
+
488
+ command ([string]): executable filename and arguments of the
489
+ command.
490
+ stdin (file|None): a file descriptor.
491
+ stdout (file|None): a file descriptor.
492
+ stderr (file|None): a file descriptor.
493
+ close_fds (bool): close all file descriptor before executing.
494
+
495
+ return (Popen): popen object.
496
+
497
+ """
498
+ self.log = None
499
+ self.exec_num += 1
500
+
501
+ # We run a selection of commands without isolate, as they need
502
+ # to create new files. This is safe because these commands do
503
+ # not depend on the user input.
504
+ if command[0] in IsolateSandbox.SECURE_COMMANDS:
505
+ logger.debug(
506
+ 'Executing non-securely: %s at %s',
507
+ str(command),
508
+ self._home,
509
+ )
510
+ try:
511
+ prev_permissions = stat.S_IMODE(self._home.stat().st_mode)
512
+ self._home.chmod(0o700)
513
+ with open(self.cmd_file, 'at', encoding='utf-8') as cmds:
514
+ cmds.write('%s\n' % str(command))
515
+ p = subprocess.Popen(
516
+ command,
517
+ cwd=str(self._home),
518
+ stdin=stdin,
519
+ stdout=stdout,
520
+ stderr=stderr,
521
+ close_fds=close_fds,
522
+ )
523
+ self._home.chmod(prev_permissions)
524
+ # For secure commands, we clear the output so that it
525
+ # is not forwarded to the contestants. Secure commands
526
+ # are "setup" commands, which should not fail or
527
+ # provide information for the contestants.
528
+ if self.params.stdout_file:
529
+ (self._home / self.params.stdout_file).open('wb').close()
530
+ if self.params.stderr_file:
531
+ (self._home / self.params.stderr_file).open('wb').close()
532
+ self._write_empty_run_log(self.exec_num)
533
+ except OSError:
534
+ logger.critical(
535
+ 'Failed to execute program in sandbox with command: %s',
536
+ str(command),
537
+ exc_info=True,
538
+ )
539
+ raise
540
+ return p
541
+
542
+ args = [self.box_exec] + self.build_box_options() + ['--'] + command
543
+ logger.debug(
544
+ "Executing program in sandbox with command: `%s'.",
545
+ str(args),
546
+ )
547
+ # Temporarily allow writing new files.
548
+ prev_permissions = stat.S_IMODE(self._home.stat().st_mode)
549
+ self._home.chmod(0o700)
550
+ with open(self.cmd_file, 'at', encoding='utf-8') as commands:
551
+ commands.write('%s\n' % (str(args)))
552
+ self._home.chmod(prev_permissions)
553
+ try:
554
+ p = subprocess.Popen(
555
+ args, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=close_fds
556
+ )
557
+ except OSError:
558
+ logger.critical(
559
+ 'Failed to execute program in sandbox ' 'with command: %s',
560
+ str(args),
561
+ exc_info=True,
562
+ )
563
+ raise
564
+
565
+ return p
566
+
567
+ def _write_empty_run_log(self, index: int):
568
+ """Write a fake run.log file with no information."""
569
+ info_file = pathlib.PosixPath('%s.%d' % (self.info_basename, index))
570
+ with info_file.open('wt', encoding='utf-8') as f:
571
+ f.write('time:0.000\n')
572
+ f.write('time-wall:0.000\n')
573
+ f.write('max-rss:0\n')
574
+ f.write('cg-mem:0\n')
575
+
576
+ def execute_without_std(
577
+ self,
578
+ command: List[str],
579
+ ) -> bool:
580
+ """Execute the given command in the sandbox using
581
+ subprocess.Popen and discarding standard input, output and
582
+ error. More specifically, the standard input gets closed just
583
+ after the execution has started; standard output and error are
584
+ read until the end, in a way that prevents the execution from
585
+ being blocked because of insufficient buffering.
586
+
587
+ command ([string]): executable filename and arguments of the
588
+ command.
589
+
590
+ return (bool|Popen): return True if the sandbox didn't report
591
+ errors (caused by the sandbox itself), False otherwise.
592
+ """
593
+ popen = self._popen(
594
+ command,
595
+ stdin=subprocess.PIPE,
596
+ stdout=subprocess.PIPE,
597
+ stderr=subprocess.PIPE,
598
+ close_fds=True,
599
+ )
600
+
601
+ # If the caller wants us to wait for completion, we also avoid
602
+ # std*** to interfere with command. Otherwise we let the
603
+ # caller handle these issues.
604
+ with popen as p:
605
+ exitcode = self.translate_box_exitcode(
606
+ wait_without_std([p], actually_pipe_to_stdout=self.debug)[0]
607
+ )
608
+ self.hydrate_logs()
609
+ return exitcode
610
+
611
+ def translate_box_exitcode(self, exitcode: int) -> bool:
612
+ """Translate the sandbox exit code to a boolean sandbox success.
613
+
614
+ Isolate emits the following exit codes:
615
+ * 0 -> both sandbox and internal process finished successfully (meta
616
+ file will contain "status:OK" -> return True;
617
+ * 1 -> sandbox finished successfully, but internal process was
618
+ terminated, e.g., due to timeout (meta file will contain
619
+ status:x" with x in (TO, SG, RE)) -> return True;
620
+ * 2 -> sandbox terminated with an error (meta file will contain
621
+ "status:XX") -> return False.
622
+
623
+ """
624
+ if exitcode == 0 or exitcode == 1:
625
+ return True
626
+ elif exitcode == 2:
627
+ return False
628
+ else:
629
+ raise Exception('Sandbox exit status (%d) unknown' % exitcode)
630
+
631
+ def initialize(self):
632
+ """Initialize isolate's box."""
633
+ init_cmd = (
634
+ [self.box_exec]
635
+ + (['--cg'] if self.params.cgroup else [])
636
+ + ['--box-id=%d' % self.box_id, '--init']
637
+ )
638
+ try:
639
+ subprocess.check_call(init_cmd)
640
+ except subprocess.CalledProcessError as e:
641
+ raise Exception('Failed to initialize sandbox') from e
642
+
643
+ def cleanup(self, delete: bool = False):
644
+ """See Sandbox.cleanup()."""
645
+ # The user isolate assigns within the sandbox might have created
646
+ # subdirectories and files therein, making the user outside the sandbox
647
+ # unable to delete the whole tree. If the caller asked us to delete the
648
+ # sandbox, we first issue a chmod within isolate to make sure that we
649
+ # will be able to delete everything. If not, we leave the files as they
650
+ # are to avoid masking possible problems the admin wanted to debug.
651
+
652
+ exe = (
653
+ [self.box_exec]
654
+ + (['--cg'] if self.params.cgroup else [])
655
+ + ['--box-id=%d' % self.box_id]
656
+ )
657
+
658
+ if delete:
659
+ # Ignore exit status as some files may be owned by our user
660
+ subprocess.call(
661
+ exe
662
+ + [
663
+ '--dir=%s=%s:rw' % (str(self._home_dest), str(self._home)),
664
+ '--run',
665
+ '--',
666
+ '/bin/chmod',
667
+ '777',
668
+ '-R',
669
+ str(self._home_dest),
670
+ ],
671
+ stdout=subprocess.DEVNULL,
672
+ stderr=subprocess.STDOUT,
673
+ )
674
+
675
+ # Tell isolate to cleanup the sandbox.
676
+ subprocess.check_call(
677
+ exe + ['--cleanup'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
678
+ )
679
+
680
+ if delete:
681
+ logger.debug('Deleting sandbox in %s.', self._outer_dir)
682
+ # Delete the working directory.
683
+ shutil.rmtree(str(self._outer_dir))