summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-14 17:57:09 +0200
committerTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-14 17:57:09 +0200
commit520420556ad9f2871cd8c5910645193508cb4082 (patch)
tree08efd34b6c9d3cc071b6c5345b213ec9f7f59dd5 /server
parent7ae2d8824ab59dfbda6eaf7f621b6d3bfdec56e7 (diff)
Use sandbox for testing Python programs
Use interpreter.py for running tests as well as interactive sessions. Signals are now sent with "sandbox <user> kill", so terminator is not needed anymore.
Diffstat (limited to 'server')
-rw-r--r--server/python_session.py107
1 files changed, 41 insertions, 66 deletions
diff --git a/server/python_session.py b/server/python_session.py
index e1be4ba..39bd8f4 100644
--- a/server/python_session.py
+++ b/server/python_session.py
@@ -4,6 +4,7 @@ from fcntl import fcntl, F_GETFL, F_SETFL
import io
import multiprocessing
import os
+import pickle
import selectors
import signal
import subprocess
@@ -36,22 +37,34 @@ class PythonSession(server.LanguageSession):
# Launch processes.
futures = []
for expr, stdin in inputs:
- conn_parent, conn_child = multiprocessing.Pipe()
- p = multiprocessing.Process(target=_run_exec, args=(conn_child, code, expr, stdin))
- p.start()
- futures.append((p, conn_parent))
+ p = subprocess.Popen(proc_args + ['run'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL)
+ data = pickle.dumps((code, expr, stdin))
+ p.stdin.write(data)
+ futures.append(p)
# Wait for results.
results = []
start = time.monotonic()
- for p, conn in futures:
+ for proc in futures:
now = time.monotonic()
real_timeout = max(0, timeout - (now - start))
- if conn.poll(real_timeout):
- results.append(conn.recv())
- else:
- results.append((None, None, None, 'timed out'))
- p.terminate()
+ try:
+ stdout, _ = proc.communicate(timeout=real_timeout)
+ result = pickle.loads(stdout)
+ results.append(result)
+ except subprocess.TimeoutExpired:
+ results.append((None, '', '', 'timed out'))
+ except EOFError:
+ results.append((None, '', '', 'sandbox violation'))
+ if proc.poll() is None:
+ try:
+ proc_kill(proc, signal.SIGKILL)
+ except:
+ pass
+
return results
def exec(self, program):
@@ -114,23 +127,21 @@ class PythonSession(server.LanguageSession):
hint_type.instantiate(hint, self._sent_hints)
self._sent_hints.extend(hints)
-def _interpreter(control, callback):
- basedir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
- script = os.path.join(basedir, 'python', 'runner', 'interpreter.py')
-
- # If the sandbox wrapper exists, use it to switch to user "nobody" and
- # enforce additional limits. Unless the daemon is running as root we are
- # not able to signal nobody's PIDs, so switch user again for the killing.
- sandbox = os.path.join(basedir, 'python', 'runner', 'sandbox')
- terminator = os.path.join(basedir, 'python', 'runner', 'terminator')
- if os.path.exists(sandbox) and os.path.exists(terminator):
- newuser = 'nobody' # TODO make this configurable
- args = [sandbox, newuser, script]
- kill = lambda proc, sig: subprocess.call([terminator, newuser, str(proc.pid), str(sig)])
- else:
- args = [script]
- kill = lambda proc, sig: proc.send_signal(sig)
+# If the sandbox wrapper exists, use it to switch to user "nobody" and enforce
+# additional limits. Unless the daemon is running as root we are not able to
+# signal nobody's PIDs, so switch user again for the killing.
+_basedir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
+_script = os.path.join(_basedir, 'python', 'runner', 'interpreter.py')
+_sandbox = os.path.join(_basedir, 'python', 'runner', 'sandbox')
+if os.path.exists(_sandbox):
+ newuser = 'nobody' # TODO make this configurable
+ proc_args = [_sandbox, newuser, _script]
+ proc_kill = lambda proc, sig: subprocess.call([_sandbox, newuser, 'kill', '-{}'.format(sig), str(proc.pid)])
+else:
+ proc_args = [_script]
+ proc_kill = lambda proc, sig: proc.send_signal(sig)
+def _interpreter(control, callback):
done = False
proc = None
@@ -156,7 +167,7 @@ def _interpreter(control, callback):
proc.stdin.write(data.encode('utf-8'))
proc.stdin.flush()
elif cmd == 'stop':
- kill(proc, signal.SIGINT)
+ proc_kill(proc, signal.SIGINT)
elif cmd == 'done':
done = True
except:
@@ -181,7 +192,7 @@ def _interpreter(control, callback):
selector.unregister(conn)
if proc.poll() is None:
# Process has not terminated yet, make sure it does.
- kill(proc, signal.SIGKILL)
+ proc_kill(proc, signal.SIGKILL)
proc = None
length = newlines = 0
callback('Interpreter restarted.\n')
@@ -190,7 +201,7 @@ def _interpreter(control, callback):
while not done:
# Ensure the interpreter process is running.
if proc is None:
- proc = subprocess.Popen(args,
+ proc = subprocess.Popen(proc_args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
@@ -210,42 +221,6 @@ def _interpreter(control, callback):
# We are done, kill the child.
selector.close()
if proc is not None:
- kill(proc, signal.SIGKILL)
-
-# Execute [code] and evaluate [expr]. Input is given by the string [stdin].
-# Return result of evaluation, the contents of stdout and stderr, and the
-# exception traceback.
-# TODO sandbox this
-def _run_exec(conn, code, expr=None, stdin=''):
- result, out, err, exc = None, None, None, None
- sys.stdin = io.StringIO(stdin)
- sys.stdout = io.StringIO()
- sys.stderr = io.StringIO()
- try:
- env = {}
- if code:
- exec(code, env)
- if expr:
- result = eval(expr, env)
- except Exception as ex:
- # Exception is not JSON serializable, so return traceback as string
- # (without the first entry, which is this function).
- import traceback
- e_type, e_value, e_tb = sys.exc_info()
- stack = traceback.extract_tb(e_tb)
- exc = ''.join(
- ['Traceback (most recent call last):\n'] +
- [' line {}, in {}\n'.format(lineno, name) + (line+'\n' if line else '')
- for filename, lineno, name, line in stack[1:]] +
- traceback.format_exception_only(e_type, e_value)
- ).rstrip()
- finally:
- out = sys.stdout.getvalue()
- err = sys.stderr.getvalue()
- sys.stdin.close()
- sys.stdout.close()
- sys.stderr.close()
- conn.send((result, out, err, exc))
+ proc_kill(proc, signal.SIGKILL)
server.language_session_handlers['python'] = lambda user_session, problem_id, language_identifier, group_identifier, problem_identifier: PythonSession(lambda text: user_session.send({'event': 'terminal_output', 'text': text}))
-