-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlookup-min-http-server.py
executable file
·185 lines (156 loc) · 5.91 KB
/
lookup-min-http-server.py
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python3
# Simple HTTP server that serves results from lookup-min.
import argparse
import json
import http.server
import os
import re
import socket
import subprocess
import sys
import urllib.parse
parser = argparse.ArgumentParser(description='Serve results from lookup-min over HTTP.')
parser.add_argument('--host', default='localhost', help='Hostname to bind to')
parser.add_argument('--port', default=8003, type=int, help='Port number to bind to')
parser.add_argument('--ipv4', default=False, type=bool, action=argparse.BooleanOptionalAction,
help='Bind to IPv4 adress instead of IPv6 address')
parser.add_argument('--lookup', default='./lookup-min', help='Path to lookup-min binary')
parser.add_argument('--minimized', default='minimized.bin', help='Path to minimized.bin or minimized.bin.xz')
args = parser.parse_args()
BIND_ADDR = (args.host, args.port)
LOOKUP_MIN_PATH = args.lookup
MINIMIZED_BIN_PATH = args.minimized
PERM_PATH = re.compile('^/perms/([.oOxXY]{26})$')
# Converts the output from lookup-min into a JSON object.
#
# Input are lines of the form:
#
# L1 d3-b2,d2-a3,d4-e4 +8660632211
# L2 e2-b2,g4-d3,c1-c2 -72702403683 838 15 13 W2*12,W4*1,T*15,L11*1,L10*8,L8*3,L7*1,L6*2,L5*4,L3*3,L2*176,L1*640
#
# Note that statuses are guaranteed to be grouped and ordered from best to
# worst outcome.
#
# Output is a JSON object of the form:
#
# {
# "status": "W1",
# "successors": [
# {
# status: "W1",
# moves: ["a1-a2,b1-b2,c1-c2", "d1-d2", etc.]
# },
# {
# status: "T",
# moves: ["a3-a4,b3-b4,c3-c4", "d3-d4", etc.]
# details: ["T*15,L11*1,L10*8,..", etc.]
# }
# etc.
# ]
# }
#
# Note that moves are grouped by status, but successors is a list, not an
# object, to guarantee that ordering is preserved.
#
# The "details" property is only provided for statuses other than W1 and L1,
# and only if detailed analysis is requested with query parameter ?d=1 (see
# HttpRequestHandler below). If it exists, the length of "details" equals the
# length of "moves", and element correspond one to one.
#
def ConvertSuccessors(output):
lines = output.splitlines()
if not lines:
return {'status': 'L0', 'successors': []}
successors = []
last_successor = None
last_status = None
for line in lines:
status, moves, *rest = line.split(' ')
if status != last_status:
last_status = status
last_successor = {'status': status, 'moves': []}
successors.append(last_successor)
last_successor['moves'].append(moves)
if len(rest) >= 5:
min_index, losses, wins, ties, details, *rest = rest
if 'details' not in last_successor:
last_successor['details'] = []
last_successor['details'].append(details)
assert(len(last_successor['moves']) == len(last_successor['details']))
return {'status': successors[0]['status'], 'successors': successors}
class HttpServer(http.server.ThreadingHTTPServer):
allow_reuse_address = True
address_family = socket.AF_INET if args.ipv4 else socket.AF_INET6
# Handles a request to analyze a position.
#
# Request URL must be of the form: '/perms/.OX.....oxX....Oox.....OX' or
# '/perms/.OX.....oxX....Oox.....OX?d=1' where the second component is a
# 26-letter permutation string that denotes a started or in-progress position,
# and the ?d=1 query parameter indicates whether to retrieve detailed
# information (corresponding to the -d parameter of lookup-min).
#
# On error, the response status is 400 (client error) or 500 (server error),
# and the body contains an error mesage.
#
# On success, the response status is 200, and the body is a JSON object
# as generated by Transform() above.
#
class HttpRequestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
request_url = urllib.parse.urlparse(self.path, scheme='http')
m = PERM_PATH.match(request_url.path)
if not m:
self.send_error(404, 'Resource not found.')
return
perm = m.group(1)
# Note: this keeps only the last value for each parameter.
params = dict(urllib.parse.parse_qsl(request_url.query))
# Execute lookup-min, which does the actual work of calculating
# successors and their statuses.
options = []
d = params.get('d', '0')
if d == '1':
options.append('-d')
elif d != '0':
self.send_client_error('Invalid value for d parameter')
return
result = subprocess.run([LOOKUP_MIN_PATH] + options + [MINIMIZED_BIN_PATH, perm],
capture_output=True, text=True)
if result.returncode == 2:
# User error
self.send_client_error(result.stderr)
return
if result.returncode != 0:
# Internal or unknown error
print('lookup-min invocation failed: perm={} returncode={} stderr={}'.format(perm, result.returncode, result.stderr), file=sys.stderr)
self.send_server_error('Internal error. Check server logs for details.')
return
# Success
assert result.returncode == 0
assert not result.stderr
self.send_json_response(ConvertSuccessors(result.stdout))
return
def send_cors_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Max-Age', 3600) # 1 hour
def send_json_response(self, obj):
self.send_response(200)
self.send_cors_headers()
self.send_header('Content-Type', 'application/json')
self.send_header('Cache-Control', 'public, max-age=86400') # 1 day
self.end_headers()
self.wfile.write(bytes(json.dumps(obj), 'utf-8'))
def send_error(self, status, error):
self.send_response(status)
self.send_cors_headers()
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(bytes(error, 'utf-8'))
def send_client_error(self, error):
self.send_error(400, error)
def send_server_error(self, error):
self.send_error(500, error)
if __name__ == "__main__":
with HttpServer(BIND_ADDR, HttpRequestHandler) as httpd:
print('Serving on host "%s" port %d' % BIND_ADDR)
httpd.serve_forever()