rbx.cp 0.13.4__py3-none-any.whl → 0.13.5__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.
rbx/grading/caching.py CHANGED
@@ -88,15 +88,26 @@ def _check_digests(artifacts_list: List[GradingArtifacts]):
88
88
  produced.add(id(output.digest))
89
89
 
90
90
 
91
- def _build_digest_list(artifacts_list: List[GradingArtifacts]) -> List[DigestHolder]:
92
- digests = []
91
+ def _build_artifact_with_digest_list(
92
+ artifacts_list: List[GradingArtifacts],
93
+ ) -> List[GradingFileOutput]:
94
+ outputs = []
93
95
  for artifacts in artifacts_list:
94
96
  for output in artifacts.outputs:
95
97
  if output.hash and output.digest is None:
96
98
  output.digest = DigestHolder()
97
99
  if output.digest is None:
98
100
  continue
99
- digests.append(output.digest)
101
+ outputs.append(output)
102
+ return outputs
103
+
104
+
105
+ def _build_digest_list(artifacts_list: List[GradingArtifacts]) -> List[DigestHolder]:
106
+ outputs = _build_artifact_with_digest_list(artifacts_list)
107
+ digests = []
108
+ for output in outputs:
109
+ assert output.digest is not None
110
+ digests.append(output.digest)
100
111
  return digests
101
112
 
102
113
 
@@ -115,27 +126,44 @@ def _build_fingerprint_list(
115
126
  return fingerprints
116
127
 
117
128
 
118
- def _maybe_check_integrity(output: GradingFileOutput):
129
+ def _maybe_check_integrity(output: GradingFileOutput, integrity_digest: str):
119
130
  if not grading_context.should_check_integrity():
120
131
  return
121
- if output.dest is None or not output.dest.is_symlink():
132
+ if not output.hash:
133
+ return
134
+ if output.dest is None or not output.dest.is_symlink() or not output.dest.is_file():
135
+ # Only makes sense if the file EXISTS and IS A SYMLINK pointing to an
136
+ # EXISTING storage file.
137
+ # If the storage file ceases to exist, we can simply evict from the cache.
122
138
  return
123
- if output.digest is None or output.digest.value is None:
139
+ if output.digest is None:
124
140
  return
125
141
  with output.dest.open('rb') as f:
126
- fingerprint = digest_cooperatively(f)
127
- if fingerprint != output.digest.value:
142
+ output_digest = digest_cooperatively(f)
143
+ if output_digest != integrity_digest:
128
144
  raise ValueError(
129
145
  f'Cache was tampered with, file {output.dest} has changed since it was cached.\nPlease run `rbx clean` to reset the cache.'
130
146
  )
131
147
 
132
148
 
133
- def _build_output_fingerprint_list(artifacts_list: List[GradingArtifacts]) -> List[str]:
149
+ def _check_digest_list_integrity(
150
+ artifacts_list: List[GradingArtifacts], integrity_digests: List[Optional[str]]
151
+ ):
152
+ outputs = _build_artifact_with_digest_list(artifacts_list)
153
+ assert len(outputs) == len(integrity_digests)
154
+ for output, integrity_digest in zip(outputs, integrity_digests):
155
+ assert output.digest is not None
156
+ if integrity_digest is None:
157
+ continue
158
+ _maybe_check_integrity(output, integrity_digest)
159
+
160
+
161
+ def _build_output_fingerprint_list(
162
+ artifacts_list: List[GradingArtifacts],
163
+ ) -> List[str]:
134
164
  fingerprints = []
135
165
  for artifacts in artifacts_list:
136
166
  for output in artifacts.outputs:
137
- if output.hash:
138
- _maybe_check_integrity(output)
139
167
  if output.dest is None or output.intermediate or output.hash:
140
168
  continue
141
169
  if not output.dest.is_file():
@@ -160,7 +188,10 @@ def _build_cache_fingerprint(
160
188
  ) -> CacheFingerprint:
161
189
  digests = [digest.value for digest in _build_digest_list(artifacts_list)]
162
190
  fingerprints = _build_fingerprint_list(artifacts_list, cacher)
163
- output_fingerprints = _build_output_fingerprint_list(artifacts_list)
191
+ output_fingerprints = _build_output_fingerprint_list(
192
+ artifacts_list,
193
+ )
194
+
164
195
  logs = _build_logs_list(artifacts_list)
165
196
  return CacheFingerprint(
166
197
  digests=digests,
@@ -389,7 +420,10 @@ class DependencyCache:
389
420
  if fingerprint is None:
390
421
  return False
391
422
 
392
- reference_fingerprint = _build_cache_fingerprint(artifact_list, self.cacher)
423
+ reference_fingerprint = _build_cache_fingerprint(
424
+ artifact_list,
425
+ self.cacher,
426
+ )
393
427
 
394
428
  if not _fingerprints_match(fingerprint, reference_fingerprint):
395
429
  self._evict_from_cache(key)
@@ -399,6 +433,11 @@ class DependencyCache:
399
433
  self._evict_from_cache(key)
400
434
  return False
401
435
 
436
+ # Check whether existing storage files were not tampered with.
437
+ _check_digest_list_integrity(
438
+ artifact_list,
439
+ fingerprint.digests,
440
+ )
402
441
  reference_digests = _build_digest_list(artifact_list)
403
442
 
404
443
  # Apply digest changes.
@@ -422,6 +461,10 @@ class DependencyCache:
422
461
  for logs, reference_logs in zip(fingerprint.logs, reference_fingerprint.logs):
423
462
  if logs.run is not None:
424
463
  reference_logs.run = logs.run.model_copy(deep=True)
464
+ if logs.interactor_run is not None:
465
+ reference_logs.interactor_run = logs.interactor_run.model_copy(
466
+ deep=True
467
+ )
425
468
  if logs.preprocess is not None:
426
469
  reference_logs.preprocess = [
427
470
  log.model_copy(deep=True) for log in logs.preprocess
@@ -448,4 +491,11 @@ class DependencyCache:
448
491
  if not are_artifacts_ok(artifact_list, self.cacher):
449
492
  return
450
493
 
451
- self._store_in_cache(key, _build_cache_fingerprint(artifact_list, self.cacher))
494
+ reference_fingerprint = _build_cache_fingerprint(
495
+ artifact_list,
496
+ self.cacher,
497
+ )
498
+ self._store_in_cache(
499
+ key,
500
+ reference_fingerprint,
501
+ )
@@ -0,0 +1,268 @@
1
+ import dataclasses
2
+ import os
3
+ import pathlib
4
+ import resource
5
+ import subprocess
6
+ import sys
7
+ import threading
8
+ import typing
9
+ from enum import Enum
10
+ from time import monotonic
11
+ from typing import IO, Any, Dict, List, Optional, Union
12
+
13
+ import psutil
14
+
15
+ from rbx.utils import PathOrStr
16
+
17
+ FileLike = Union[PathOrStr, IO[bytes], int]
18
+
19
+
20
+ def _maybe_close_files(files):
21
+ for fobj in files:
22
+ if isinstance(fobj, int):
23
+ continue
24
+ fobj.close()
25
+
26
+
27
+ def _is_pathlike(obj: Any) -> bool:
28
+ return isinstance(obj, str) or isinstance(obj, pathlib.Path)
29
+
30
+
31
+ @dataclasses.dataclass
32
+ class ProgramIO:
33
+ input: FileLike = subprocess.PIPE
34
+ output: FileLike = subprocess.PIPE
35
+ stderr: FileLike = subprocess.PIPE
36
+
37
+ def get_file_objects(self):
38
+ if isinstance(self.input, int):
39
+ input_fobj = self.input
40
+ elif _is_pathlike(self.input):
41
+ input_fobj = pathlib.Path(typing.cast(str, self.input)).open('r')
42
+ else:
43
+ input_fobj = typing.cast(IO[bytes], self.input)
44
+ if isinstance(self.output, int):
45
+ output_fobj = self.output
46
+ elif _is_pathlike(self.output):
47
+ output_path = pathlib.Path(typing.cast(str, self.output))
48
+ output_path.parent.mkdir(parents=True, exist_ok=True)
49
+ output_fobj = output_path.open('w')
50
+ else:
51
+ output_fobj = typing.cast(IO[bytes], self.output)
52
+ if isinstance(self.stderr, int):
53
+ stderr_fobj = self.stderr
54
+ elif _is_pathlike(self.stderr):
55
+ stderr_path = pathlib.Path(typing.cast(str, self.stderr))
56
+ stderr_path.parent.mkdir(parents=True, exist_ok=True)
57
+ stderr_fobj = stderr_path.open('w')
58
+ else:
59
+ stderr_fobj = typing.cast(IO[bytes], self.stderr)
60
+ return input_fobj, output_fobj, stderr_fobj
61
+
62
+
63
+ @dataclasses.dataclass
64
+ class ProgramPipes:
65
+ input: Optional[IO[bytes]] = None
66
+ output: Optional[IO[bytes]] = None
67
+ stderr: Optional[IO[bytes]] = None
68
+
69
+
70
+ @dataclasses.dataclass
71
+ class ProgramParams:
72
+ io: ProgramIO = dataclasses.field(default_factory=ProgramIO)
73
+ chdir: Optional[pathlib.Path] = None
74
+ time_limit: Optional[float] = None # seconds
75
+ wall_time_limit: Optional[float] = None # seconds
76
+ memory_limit: Optional[int] = None # megabytes
77
+ fs_limit: Optional[int] = None # kilobytes
78
+ env: Dict[str, str] = dataclasses.field(default_factory=dict)
79
+ pgid: Optional[int] = None
80
+
81
+
82
+ def get_preexec_fn(params: ProgramParams):
83
+ def preexec_fn():
84
+ os.setpgid(0, params.pgid or 0)
85
+ if params.time_limit is not None:
86
+ time_limit_in_ms = int(params.time_limit * 1000)
87
+ rlimit_cpu = int((time_limit_in_ms + 999) // 1000)
88
+ resource.setrlimit(resource.RLIMIT_CPU, (rlimit_cpu, rlimit_cpu + 1))
89
+ if params.fs_limit is not None:
90
+ fs_limit = params.fs_limit * 1024 # in bytes
91
+ resource.setrlimit(resource.RLIMIT_FSIZE, (fs_limit + 1, fs_limit * 2))
92
+
93
+ return preexec_fn
94
+
95
+
96
+ def get_memory_usage(ru: resource.struct_rusage) -> int:
97
+ if sys.platform == 'darwin':
98
+ return ru.ru_maxrss // 1024 + ru.ru_ixrss
99
+ return ru.ru_maxrss + ru.ru_ixrss + ru.ru_idrss + ru.ru_isrss
100
+
101
+
102
+ def get_cpu_time(ru: resource.struct_rusage) -> float:
103
+ return ru.ru_utime + ru.ru_stime
104
+
105
+
106
+ def get_file_sizes(io: ProgramIO):
107
+ return _get_file_size(io.output) + _get_file_size(io.stderr)
108
+
109
+
110
+ def _get_file_size(filename: Optional[FileLike]) -> int:
111
+ if filename is None or not _is_pathlike(filename):
112
+ return 0
113
+ path = pathlib.Path(typing.cast(str, filename))
114
+ if not path.is_file():
115
+ return 0
116
+ return path.stat().st_size
117
+
118
+
119
+ class ProgramCode(Enum):
120
+ RE = 'RE'
121
+ SG = 'SG'
122
+ TO = 'TO'
123
+ WT = 'WT'
124
+ ML = 'ML'
125
+ OL = 'OL'
126
+ TE = 'TE'
127
+
128
+
129
+ @dataclasses.dataclass
130
+ class ProgramResult:
131
+ exitcode: int
132
+ wall_time: float
133
+ cpu_time: float
134
+ memory_used: int
135
+ file_sizes: int
136
+ program_codes: List[ProgramCode]
137
+ killing_signal: Optional[int] = None
138
+ alarm_msg: Optional[str] = None
139
+
140
+
141
+ class Program:
142
+ def __init__(self, command: List[str], params: ProgramParams):
143
+ self.command = command
144
+ self.params = params
145
+ self.popen: Optional[subprocess.Popen] = None
146
+ self._files = []
147
+
148
+ self._stop_wall_handler = threading.Event()
149
+ self._stop_alarm_handler = threading.Event()
150
+ self._alarm_msg = ''
151
+
152
+ self._run()
153
+
154
+ @property
155
+ def pipes(self) -> ProgramPipes:
156
+ assert self.popen is not None
157
+ return ProgramPipes(
158
+ input=self.popen.stdin,
159
+ output=self.popen.stdout,
160
+ stderr=self.popen.stderr,
161
+ )
162
+
163
+ @property
164
+ def pid(self) -> int:
165
+ assert self.popen is not None
166
+ return self.popen.pid
167
+
168
+ def _kill_process(self):
169
+ if self.popen is not None:
170
+ self.popen.kill()
171
+
172
+ def _handle_wall(self):
173
+ if self._stop_wall_handler.wait(self.params.wall_time_limit):
174
+ return
175
+ self._stop_alarm_handler.set()
176
+ self._alarm_msg = 'wall timelimit'
177
+ self._kill_process()
178
+
179
+ def _handle_alarm(self):
180
+ if self._stop_alarm_handler.wait(0.3):
181
+ return
182
+ try:
183
+ process = psutil.Process(self.pid)
184
+ if self.params.time_limit is not None:
185
+ times = process.cpu_times()
186
+ cpu_time = times.user + times.system
187
+ if cpu_time > self.params.time_limit:
188
+ self._alarm_msg = 'timelimit'
189
+ self._kill_process()
190
+ if self.params.memory_limit is not None:
191
+ memory_info = process.memory_info()
192
+ memory_used = memory_info.rss
193
+ if memory_used > self.params.memory_limit * 1024 * 1024:
194
+ self._alarm_msg = 'memorylimit'
195
+ self._kill_process()
196
+ self._stop_alarm_handler.clear()
197
+ self._handle_alarm()
198
+ except psutil.NoSuchProcess:
199
+ return
200
+
201
+ def _run(self):
202
+ self._files = self.params.io.get_file_objects()
203
+ self.popen = subprocess.Popen(
204
+ self.command,
205
+ stdin=self._files[0],
206
+ stdout=self._files[1],
207
+ stderr=self._files[2],
208
+ cwd=self.params.chdir,
209
+ env={**os.environ, **self.params.env},
210
+ preexec_fn=get_preexec_fn(self.params),
211
+ close_fds=True,
212
+ )
213
+ self.start_time = monotonic()
214
+
215
+ threading.Thread(target=self._handle_wall, daemon=True).start()
216
+ threading.Thread(target=self._handle_alarm, daemon=True).start()
217
+
218
+ def process_exit(self, exitstatus, ru) -> ProgramResult:
219
+ wall_time = monotonic() - self.start_time
220
+ cpu_time = get_cpu_time(ru)
221
+ memory_used = get_memory_usage(ru)
222
+ file_sizes = get_file_sizes(self.params.io)
223
+ exitcode = os.waitstatus_to_exitcode(exitstatus)
224
+ killing_signal = None
225
+ program_codes = []
226
+
227
+ if exitcode < 0:
228
+ killing_signal = -exitcode
229
+ program_codes.append(ProgramCode.SG)
230
+ if exitcode > 0:
231
+ program_codes.append(ProgramCode.RE)
232
+ if self.params.time_limit is not None and (
233
+ cpu_time > self.params.time_limit or -exitcode == 24
234
+ ):
235
+ program_codes.append(ProgramCode.TO)
236
+ if (
237
+ self.params.wall_time_limit is not None
238
+ and wall_time > self.params.wall_time_limit
239
+ ):
240
+ program_codes.append(ProgramCode.WT)
241
+ program_codes.append(ProgramCode.TO)
242
+ if (
243
+ self.params.memory_limit is not None
244
+ and memory_used > self.params.memory_limit * 1024 * 1024
245
+ or self._alarm_msg == 'memorylimit'
246
+ ):
247
+ program_codes.append(ProgramCode.ML)
248
+ if (
249
+ self.params.fs_limit is not None
250
+ and file_sizes > self.params.fs_limit * 1024
251
+ ):
252
+ program_codes.append(ProgramCode.OL)
253
+
254
+ return ProgramResult(
255
+ exitcode=exitcode,
256
+ wall_time=wall_time,
257
+ cpu_time=cpu_time,
258
+ memory_used=memory_used,
259
+ file_sizes=file_sizes,
260
+ program_codes=program_codes,
261
+ killing_signal=killing_signal,
262
+ alarm_msg=self._alarm_msg or None,
263
+ )
264
+
265
+ def wait(self):
266
+ assert self.popen is not None
267
+ _, exitstatus, ru = os.wait4(self.pid, 0)
268
+ return self.process_exit(exitstatus, ru)
@@ -3,16 +3,16 @@ import asyncio
3
3
  import collections
4
4
  import dataclasses
5
5
  import io
6
+ import json
6
7
  import logging
7
8
  import os
8
9
  import pathlib
9
10
  import select
10
- import signal
11
11
  import stat
12
12
  import subprocess
13
13
  import sys
14
14
  import typing
15
- from typing import IO, Any, Dict, List, Optional
15
+ from typing import IO, Any, Dict, List, Optional, Tuple
16
16
 
17
17
  import pydantic
18
18
 
@@ -210,6 +210,22 @@ class SandboxParams(pydantic.BaseModel):
210
210
  self.dirs.append(DirectoryMount(src, dest, options))
211
211
 
212
212
 
213
+ class SandboxLog(pydantic.BaseModel):
214
+ """A log of the sandbox."""
215
+
216
+ params: SandboxParams
217
+ execution_time: float # seconds
218
+ memory_used: int # bytes
219
+ exitcode: int
220
+ exitstatus: str
221
+ killing_signal: Optional[int] = None
222
+ exit_index: int = 0
223
+ other_logs: Dict[str, Any] = pydantic.Field(default_factory=dict)
224
+
225
+ def dump_other_logs(self) -> str:
226
+ return json.dumps(self.other_logs)
227
+
228
+
213
229
  class SandboxBase(abc.ABC):
214
230
  """A base class for all sandboxes, meant to contain common
215
231
  resources.
@@ -229,16 +245,12 @@ class SandboxBase(abc.ABC):
229
245
  file_cacher: cacher.FileCacher
230
246
  name: str
231
247
  temp_dir: Optional[pathlib.Path]
232
- cmd_file: pathlib.Path
233
-
234
- params: SandboxParams
235
248
 
236
249
  def __init__(
237
250
  self,
238
251
  file_cacher: Optional[cacher.FileCacher] = None,
239
252
  name: Optional[str] = None,
240
253
  temp_dir: Optional[pathlib.Path] = None,
241
- params: Optional[SandboxParams] = None,
242
254
  ):
243
255
  """Initialization.
244
256
 
@@ -253,56 +265,7 @@ class SandboxBase(abc.ABC):
253
265
  self.file_cacher = file_cacher or cacher.FileCacher(storage.NullStorage())
254
266
  self.name = name if name is not None else 'unnamed'
255
267
  self.temp_dir = temp_dir
256
-
257
- self.cmd_file = pathlib.PosixPath('commands.log')
258
-
259
- self.params = params or SandboxParams()
260
- self.pid = None
261
- self.pid_event = Event_ts()
262
-
263
- # Set common environment variables.
264
- # Specifically needed by Python, that searches the home for
265
- # packages.
266
- self.params.set_env['HOME'] = './'
267
-
268
- def set_params(self, params: SandboxParams):
269
- """Set the parameters of the sandbox.
270
-
271
- params (SandboxParams): the parameters to set.
272
-
273
- """
274
- self.params = params
275
-
276
- def set_multiprocess(self, multiprocess: bool):
277
- """Set the sandbox to (dis-)allow multiple threads and processes.
278
-
279
- multiprocess (bool): whether to allow multiple thread/processes or not.
280
-
281
- """
282
- if multiprocess:
283
- # Max processes is set to 1000 to limit the effect of fork bombs.
284
- self.params.max_processes = 1000
285
- else:
286
- self.params.max_processes = 1
287
-
288
- def get_stats(self) -> str:
289
- """Return a human-readable string representing execution time
290
- and memory usage.
291
-
292
- return (string): human-readable stats.
293
-
294
- """
295
- execution_time = self.get_execution_time()
296
- if execution_time is not None:
297
- time_str = f'{execution_time:.3f} sec'
298
- else:
299
- time_str = '(time unknown)'
300
- memory_used = self.get_memory_used()
301
- if memory_used is not None:
302
- mem_str = f'{memory_used / (1024 * 1024):.2f} MB'
303
- else:
304
- mem_str = '(memory usage unknown)'
305
- return f'[{time_str} - {mem_str}]'
268
+ self.cmd_file = pathlib.PosixPath('__commands__.log')
306
269
 
307
270
  @abc.abstractmethod
308
271
  def get_root_path(self) -> pathlib.Path:
@@ -313,101 +276,6 @@ class SandboxBase(abc.ABC):
313
276
  """
314
277
  pass
315
278
 
316
- @abc.abstractmethod
317
- def get_execution_time(self) -> Optional[float]:
318
- """Return the time spent in the sandbox.
319
-
320
- return (float): time spent in the sandbox.
321
-
322
- """
323
- pass
324
-
325
- @abc.abstractmethod
326
- def get_memory_used(self) -> Optional[int]:
327
- """Return the memory used by the sandbox.
328
-
329
- return (int): memory used by the sandbox (in bytes).
330
-
331
- """
332
- pass
333
-
334
- @abc.abstractmethod
335
- def get_killing_signal(self) -> int:
336
- """Return the signal that killed the sandboxed process.
337
-
338
- return (int): offending signal, or 0.
339
-
340
- """
341
- pass
342
-
343
- @abc.abstractmethod
344
- def get_exit_status(self) -> str:
345
- """Get information about how the sandbox terminated.
346
-
347
- return (string): the main reason why the sandbox terminated.
348
-
349
- """
350
- pass
351
-
352
- @abc.abstractmethod
353
- def get_exit_code(self) -> int:
354
- """Return the exit code of the sandboxed process.
355
-
356
- return (int): exitcode, or 0.
357
-
358
- """
359
- pass
360
-
361
- def set_pid(self, pid: int):
362
- """Set the PID of the sandboxed process.
363
-
364
- pid (int): the PID of the sandboxed process.
365
-
366
- """
367
- self.pid = pid
368
- self.pid_event.set()
369
-
370
- async def get_pid(self) -> int:
371
- """Return the PID of the sandboxed process.
372
-
373
- Blocks until the PID is set.
374
-
375
- return (int): the PID of the sandboxed process.
376
-
377
- """
378
- await self.pid_event.wait()
379
- assert self.pid is not None
380
- return self.pid
381
-
382
- def clear_pid(self):
383
- """Clear the PID of the sandboxed process."""
384
- self.pid_event.clear()
385
- self.pid = None
386
-
387
- def use_pgid(self) -> bool:
388
- """Whether the sandbox supports process groups."""
389
- return False
390
-
391
- @abc.abstractmethod
392
- def get_detailed_logs(self) -> str:
393
- """Return the detailed logs of the sandbox.
394
-
395
- return (string): the detailed logs of the sandbox.
396
-
397
- """
398
- pass
399
-
400
- @abc.abstractmethod
401
- def get_human_exit_description(self) -> str:
402
- """Get the status of the sandbox and return a human-readable
403
- string describing it.
404
-
405
- return (string): human-readable explaination of why the
406
- sandbox terminated.
407
-
408
- """
409
- pass
410
-
411
279
  def use_soft_timeout(self) -> bool:
412
280
  return False
413
281
 
@@ -702,52 +570,24 @@ class SandboxBase(abc.ABC):
702
570
  ]
703
571
 
704
572
  @abc.abstractmethod
705
- def execute_without_std(
573
+ def run(
706
574
  self,
707
575
  command: List[str],
708
- ) -> bool:
709
- """Execute the given command in the sandbox using
710
- subprocess.Popen and discarding standard input, output and
711
- error. More specifically, the standard input gets closed just
712
- after the execution has started; standard output and error are
713
- read until the end, in a way that prevents the execution from
714
- being blocked because of insufficient buffering.
715
-
716
- command ([string]): executable filename and arguments of the
717
- command.
718
- wait (bool): True if this call is blocking, False otherwise
719
-
720
- return (bool|Popen): if the call is blocking, then return True
721
- if the sandbox didn't report errors (caused by the sandbox
722
- itself), False otherwise; if the call is not blocking,
723
- return the Popen object from subprocess.
724
-
725
- """
576
+ params: SandboxParams,
577
+ ) -> SandboxLog:
726
578
  pass
727
579
 
728
580
  @abc.abstractmethod
729
- def hydrate_logs(self):
730
- """Fetch the results of the execution and hydrate logs.
731
-
732
- This method should be called after the execution has
733
- terminated, to hydrate logs and stuff.
734
- """
581
+ def run_communication(
582
+ self,
583
+ command: List[str],
584
+ params: SandboxParams,
585
+ interactor_command: List[str],
586
+ interactor_params: SandboxParams,
587
+ merged_capture: Optional[pathlib.Path] = None,
588
+ ) -> Tuple[SandboxLog, SandboxLog]:
735
589
  pass
736
590
 
737
- def translate_box_exitcode(self, exitcode: int) -> bool:
738
- """Translate the sandbox exit code to a boolean sandbox success.
739
-
740
- _ (int): the exit code of the sandbox.
741
-
742
- return (bool): False if the sandbox had an error, True if it
743
- terminated correctly (regardless of what the internal process
744
- did).
745
-
746
- """
747
- # SIGTERM can be safely ignored, just in case it leaks away from
748
- # the sandbox.
749
- return exitcode == 0 or exitcode == -signal.SIGTERM
750
-
751
591
  @abc.abstractmethod
752
592
  def initialize(self):
753
593
  """Initialize the sandbox.
@@ -779,9 +619,6 @@ class SandboxBase(abc.ABC):
779
619
  """
780
620
  pass
781
621
 
782
- def debug_message(self) -> Any:
783
- return 'N/A'
784
-
785
622
 
786
623
  class Truncator(io.RawIOBase):
787
624
  """Wrap a file-like object to simulate truncation.