From 86737c42e6c7704a44487f2ec936140db2c67d26 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Fri, 21 Aug 2015 15:06:56 +0200 Subject: Add a Python server for running users' code --- python/engine.py | 57 +++++++++++++++++++++++++++++++ python/runner/main.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100755 python/engine.py create mode 100755 python/runner/main.py 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() -- cgit v1.2.1