diff options
Diffstat (limited to 'python/runner')
-rwxr-xr-x | python/runner/main.py | 95 |
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() |