summaryrefslogtreecommitdiff
path: root/python/runner
diff options
context:
space:
mode:
Diffstat (limited to 'python/runner')
-rwxr-xr-xpython/runner/main.py95
1 files changed, 95 insertions, 0 deletions
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()