summaryrefslogtreecommitdiff
path: root/python/runner/main.py
blob: 2b1ba1547eeb8c7abd96c938e339878610e01595 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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()