summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@araneo.org>2015-08-21 15:06:56 +0200
committerTimotej Lazar <timotej.lazar@araneo.org>2015-08-21 15:06:56 +0200
commit86737c42e6c7704a44487f2ec936140db2c67d26 (patch)
tree855d6e0a4692becdbcb9b646ba62e33363fa2f7d
parent0d4edaca1dc14e16e5204b3af275570e869a9a0a (diff)
Add a Python server for running users' code
-rwxr-xr-xpython/engine.py57
-rwxr-xr-xpython/runner/main.py95
2 files changed, 152 insertions, 0 deletions
diff --git a/python/engine.py b/python/engine.py
new file mode 100755
index 0000000..f1b8d36
--- /dev/null
+++ b/python/engine.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python3
+
+# This module submits Python code to the python-tester.
+
+import http.client
+import json
+
+address, port = 'localhost', 3031 # TODO put this somewhere sane
+def request(method, path, body=None):
+ headers = {'Content-Type': 'application/json;charset=utf-8'}
+ messages = []
+ try:
+ conn = http.client.HTTPConnection(address, port, timeout=30)
+ conn.request(method, path, body, headers=headers)
+ response = conn.getresponse()
+ if response.status != http.client.OK:
+ raise Exception('server returned {}'.format(response.status))
+
+ reply = json.loads(response.read().decode('utf-8'))
+ return reply
+ finally:
+ conn.close()
+
+# Inputs are given as a list of pairs (expression, stdin). Receives and returns
+# a list of answers given as tuples (result, stdout, stderr, exception).
+def run(code=None, inputs=None, timeout=1.0):
+ body = {'code': code, 'inputs': inputs, 'timeout': timeout}
+ return request('GET', '/run', body=json.dumps(body))
+
+
+# Basic sanity check.
+if __name__ == '__main__':
+ import time
+
+ code = '''\
+import sys
+def foo(x):
+ y = int(input())
+ print(x+y)
+ sys.stderr.write('bar')
+ if x+y == 6:
+ while True:
+ pass
+ return x*y
+'''
+ inputs = [
+ ('foo(1)', '2\n'),
+ ('foo(2)', '4\n'),
+ ('foo(3)', '6\n'),
+ ]
+
+ start = time.monotonic()
+ result = run(code=code, inputs=inputs, timeout=1.0)
+ end = time.monotonic()
+ for r in result['results']:
+ print(r)
+ print('time taken: {:.3f}'.format(end-start))
diff --git a/python/runner/main.py b/python/runner/main.py
new file mode 100755
index 0000000..2b1ba15
--- /dev/null
+++ b/python/runner/main.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python3
+
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from socketserver import ForkingMixIn
+
+import io
+import json
+import multiprocessing
+import sys
+import time
+
+# 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.
+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.
+ import traceback
+ exc = traceback.format_exc()
+ finally:
+ out = sys.stdout.getvalue()
+ err = sys.stderr.getvalue()
+ sys.stdin.close()
+ sys.stdout.close()
+ sys.stderr.close()
+ conn.send((result, out, err, exc))
+
+# Call run_exec in a separate process for each input and return a list of
+# results from those calls. If a call times out, 'timed out' is returned in
+# place of exception traceback.
+def run(code, inputs, timeout):
+ # 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))
+
+ # Wait for results.
+ results = []
+ start = time.monotonic()
+ for p, conn 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, '', '', 'timed out'))
+ p.terminate()
+
+ return results
+
+class RequestHandler(BaseHTTPRequestHandler):
+ def do_OPTIONS(self):
+ self.send_response(200)
+ self.end_headers()
+
+ def do_GET(self):
+ reply = {'status': 'error'}
+ if self.path == '/run':
+ length = int(self.headers.get('Content-Length', 0))
+ text = self.rfile.read(length)
+ request = json.loads(text.decode('utf-8'))
+ results = run(request.get('code', ''),
+ request.get('inputs', []),
+ request.get('timeout', 1.0))
+ reply = {'status': 'ok', 'results': results}
+
+ reply_text = json.dumps(reply).encode('utf-8')
+ self.send_response(200)
+ self.send_header('Content-Length', len(reply_text))
+ self.end_headers()
+ self.wfile.write(reply_text)
+
+class ForkingHTTPServer(ForkingMixIn, HTTPServer):
+ pass
+
+server = ForkingHTTPServer(('localhost', 3031), RequestHandler)
+print('Python engine started.')
+try:
+ server.serve_forever()
+except KeyboardInterrupt:
+ print('Keyboard interrupt received, exiting.')
+ server.server_close()