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.
@@ -5,21 +5,45 @@ import logging
5
5
  import os
6
6
  import pathlib
7
7
  import shutil
8
- import signal
9
8
  import subprocess
10
9
  import sys
11
10
  import tempfile
12
- from typing import Any, Dict, List, Optional
11
+ import typing
12
+ from typing import List, Optional, Tuple
13
13
 
14
14
  from rbx import utils
15
15
  from rbx.grading.judge.cacher import FileCacher
16
+ from rbx.grading.judge.program import (
17
+ FileLike,
18
+ Program,
19
+ ProgramCode,
20
+ ProgramIO,
21
+ ProgramParams,
22
+ ProgramResult,
23
+ )
16
24
  from rbx.grading.judge.sandbox import (
17
25
  SandboxBase,
26
+ SandboxLog,
18
27
  SandboxParams,
19
28
  )
20
29
 
21
30
  logger = logging.getLogger(__name__)
22
31
 
32
+ TEE_CODE = R"""
33
+ import sys
34
+ c = sys.argv[1]
35
+ new = True
36
+ while True:
37
+ l = sys.stdin.read(1)
38
+ if l=='': break
39
+ sys.stdout.write(l)
40
+ sys.stdout.flush()
41
+ if new: sys.stderr.write(c)
42
+ sys.stderr.write(l)
43
+ sys.stderr.flush()
44
+ new = l=='\n'
45
+ """
46
+
23
47
 
24
48
  class StupidSandbox(SandboxBase):
25
49
  """A stupid sandbox implementation. It has very few features and
@@ -31,16 +55,12 @@ class StupidSandbox(SandboxBase):
31
55
  """
32
56
 
33
57
  exec_num: int
34
- popen: Optional[subprocess.Popen]
35
- returncode: Optional[int]
36
- log: Optional[Dict[str, str]]
37
58
 
38
59
  def __init__(
39
60
  self,
40
61
  file_cacher: Optional[FileCacher] = None,
41
62
  name: Optional[str] = None,
42
63
  temp_dir: Optional[pathlib.Path] = None,
43
- params: Optional[SandboxParams] = None,
44
64
  ):
45
65
  """Initialization.
46
66
 
@@ -49,7 +69,7 @@ class StupidSandbox(SandboxBase):
49
69
  """
50
70
  if not temp_dir:
51
71
  temp_dir = pathlib.Path(tempfile.gettempdir())
52
- SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
72
+ SandboxBase.__init__(self, file_cacher, name, temp_dir)
53
73
 
54
74
  # Make box directory
55
75
  self.initialize()
@@ -68,56 +88,6 @@ class StupidSandbox(SandboxBase):
68
88
  # Box parameters
69
89
  self.chdir = self._path
70
90
 
71
- def get_timeit_executable(self) -> pathlib.Path:
72
- with importlib.resources.as_file(
73
- importlib.resources.files('rbx')
74
- / 'grading'
75
- / 'judge'
76
- / 'sandboxes'
77
- / 'timeit.py'
78
- ) as file:
79
- return file
80
-
81
- def get_timeit_args(self) -> List[str]:
82
- args = []
83
- if self.params.timeout:
84
- timeout_in_s = self.params.timeout / 1000
85
- if self.params.extra_timeout:
86
- timeout_in_s += self.params.extra_timeout / 1000
87
- args.append(f'-t{timeout_in_s:.3f}')
88
- if self.params.wallclock_timeout:
89
- walltimeout_in_s = self.params.wallclock_timeout / 1000
90
- args.append(f'-w{walltimeout_in_s:.3f}')
91
- if self.params.address_space:
92
- args.append(f'-m{self.params.address_space}')
93
- if self.params.fsize:
94
- args.append(f'-f{self.params.fsize}')
95
- if self.chdir:
96
- args.append(f'-c{self.chdir}')
97
- if self.use_pgid() and self.params.pgid is not None:
98
- args.append(f'-g{self.params.pgid}')
99
-
100
- file_args = []
101
- if self.params.stdin_file:
102
- file_args.append(f'-i{self.params.stdin_file}')
103
- if self.params.stdout_file:
104
- file_args.append(f'-o{self.params.stdout_file}')
105
- if self.params.stderr_file:
106
- file_args.append(f'-e{self.params.stderr_file}')
107
- if self.params.reverse_io:
108
- file_args.reverse()
109
- args.extend(file_args)
110
-
111
- if self.params.timeit_dups:
112
- for i, files in self.params.timeit_dups.items():
113
- assert i.lower() in ['di', 'do', 'de']
114
- for file in files:
115
- args.append(f'-{i}{file}')
116
- if self.params.timeit_prefix:
117
- args.append(f'-P{self.params.timeit_prefix}')
118
-
119
- return args
120
-
121
91
  def get_root_path(self) -> pathlib.Path:
122
92
  """Return the toplevel path of the sandbox.
123
93
 
@@ -126,171 +96,153 @@ class StupidSandbox(SandboxBase):
126
96
  """
127
97
  return self._path
128
98
 
129
- def get_execution_time(self) -> Optional[float]:
130
- """Return the time spent in the sandbox.
131
-
132
- return (float): time spent in the sandbox.
133
-
134
- """
135
- if self.log is None:
136
- return None
137
- return float(self.log['time'])
138
-
139
- def get_execution_wall_clock_time(self) -> Optional[float]:
140
- """Return the total time from the start of the sandbox to the
141
- conclusion of the task.
142
-
143
- return (float): total time the sandbox was alive.
144
-
145
- """
146
- if self.log is None:
147
- return None
148
- return float(self.log['time-wall'])
149
-
150
99
  def use_soft_timeout(self) -> bool:
151
100
  return True
152
101
 
153
- def use_pgid(self) -> bool:
154
- return True
155
-
156
- def get_memory_used(self) -> Optional[int]:
157
- """Return the memory used by the sandbox.
158
-
159
- return (int): memory used by the sandbox (in bytes).
160
-
161
- """
162
- if self.log is None:
163
- return None
164
- return int(self.log['mem']) * 1024
165
-
166
- def get_killing_signal(self) -> int:
167
- """Return the signal that killed the sandboxed process.
168
-
169
- return (int): offending signal, or 0.
102
+ def _get_exit_status(self, result: ProgramResult) -> str:
103
+ if ProgramCode.TE in result.program_codes:
104
+ return SandboxBase.EXIT_TERMINATED
105
+ if ProgramCode.WT in result.program_codes:
106
+ return SandboxBase.EXIT_TIMEOUT_WALL
107
+ if ProgramCode.TO in result.program_codes:
108
+ return SandboxBase.EXIT_TIMEOUT
109
+ if ProgramCode.OL in result.program_codes:
110
+ return SandboxBase.EXIT_OUTPUT_LIMIT_EXCEEDED
111
+ if ProgramCode.ML in result.program_codes:
112
+ return SandboxBase.EXIT_MEMORY_LIMIT_EXCEEDED
113
+ if ProgramCode.SG in result.program_codes:
114
+ return SandboxBase.EXIT_SIGNAL
115
+ if ProgramCode.RE in result.program_codes:
116
+ return SandboxBase.EXIT_NONZERO_RETURN
117
+ return SandboxBase.EXIT_OK
118
+
119
+ def _get_io(self, params: SandboxParams, pipe_io: bool = False) -> ProgramIO:
120
+ io = ProgramIO()
121
+ if params.stdin_file and not pipe_io:
122
+ io.input = self.relative_path(params.stdin_file)
123
+ if params.stdout_file and not pipe_io:
124
+ io.output = self.relative_path(params.stdout_file)
125
+ if params.stderr_file:
126
+ io.stderr = self.relative_path(params.stderr_file)
127
+ return io
128
+
129
+ def _get_program_params(self, params: SandboxParams) -> ProgramParams:
130
+ return ProgramParams(
131
+ chdir=self.chdir,
132
+ time_limit=params.timeout / 1000 if params.timeout else None,
133
+ wall_time_limit=params.wallclock_timeout / 1000
134
+ if params.wallclock_timeout
135
+ else None,
136
+ memory_limit=params.address_space,
137
+ fs_limit=params.fsize,
138
+ env=params.set_env,
139
+ io=self._get_io(params),
140
+ )
170
141
 
171
- """
172
- assert self.log is not None
173
- if 'exit-sig' not in self.log:
174
- return 0
175
- return int(self.log['exit-sig'])
142
+ def _get_tee_program_params(self, io: ProgramIO, pgid: int) -> ProgramParams:
143
+ return ProgramParams(
144
+ chdir=self.chdir,
145
+ time_limit=None,
146
+ wall_time_limit=None,
147
+ memory_limit=None,
148
+ io=io,
149
+ pgid=pgid,
150
+ )
176
151
 
177
- def get_status_list(self) -> List[str]:
178
- """Reads the sandbox log file, and set and return the status
179
- of the sandbox.
152
+ def _get_sandbox_log(
153
+ self, result: ProgramResult, params: SandboxParams
154
+ ) -> SandboxLog:
155
+ return SandboxLog(
156
+ params=params.model_copy(deep=True),
157
+ execution_time=result.wall_time,
158
+ memory_used=result.memory_used,
159
+ exitcode=result.exitcode,
160
+ exitstatus=self._get_exit_status(result),
161
+ killing_signal=result.killing_signal,
162
+ other_logs={
163
+ 'program_codes': [code.value for code in result.program_codes],
164
+ 'alarm_msg': result.alarm_msg,
165
+ },
166
+ )
180
167
 
181
- return (list): list of statuses of the sandbox.
168
+ def _needs_teeing(
169
+ self,
170
+ params: SandboxParams,
171
+ interactor_params: SandboxParams,
172
+ merged_capture: Optional[pathlib.Path] = None,
173
+ ) -> bool:
174
+ return (
175
+ params.stdout_file is not None
176
+ or interactor_params.stdout_file is not None
177
+ or merged_capture is not None
178
+ )
182
179
 
183
- """
184
- assert self.log is not None
185
- if 'status' in self.log:
186
- return self.log['status'].split(',')
187
- return []
180
+ def _get_tee_executable(self) -> pathlib.Path:
181
+ with importlib.resources.as_file(
182
+ importlib.resources.files('rbx')
183
+ / 'grading'
184
+ / 'judge'
185
+ / 'sandboxes'
186
+ / 'tee.py'
187
+ ) as file:
188
+ return file
188
189
 
189
- # This sandbox only discriminates between processes terminating
190
- # properly or being killed with a signal; all other exceptional
191
- # conditions (RAM or CPU limitations, ...) result in some signal
192
- # being delivered to the process
193
- def get_exit_status(self) -> str:
194
- """Get information about how the sandbox terminated.
190
+ def _get_tee_command(self, char: str, extra: Optional[str] = None) -> List[str]:
191
+ return [
192
+ sys.executable,
193
+ str(utils.abspath(self._get_tee_executable())),
194
+ char,
195
+ extra or '/dev/null',
196
+ ]
195
197
 
196
- return (string): the main reason why the sandbox terminated.
198
+ def _get_tee_program(
199
+ self,
200
+ char: str,
201
+ stdin: FileLike,
202
+ stdout: FileLike,
203
+ pgid: int,
204
+ capture: Optional[pathlib.Path] = None,
205
+ merged_capture: Optional[pathlib.Path] = None,
206
+ ) -> Program:
207
+ io = ProgramIO(input=stdin, output=stdout, stderr=subprocess.DEVNULL)
208
+ if merged_capture:
209
+ io.stderr = self.relative_path(merged_capture).open('ab')
210
+ return Program(
211
+ self._get_tee_command(
212
+ char, str(self.relative_path(capture)) if capture else None
213
+ ),
214
+ self._get_tee_program_params(io, pgid),
215
+ )
197
216
 
198
- """
199
- if self.returncode is not None and not self.translate_box_exitcode(
200
- self.returncode
201
- ):
202
- return self.EXIT_SANDBOX_ERROR
203
- status_list = self.get_status_list()
204
- if 'TE' in status_list:
205
- return self.EXIT_TERMINATED
206
- if 'WT' in status_list:
207
- return self.EXIT_TIMEOUT_WALL
208
- if 'TO' in status_list:
209
- return self.EXIT_TIMEOUT
210
- if 'OL' in status_list:
211
- return self.EXIT_OUTPUT_LIMIT_EXCEEDED
212
- if 'ML' in status_list:
213
- return self.EXIT_MEMORY_LIMIT_EXCEEDED
214
- if 'SG' in status_list:
215
- return self.EXIT_SIGNAL
216
- if 'RE' in status_list:
217
- return self.EXIT_NONZERO_RETURN
218
- return self.EXIT_OK
219
-
220
- def get_exit_code(self) -> int:
221
- """Return the exit code of the sandboxed process.
222
-
223
- return (float): exitcode, or 0.
217
+ def _get_pathlike_stdout(self, io: ProgramIO) -> Optional[pathlib.Path]:
218
+ if isinstance(io.output, str) or isinstance(io.output, pathlib.Path):
219
+ return pathlib.Path(io.output)
220
+ return None
224
221
 
225
- """
226
- assert self.log is not None
227
- return int(self.log['exit-code'])
222
+ def run(self, command: List[str], params: SandboxParams) -> SandboxLog:
223
+ self.exec_num += 1
228
224
 
229
- def get_detailed_logs(self) -> str:
230
- return str(self.log)
225
+ logger.debug(
226
+ "Executing program in sandbox with command: `%s'.", ' '.join(command)
227
+ )
228
+ with open(
229
+ self.relative_path(self.cmd_file), 'at', encoding='utf-8'
230
+ ) as commands:
231
+ commands.write('%s\n' % command)
231
232
 
232
- def get_human_exit_description(self) -> str:
233
- """Get the status of the sandbox and return a human-readable
234
- string describing it.
233
+ program = Program(command, self._get_program_params(params))
234
+ result = program.wait()
235
235
 
236
- return (string): human-readable explaination of why the
237
- sandbox terminated.
236
+ return self._get_sandbox_log(result, params)
238
237
 
239
- """
240
- status = self.get_exit_status()
241
- if status == self.EXIT_OK:
242
- return (
243
- 'Execution successfully finished (with exit code %d)'
244
- % self.get_exit_code()
245
- )
246
- elif status == self.EXIT_SANDBOX_ERROR:
247
- return 'Execution failed because of sandbox error'
248
- elif status == self.EXIT_TIMEOUT:
249
- return 'Execution timed out'
250
- elif status == self.EXIT_TIMEOUT_WALL:
251
- return 'Execution timed out (wall clock limit exceeded)'
252
- elif status == self.EXIT_SIGNAL:
253
- return 'Execution killed with signal %s' % self.get_killing_signal()
254
- elif status == self.EXIT_NONZERO_RETURN:
255
- return 'Execution failed because the return code was nonzero'
256
- elif status == self.EXIT_OUTPUT_LIMIT_EXCEEDED:
257
- return 'Execution exceeded output limit'
258
- return ''
259
-
260
- def get_current_log_name(self) -> pathlib.Path:
261
- return pathlib.Path(f'logs.{self.exec_num}')
262
-
263
- def hydrate_logs(self):
264
- self.log = None
265
- if not self.relative_path(self.get_current_log_name()).is_file():
266
- return
267
- self.log = {}
268
- raw_log = self.get_file_to_string(self.get_current_log_name(), maxlen=None)
269
- for line in raw_log.splitlines():
270
- items = line.split(':', 1)
271
- if len(items) != 2:
272
- continue
273
- key, value = items
274
- self.log[key] = value.strip()
275
-
276
- def execute_without_std(
238
+ def run_communication(
277
239
  self,
278
240
  command: List[str],
279
- ) -> bool:
280
- """Execute the given command in the sandbox using
281
- subprocess.Popen and discarding standard input, output and
282
- error. More specifically, the standard input gets closed just
283
- after the execution has started; standard output and error are
284
- read until the end, in a way that prevents the execution from
285
- being blocked because of insufficient buffering.
286
-
287
- command ([string]): executable filename and arguments of the
288
- command.
289
-
290
- return (bool): True if the sandbox didn't report errors
291
- (caused by the sandbox itself), False otherwise
292
-
293
- """
241
+ params: SandboxParams,
242
+ interactor_command: List[str],
243
+ interactor_params: SandboxParams,
244
+ merged_capture: Optional[pathlib.Path] = None,
245
+ ) -> Tuple[SandboxLog, SandboxLog]:
294
246
  self.exec_num += 1
295
247
 
296
248
  logger.debug(
@@ -301,43 +253,82 @@ class StupidSandbox(SandboxBase):
301
253
  ) as commands:
302
254
  commands.write('%s\n' % command)
303
255
 
304
- real_command = (
305
- [
306
- sys.executable,
307
- str(utils.abspath(self.get_timeit_executable())),
308
- str(utils.abspath(self.relative_path(self.get_current_log_name()))),
309
- ]
310
- + self.get_timeit_args()
311
- + command
256
+ interactor_program_params = self._get_program_params(interactor_params)
257
+ interactor_program_params.io = self._get_io(interactor_params, pipe_io=True)
258
+ interactor = Program(
259
+ interactor_command,
260
+ interactor_program_params,
312
261
  )
313
-
314
- self.clear_pid()
315
- with subprocess.Popen(
316
- real_command,
317
- stdin=subprocess.PIPE,
318
- stdout=subprocess.PIPE,
319
- stderr=subprocess.STDOUT,
320
- env={**os.environ, **self.params.set_env},
321
- ) as p:
322
- self.set_pid(p.pid)
323
- try:
324
- self.returncode = p.wait()
325
- except Exception:
326
- p.kill()
327
- raise
328
- self.hydrate_logs()
329
- return self.translate_box_exitcode(self.returncode)
330
-
331
- def translate_box_exitcode(self, exitcode: int) -> bool:
332
- # SIGALRM can be safely ignored, just in case it leaks away. SIGTERM also.
333
- if self.log is None:
334
- return False
335
- if 'TE' in self.get_status_list():
336
- return True
337
- return super().translate_box_exitcode(exitcode) or -exitcode == signal.SIGALRM
338
-
339
- def debug_message(self) -> Any:
340
- return f'returncode = {self.returncode}\nlogs = {self.log}\ntimeit_args = {self.get_timeit_args()}'
262
+ assert interactor.pipes.output is not None
263
+ assert interactor.pipes.input is not None
264
+ solution_input_pipe = interactor.pipes.output
265
+ solution_output_pipe = interactor.pipes.input
266
+
267
+ group_id = os.getpgid(interactor.pid)
268
+ should_tee = self._needs_teeing(params, interactor_params, merged_capture)
269
+
270
+ if should_tee:
271
+ if merged_capture:
272
+ self.create_file_from_string(merged_capture, '<\n>\n', override=True)
273
+
274
+ solution_tee = self._get_tee_program(
275
+ '>',
276
+ stdin=subprocess.PIPE,
277
+ stdout=interactor.pipes.input,
278
+ capture=self._get_pathlike_stdout(self._get_io(params)),
279
+ merged_capture=merged_capture,
280
+ pgid=group_id,
281
+ )
282
+ interactor_tee = self._get_tee_program(
283
+ '<',
284
+ stdin=interactor.pipes.output,
285
+ stdout=subprocess.PIPE,
286
+ capture=self._get_pathlike_stdout(self._get_io(interactor_params)),
287
+ merged_capture=merged_capture,
288
+ pgid=group_id,
289
+ )
290
+ assert solution_tee.pipes.input is not None
291
+ assert interactor_tee.pipes.output is not None
292
+ solution_input_pipe = interactor_tee.pipes.output
293
+ solution_output_pipe = solution_tee.pipes.input
294
+
295
+ program_params = self._get_program_params(params)
296
+ program_params.io = self._get_io(params, pipe_io=True)
297
+ program_params.io.input = solution_input_pipe
298
+ program_params.io.output = solution_output_pipe
299
+ program_params.pgid = group_id
300
+ program = Program(command, program_params)
301
+
302
+ results: List[Optional[SandboxLog]] = [None, None]
303
+
304
+ for idx in range(4 if should_tee else 2):
305
+ pid, status, ru = os.wait4(-group_id, 0)
306
+
307
+ if pid == interactor.pid:
308
+ program_result = interactor.process_exit(status, ru)
309
+ results[1] = self._get_sandbox_log(program_result, interactor_params)
310
+ results[1].exit_index = idx
311
+
312
+ interactor.pipes.output.close()
313
+ if should_tee:
314
+ assert interactor_tee.pipes.output is not None
315
+ interactor_tee.pipes.output.close()
316
+ # TODO: kill in case of WA
317
+ elif pid == program.pid:
318
+ program_result = program.process_exit(status, ru)
319
+ results[0] = self._get_sandbox_log(program_result, params)
320
+ results[0].exit_index = idx
321
+
322
+ interactor.pipes.input.close()
323
+ if should_tee:
324
+ assert solution_tee.pipes.input is not None
325
+ solution_tee.pipes.input.close()
326
+ elif should_tee and (pid in (solution_tee.pid, interactor_tee.pid)):
327
+ pass
328
+ else:
329
+ raise RuntimeError(f'Unknown pid: {pid}')
330
+
331
+ return typing.cast(Tuple[SandboxLog, SandboxLog], tuple(results))
341
332
 
342
333
  def cleanup(self, delete=False):
343
334
  """See Sandbox.cleanup()."""
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Forked from https://github.com/RagnarGrootKoerkamp/BAPCtools/blob/master/bin/interactive.py
4
+
5
+ Takes a character and a file name as arguments.
6
+
7
+ Reads from stdin, and writes to stdout and stderr, with the given character prepended to
8
+ every line read from stdin.
9
+ """
10
+
11
+ import sys
12
+
13
+ c = sys.argv[1]
14
+ extra = sys.argv[2]
15
+
16
+ new = True
17
+
18
+ with open(extra, 'w') as f:
19
+ while True:
20
+ rd = sys.stdin.read(1)
21
+ if rd == '':
22
+ break
23
+ sys.stdout.write(rd)
24
+ sys.stdout.flush()
25
+ if new:
26
+ sys.stderr.write(c)
27
+ sys.stderr.write(rd)
28
+ sys.stderr.flush()
29
+
30
+ f.write(rd)
31
+ new = rd == '\n'