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