#!/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()