OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 ''' | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 Google Inc. |
5 | 5 |
6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
7 found in the LICENSE file. | 7 found in the LICENSE file. |
8 ''' | |
9 | 8 |
10 ''' | |
11 HTTP server for our HTML rebaseline viewer. | 9 HTTP server for our HTML rebaseline viewer. |
12 ''' | 10 """ |
13 | 11 |
14 # System-level imports | 12 # System-level imports |
15 import argparse | 13 import argparse |
16 import BaseHTTPServer | 14 import BaseHTTPServer |
17 import json | 15 import json |
18 import os | 16 import os |
19 import posixpath | 17 import posixpath |
20 import re | 18 import re |
21 import shutil | 19 import shutil |
22 import sys | 20 import sys |
(...skipping 30 matching lines...) Expand all Loading... |
53 'json': 'application/json' | 51 'json': 'application/json' |
54 } | 52 } |
55 | 53 |
56 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 54 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
57 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 55 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
58 DEFAULT_PORT = 8888 | 56 DEFAULT_PORT = 8888 |
59 | 57 |
60 _SERVER = None # This gets filled in by main() | 58 _SERVER = None # This gets filled in by main() |
61 | 59 |
62 class Server(object): | 60 class Server(object): |
63 """ HTTP server for our HTML rebaseline viewer. | 61 """ HTTP server for our HTML rebaseline viewer. """ |
64 | 62 |
65 params: | |
66 actuals_dir: directory under which we will check out the latest actual | |
67 GM results | |
68 expectations_dir: directory under which to find GM expectations (they | |
69 must already be in that directory) | |
70 port: which TCP port to listen on for HTTP requests | |
71 export: whether to allow HTTP clients on other hosts to access this server | |
72 """ | |
73 def __init__(self, | 63 def __init__(self, |
74 actuals_dir=DEFAULT_ACTUALS_DIR, | 64 actuals_dir=DEFAULT_ACTUALS_DIR, |
75 expectations_dir=DEFAULT_EXPECTATIONS_DIR, | 65 expectations_dir=DEFAULT_EXPECTATIONS_DIR, |
76 port=DEFAULT_PORT, export=False): | 66 port=DEFAULT_PORT, export=False): |
| 67 """ |
| 68 Args: |
| 69 actuals_dir: directory under which we will check out the latest actual |
| 70 GM results |
| 71 expectations_dir: directory under which to find GM expectations (they |
| 72 must already be in that directory) |
| 73 port: which TCP port to listen on for HTTP requests |
| 74 export: whether to allow HTTP clients on other hosts to access this server |
| 75 """ |
77 self._actuals_dir = actuals_dir | 76 self._actuals_dir = actuals_dir |
78 self._expectations_dir = expectations_dir | 77 self._expectations_dir = expectations_dir |
79 self._port = port | 78 self._port = port |
80 self._export = export | 79 self._export = export |
81 | 80 |
| 81 def is_exported(self): |
| 82 """ Returns true iff HTTP clients on other hosts are allowed to access |
| 83 this server. """ |
| 84 return self._export |
| 85 |
82 def fetch_results(self): | 86 def fetch_results(self): |
83 """ Create self.results, based on the expectations in | 87 """ Create self.results, based on the expectations in |
84 self._expectations_dir and the latest actuals from skia-autogen. | 88 self._expectations_dir and the latest actuals from skia-autogen. |
85 | 89 |
86 TODO(epoger): Add a new --browseonly mode setting. In that mode, | 90 TODO(epoger): Add a new --browseonly mode setting. In that mode, |
87 the gm-actuals and expectations will automatically be updated every few | 91 the gm-actuals and expectations will automatically be updated every few |
88 minutes. See discussion in https://codereview.chromium.org/24274003/ . | 92 minutes. See discussion in https://codereview.chromium.org/24274003/ . |
89 """ | 93 """ |
90 print 'Checking out latest actual GM results from %s into %s ...' % ( | 94 print 'Checking out latest actual GM results from %s into %s ...' % ( |
91 ACTUALS_SVN_REPO, self._actuals_dir) | 95 ACTUALS_SVN_REPO, self._actuals_dir) |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
124 do_GET_* dispatcher. """ | 128 do_GET_* dispatcher. """ |
125 if self.path == '' or self.path == '/' or self.path == '/index.html' : | 129 if self.path == '' or self.path == '/' or self.path == '/index.html' : |
126 self.redirect_to('/static/view.html') | 130 self.redirect_to('/static/view.html') |
127 return | 131 return |
128 if self.path == '/favicon.ico' : | 132 if self.path == '/favicon.ico' : |
129 self.redirect_to('/static/favicon.ico') | 133 self.redirect_to('/static/favicon.ico') |
130 return | 134 return |
131 | 135 |
132 # All requests must be of this form: | 136 # All requests must be of this form: |
133 # /dispatcher/remainder | 137 # /dispatcher/remainder |
134 # where "dispatcher" indicates which do_GET_* dispatcher to run | 138 # where 'dispatcher' indicates which do_GET_* dispatcher to run |
135 # and "remainder" is the remaining path sent to that dispatcher. | 139 # and 'remainder' is the remaining path sent to that dispatcher. |
136 normpath = posixpath.normpath(self.path) | 140 normpath = posixpath.normpath(self.path) |
137 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() | 141 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() |
138 dispatchers = { | 142 dispatchers = { |
139 'results': self.do_GET_results, | 143 'results': self.do_GET_results, |
140 'static': self.do_GET_static, | 144 'static': self.do_GET_static, |
141 } | 145 } |
142 dispatcher = dispatchers[dispatcher_name] | 146 dispatcher = dispatchers[dispatcher_name] |
143 dispatcher(remainder) | 147 dispatcher(remainder) |
144 | 148 |
145 def do_GET_results(self, result_type): | 149 def do_GET_results(self, result_type): |
146 """ Handle a GET request for GM results. | 150 """ Handle a GET request for GM results. |
147 For now, we ignore the remaining path info, because we only know how to | 151 For now, we ignore the remaining path info, because we only know how to |
148 return all results. | 152 return all results. |
149 | 153 |
| 154 Args: |
| 155 result_type: currently unused |
| 156 |
150 TODO(epoger): Unless we start making use of result_type, remove that | 157 TODO(epoger): Unless we start making use of result_type, remove that |
151 parameter.""" | 158 parameter.""" |
152 print 'do_GET_results: sending results of type "%s"' % result_type | 159 print 'do_GET_results: sending results of type "%s"' % result_type |
| 160 # TODO(epoger): Cache response_dict rather than the results object, to save |
| 161 # time on subsequent fetches (no need to regenerate the header, etc.) |
153 response_dict = _SERVER.results.GetAll() | 162 response_dict = _SERVER.results.GetAll() |
154 if response_dict: | 163 if response_dict: |
| 164 response_dict['header'] = { |
| 165 # Hash of testData, which the client must return with any edits-- |
| 166 # this ensures that the edits were made to a particular dataset. |
| 167 'data-hash': str(hash(repr(response_dict['testData']))), |
| 168 |
| 169 # Whether the server will accept edits back. |
| 170 # TODO(epoger): Not yet implemented, so hardcoding to False; |
| 171 # once we implement the 'browseonly' mode discussed in |
| 172 # https://codereview.chromium.org/24274003/#msg6 , this value will vary. |
| 173 'isEditable': False, |
| 174 |
| 175 # Whether the service is accessible from other hosts. |
| 176 'isExported': _SERVER.is_exported(), |
| 177 } |
155 self.send_json_dict(response_dict) | 178 self.send_json_dict(response_dict) |
156 else: | 179 else: |
157 self.send_error(404) | 180 self.send_error(404) |
158 | 181 |
159 def do_GET_static(self, path): | 182 def do_GET_static(self, path): |
160 """ Handle a GET request for a file under the 'static' directory. | 183 """ Handle a GET request for a file under the 'static' directory. |
161 Only allow serving of files within the 'static' directory that is a | 184 Only allow serving of files within the 'static' directory that is a |
162 filesystem sibling of this script. """ | 185 filesystem sibling of this script. |
| 186 |
| 187 Args: |
| 188 path: path to file (under static directory) to retrieve |
| 189 """ |
163 print 'do_GET_static: sending file "%s"' % path | 190 print 'do_GET_static: sending file "%s"' % path |
164 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | 191 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) |
165 full_path = os.path.realpath(os.path.join(static_dir, path)) | 192 full_path = os.path.realpath(os.path.join(static_dir, path)) |
166 if full_path.startswith(static_dir): | 193 if full_path.startswith(static_dir): |
167 self.send_file(full_path) | 194 self.send_file(full_path) |
168 else: | 195 else: |
169 print ('Attempted do_GET_static() of path [%s] outside of static dir [%s]' | 196 print ('Attempted do_GET_static() of path [%s] outside of static dir [%s]' |
170 % (full_path, static_dir)) | 197 % (full_path, static_dir)) |
171 self.send_error(404) | 198 self.send_error(404) |
172 | 199 |
173 def redirect_to(self, url): | 200 def redirect_to(self, url): |
174 """ Redirect the HTTP client to a different url. """ | 201 """ Redirect the HTTP client to a different url. |
| 202 |
| 203 Args: |
| 204 url: URL to redirect the HTTP client to |
| 205 """ |
175 self.send_response(301) | 206 self.send_response(301) |
176 self.send_header('Location', url) | 207 self.send_header('Location', url) |
177 self.end_headers() | 208 self.end_headers() |
178 | 209 |
179 def send_file(self, path): | 210 def send_file(self, path): |
180 """ Send the contents of the file at this path, with a mimetype based | 211 """ Send the contents of the file at this path, with a mimetype based |
181 on the filename extension. """ | 212 on the filename extension. |
| 213 |
| 214 Args: |
| 215 path: path of file whose contents to send to the HTTP client |
| 216 """ |
182 # Grab the extension if there is one | 217 # Grab the extension if there is one |
183 extension = os.path.splitext(path)[1] | 218 extension = os.path.splitext(path)[1] |
184 if len(extension) >= 1: | 219 if len(extension) >= 1: |
185 extension = extension[1:] | 220 extension = extension[1:] |
186 | 221 |
187 # Determine the MIME type of the file from its extension | 222 # Determine the MIME type of the file from its extension |
188 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) | 223 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) |
189 | 224 |
190 # Open the file and send it over HTTP | 225 # Open the file and send it over HTTP |
191 if os.path.isfile(path): | 226 if os.path.isfile(path): |
192 with open(path, 'rb') as sending_file: | 227 with open(path, 'rb') as sending_file: |
193 self.send_response(200) | 228 self.send_response(200) |
194 self.send_header('Content-type', mime_type) | 229 self.send_header('Content-type', mime_type) |
195 self.end_headers() | 230 self.end_headers() |
196 self.wfile.write(sending_file.read()) | 231 self.wfile.write(sending_file.read()) |
197 else: | 232 else: |
198 self.send_error(404) | 233 self.send_error(404) |
199 | 234 |
200 def send_json_dict(self, json_dict): | 235 def send_json_dict(self, json_dict): |
201 """ Send the contents of this dictionary in JSON format, with a JSON | 236 """ Send the contents of this dictionary in JSON format, with a JSON |
202 mimetype. """ | 237 mimetype. |
| 238 |
| 239 Args: |
| 240 json_dict: dictionary to send |
| 241 """ |
203 self.send_response(200) | 242 self.send_response(200) |
204 self.send_header('Content-type', 'application/json') | 243 self.send_header('Content-type', 'application/json') |
205 self.end_headers() | 244 self.end_headers() |
206 json.dump(json_dict, self.wfile) | 245 json.dump(json_dict, self.wfile) |
207 | 246 |
208 | 247 |
209 def main(): | 248 def main(): |
210 parser = argparse.ArgumentParser() | 249 parser = argparse.ArgumentParser() |
211 parser.add_argument('--actuals-dir', | 250 parser.add_argument('--actuals-dir', |
212 help=('Directory into which we will check out the latest ' | 251 help=('Directory into which we will check out the latest ' |
(...skipping 15 matching lines...) Expand all Loading... |
228 'defaults to %(default)s'), | 267 'defaults to %(default)s'), |
229 default=DEFAULT_PORT) | 268 default=DEFAULT_PORT) |
230 args = parser.parse_args() | 269 args = parser.parse_args() |
231 global _SERVER | 270 global _SERVER |
232 _SERVER = Server(expectations_dir=args.expectations_dir, | 271 _SERVER = Server(expectations_dir=args.expectations_dir, |
233 port=args.port, export=args.export) | 272 port=args.port, export=args.export) |
234 _SERVER.run() | 273 _SERVER.run() |
235 | 274 |
236 if __name__ == '__main__': | 275 if __name__ == '__main__': |
237 main() | 276 main() |
OLD | NEW |