Skip to content

Commit 8b852dc

Browse files
Interactive problems via I/O (#26)
* Temporary changes in sioworkers for interactive problems * Some fixes * Fixes in interactive_common * gaming * very gaming * Strip interactor_out and improve sigpipe handling * Get rid of some Hacks * Respect mem and time limit for interactor * Interactive tasks can't be output-only * Supervisor trolling Co-authored-by: Mateusz Masiarz <[email protected]> * Supervisor is not trolling Co-authored-by: Mateusz Masiarz <[email protected]> * Remove debug * Adapt to new results percentage * Refactor a bit --------- Co-authored-by: Mateusz Masiarz <[email protected]> Co-authored-by: Mateusz Masiarz <[email protected]>
1 parent 9137c6a commit 8b852dc

File tree

7 files changed

+248
-22
lines changed

7 files changed

+248
-22
lines changed

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@
3737
'ping = sio.workers.ping:run',
3838
'compile = sio.compilers.job:run',
3939
'exec = sio.executors.executor:run',
40+
'interactive-exec = sio.executors.executor:interactive_run',
4041
'sio2jail-exec = sio.executors.sio2jail_exec:run',
42+
'sio2jail-interactive-exec = sio.executors.sio2jail_exec:interactive_run',
4143
'cpu-exec = sio.executors.executor:run',
44+
'cpu-interactive-exec = sio.executors.executor:interactive_run',
4245
'unsafe-exec = sio.executors.unsafe_exec:run',
46+
'unsafe-interactive-exec = sio.executors.unsafe_exec:interactive_run',
4347
'ingen = sio.executors.ingen:run',
4448
'inwer = sio.executors.inwer:run',
4549
],

sio/executors/common.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ def _populate_environ(renv, environ):
2020
environ['out_file'] = renv['out_file']
2121

2222

23+
def _extract_input_if_zipfile(input_name, zipdir):
24+
if is_zipfile(input_name):
25+
try:
26+
# If not a zip file, will pass it directly to exe
27+
with ZipFile(tempcwd('in'), 'r') as f:
28+
if len(f.namelist()) != 1:
29+
raise Exception("Archive should have only one file.")
30+
31+
f.extract(f.namelist()[0], zipdir)
32+
input_name = os.path.join(zipdir, f.namelist()[0])
33+
# zipfile throws some undocumented exceptions
34+
except Exception as e:
35+
raise Exception("Failed to open archive: " + six.text_type(e))
36+
37+
return input_name
38+
39+
2340
@decode_fields(['result_string'])
2441
def run(environ, executor, use_sandboxes=True):
2542
"""
@@ -70,18 +87,7 @@ def _run(environ, executor, use_sandboxes):
7087
zipdir = tempcwd('in_dir')
7188
os.mkdir(zipdir)
7289
try:
73-
if is_zipfile(input_name):
74-
try:
75-
# If not a zip file, will pass it directly to exe
76-
with ZipFile(tempcwd('in'), 'r') as f:
77-
if len(f.namelist()) != 1:
78-
raise Exception("Archive should have only one file.")
79-
80-
f.extract(f.namelist()[0], zipdir)
81-
input_name = os.path.join(zipdir, f.namelist()[0])
82-
# zipfile throws some undocumented exceptions
83-
except Exception as e:
84-
raise Exception("Failed to open archive: " + six.text_type(e))
90+
input_name = _extract_input_if_zipfile(input_name, zipdir)
8591

8692
with file_executor as fe:
8793
with open(input_name, 'rb') as inf:

sio/executors/executor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import absolute_import
2-
from sio.executors import common
2+
from sio.executors import common, interactive_common
33
from sio.workers.executors import SupervisedExecutor
44

55

66
def run(environ):
77
return common.run(environ, SupervisedExecutor())
8+
9+
def interactive_run(environ):
10+
return interactive_common.run(environ, SupervisedExecutor())
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import os
2+
from shutil import rmtree
3+
from threading import Thread
4+
5+
from sio.executors.checker import output_to_fraction
6+
from sio.executors.common import _extract_input_if_zipfile, _populate_environ
7+
from sio.workers import ft
8+
from sio.workers.executors import DetailedUnprotectedExecutor
9+
from sio.workers.util import TemporaryCwd, decode_fields, replace_invalid_UTF, tempcwd
10+
from sio.workers.file_runners import get_file_runner
11+
12+
import signal
13+
import six
14+
15+
DEFAULT_INTERACTOR_MEM_LIMIT = 256 * 2 ** 10 # in KiB
16+
RESULT_STRING_LENGTH_LIMIT = 1024 # in bytes
17+
18+
19+
class InteractorError(Exception):
20+
def __init__(self, message, interactor_out, env, renv, irenv):
21+
super().__init__(
22+
f'{message}\n'
23+
f'Interactor out: {interactor_out}\n'
24+
f'Interactor environ dump: {irenv}\n'
25+
f'Solution environ dump: {renv}\n'
26+
f'Environ dump: {env}'
27+
)
28+
29+
30+
def _limit_length(s):
31+
if len(s) > RESULT_STRING_LENGTH_LIMIT:
32+
suffix = b'[...]'
33+
return s[: max(0, RESULT_STRING_LENGTH_LIMIT - len(suffix))] + suffix
34+
return s
35+
36+
37+
@decode_fields(['result_string'])
38+
def run(environ, executor, use_sandboxes=True):
39+
"""
40+
Common code for executors.
41+
42+
:param: environ Recipe to pass to `filetracker` and `sio.workers.executors`
43+
For all supported options, see the global documentation for
44+
`sio.workers.executors` and prefix them with ``exec_``.
45+
:param: executor Executor instance used for executing commands.
46+
:param: use_sandboxes Enables safe checking output correctness.
47+
See `sio.executors.checkers`. True by default.
48+
"""
49+
50+
renv = _run(environ, executor, use_sandboxes)
51+
52+
_populate_environ(renv, environ)
53+
54+
for key in ('result_code', 'result_string'):
55+
environ[key] = replace_invalid_UTF(environ[key])
56+
57+
if 'out_file' in environ:
58+
ft.upload(
59+
environ,
60+
'out_file',
61+
tempcwd('out'),
62+
to_remote_store=environ.get('upload_out', False),
63+
)
64+
65+
return environ
66+
67+
68+
def _fill_result(env, renv, irenv, interactor_out):
69+
sol_sig = renv.get('exit_signal', None)
70+
inter_sig = irenv.get('exit_signal', None)
71+
sigpipe = signal.SIGPIPE.value
72+
73+
if irenv['result_code'] != 'OK' and inter_sig != sigpipe:
74+
renv['result_code'] = 'SE'
75+
raise InteractorError(f'Interactor got {irenv["result_code"]}.', interactor_out, env, renv, irenv)
76+
elif renv['result_code'] != 'OK' and sol_sig != sigpipe:
77+
return
78+
elif len(interactor_out) == 0:
79+
renv['result_code'] = 'SE'
80+
raise InteractorError(f'Empty interactor out.', interactor_out, env, renv, irenv)
81+
elif inter_sig == sigpipe:
82+
renv['result_code'] = 'WA'
83+
renv['result_string'] = 'solution exited prematurely'
84+
else:
85+
renv['result_string'] = ''
86+
if six.ensure_binary(interactor_out[0]) == b'OK':
87+
renv['result_code'] = 'OK'
88+
if interactor_out[1]:
89+
renv['result_string'] = _limit_length(interactor_out[1])
90+
renv['result_percentage'] = output_to_fraction(interactor_out[2])
91+
else:
92+
renv['result_code'] = 'WA'
93+
if interactor_out[1]:
94+
renv['result_string'] = _limit_length(interactor_out[1])
95+
renv['result_percentage'] = (0, 1)
96+
97+
98+
def _run(environ, executor, use_sandboxes):
99+
input_name = tempcwd('in')
100+
101+
file_executor = get_file_runner(executor, environ)
102+
interactor_executor = DetailedUnprotectedExecutor()
103+
exe_filename = file_executor.preferred_filename()
104+
interactor_filename = 'soc'
105+
106+
ft.download(environ, 'exe_file', exe_filename, add_to_cache=True)
107+
os.chmod(tempcwd(exe_filename), 0o700)
108+
ft.download(environ, 'interactor_file', interactor_filename, add_to_cache=True)
109+
os.chmod(tempcwd(interactor_filename), 0o700)
110+
ft.download(environ, 'in_file', input_name, add_to_cache=True)
111+
112+
zipdir = tempcwd('in_dir')
113+
os.mkdir(zipdir)
114+
try:
115+
input_name = _extract_input_if_zipfile(input_name, zipdir)
116+
117+
r1, w1 = os.pipe()
118+
r2, w2 = os.pipe()
119+
for fd in (r1, w1, r2, w2):
120+
os.set_inheritable(fd, True)
121+
122+
interactor_args = [os.path.basename(input_name), 'out']
123+
124+
interactor_time_limit = 2 * environ['exec_time_limit']
125+
126+
class ExecutionWrapper(Thread):
127+
def __init__(self, executor, *args, **kwargs):
128+
super(ExecutionWrapper, self).__init__()
129+
self.executor = executor
130+
self.args = args
131+
self.kwargs = kwargs
132+
self.value = None
133+
self.exception = None
134+
135+
def run(self):
136+
with TemporaryCwd():
137+
try:
138+
self.value = self.executor(*self.args, **self.kwargs)
139+
except Exception as e:
140+
self.exception = e
141+
142+
with interactor_executor as ie:
143+
interactor = ExecutionWrapper(
144+
ie,
145+
[tempcwd(interactor_filename)] + interactor_args,
146+
stdin=r2,
147+
stdout=w1,
148+
ignore_errors=True,
149+
environ=environ,
150+
environ_prefix='interactor_',
151+
mem_limit=DEFAULT_INTERACTOR_MEM_LIMIT,
152+
time_limit=interactor_time_limit,
153+
pass_fds=(r2, w1),
154+
close_passed_fd=True,
155+
cwd=tempcwd(),
156+
in_file=environ['in_file'],
157+
)
158+
159+
with file_executor as fe:
160+
exe = ExecutionWrapper(
161+
fe,
162+
tempcwd(exe_filename),
163+
[],
164+
stdin=r1,
165+
stdout=w2,
166+
ignore_errors=True,
167+
environ=environ,
168+
environ_prefix='exec_',
169+
pass_fds=(r1, w2),
170+
close_passed_fd=True,
171+
cwd=tempcwd(),
172+
in_file=environ['in_file'],
173+
)
174+
175+
exe.start()
176+
interactor.start()
177+
178+
exe.join()
179+
interactor.join()
180+
181+
for ew in (exe, interactor):
182+
if ew.exception is not None:
183+
raise ew.exception
184+
185+
renv = exe.value
186+
irenv = interactor.value
187+
188+
try:
189+
with open(tempcwd('out'), 'rb') as result_file:
190+
interactor_out = [line.rstrip() for line in result_file.readlines()]
191+
while len(interactor_out) < 3:
192+
interactor_out.append(b'')
193+
except FileNotFoundError:
194+
interactor_out = []
195+
196+
_fill_result(environ, renv, irenv, interactor_out)
197+
finally:
198+
rmtree(zipdir)
199+
200+
return renv

sio/executors/sio2jail_exec.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from sio.executors import common
1+
from sio.executors import common, interactive_common
22
from sio.workers.executors import Sio2JailExecutor
33

44

55
def run(environ):
66
return common.run(environ, Sio2JailExecutor())
7+
8+
def interactive_run(environ):
9+
return interactive_common.run(environ, Sio2JailExecutor())

sio/executors/unsafe_exec.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import absolute_import
2-
from sio.executors import common
2+
from sio.executors import common, interactive_common
33
from sio.workers.executors import DetailedUnprotectedExecutor
44

55

66
def run(environ):
77
return common.run(environ, DetailedUnprotectedExecutor(), use_sandboxes=False)
8+
9+
def interactive_run(environ):
10+
return interactive_common.run(environ, DetailedUnprotectedExecutor(), use_sandboxes=False)

sio/workers/executors.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def execute_command(
7979
real_time_limit=None,
8080
ignore_errors=False,
8181
extra_ignore_errors=(),
82-
**kwargs
82+
cwd=None,
83+
fds_to_close=(),
84+
**kwargs,
8385
):
8486
"""Utility function to run arbitrary command.
8587
``stdin``
@@ -123,8 +125,9 @@ def execute_command(
123125
devnull = open(os.devnull, 'wb')
124126
stdout = stdout or devnull
125127
stderr = stderr or devnull
126-
128+
cwd = cwd or tempcwd()
127129
ret_env = {}
130+
128131
if env is not None:
129132
for key, value in six.iteritems(env):
130133
env[key] = str(value)
@@ -139,10 +142,13 @@ def execute_command(
139142
close_fds=True,
140143
universal_newlines=True,
141144
env=env,
142-
cwd=tempcwd(),
145+
cwd=cwd,
143146
preexec_fn=os.setpgrp,
144147
)
145148

149+
for fd in fds_to_close:
150+
os.close(fd)
151+
146152
kill_timer = None
147153
if real_time_limit:
148154

@@ -180,7 +186,6 @@ def oot_killer():
180186
raise ExecError(
181187
'Failed to execute command: %s. Returned with code %s\n' % (command, rc)
182188
)
183-
184189
return ret_env
185190

186191

@@ -417,9 +422,8 @@ def _execute(self, command, **kwargs):
417422
renv['result_string'] = 'ok'
418423
renv['result_code'] = 'OK'
419424
elif renv['return_code'] > 128: # os.WIFSIGNALED(1) returns True
420-
renv['result_string'] = 'program exited due to signal %d' % os.WTERMSIG(
421-
renv['return_code']
422-
)
425+
renv['exit_signal'] = os.WTERMSIG(renv['return_code'])
426+
renv['result_string'] = 'program exited due to signal %d' % renv['exit_signal']
423427
renv['result_code'] = 'RE'
424428
else:
425429
renv['result_string'] = 'program exited with code %d' % renv['return_code']
@@ -669,6 +673,9 @@ def _execute(self, command, **kwargs):
669673
renv['result_code'] = 'RV'
670674
elif renv['result_string'].startswith('process exited due to signal'):
671675
renv['result_code'] = 'RE'
676+
renv['exit_signal'] = int(
677+
renv['result_string'][len('process exited due to signal '):]
678+
)
672679
else:
673680
raise ExecError(
674681
'Unrecognized Sio2Jail result string: %s' % renv['result_string']

0 commit comments

Comments
 (0)