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 | 8 |
9 HTTP server for our HTML rebaseline viewer. | 9 HTTP server for our HTML rebaseline viewer. |
10 """ | 10 """ |
(...skipping 10 matching lines...) Expand all Loading... |
21 import socket | 21 import socket |
22 import subprocess | 22 import subprocess |
23 import sys | 23 import sys |
24 import thread | 24 import thread |
25 import threading | 25 import threading |
26 import time | 26 import time |
27 import urlparse | 27 import urlparse |
28 | 28 |
29 # Imports from within Skia | 29 # Imports from within Skia |
30 # | 30 # |
31 # We need to add the 'tools' directory for svn.py, and the 'gm' directory for | 31 # We need to add the 'tools' directory, so that we can import svn.py within |
32 # gm_json.py . | 32 # that directory. |
33 # Make sure that these dirs are in the PYTHONPATH, but add them at the *end* | 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* |
34 # so any dirs that are already in the PYTHONPATH will be preferred. | 34 # so any dirs that are already in the PYTHONPATH will be preferred. |
35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
36 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) | 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) |
37 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) | |
38 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
39 if TOOLS_DIRECTORY not in sys.path: | 38 if TOOLS_DIRECTORY not in sys.path: |
40 sys.path.append(TOOLS_DIRECTORY) | 39 sys.path.append(TOOLS_DIRECTORY) |
41 import svn | 40 import svn |
42 if GM_DIRECTORY not in sys.path: | |
43 sys.path.append(GM_DIRECTORY) | |
44 import gm_json | |
45 | 41 |
46 # Imports from local dir | 42 # Imports from local dir |
47 # | 43 # |
48 # Note: we import results under a different name, to avoid confusion with the | 44 # Note: we import results under a different name, to avoid confusion with the |
49 # Server.results() property. See discussion at | 45 # Server.results() property. See discussion at |
50 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p
y#newcode44 | 46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p
y#newcode44 |
51 import imagepairset | 47 import imagepairset |
52 import results as results_mod | 48 import results as results_mod |
53 | 49 |
54 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 50 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
(...skipping 14 matching lines...) Expand all Loading... |
69 KEY__EDITS__MODIFICATIONS = 'modifications' | 65 KEY__EDITS__MODIFICATIONS = 'modifications' |
70 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
71 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 67 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
72 | 68 |
73 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR | 69 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
74 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 70 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' |
75 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 71 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
76 DEFAULT_PORT = 8888 | 72 DEFAULT_PORT = 8888 |
77 | 73 |
78 # Directory within which the server will serve out static files. | 74 # Directory within which the server will serve out static files. |
79 STATIC_CONTENTS_DIR = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | 75 STATIC_CONTENTS_SUBDIR = 'static' # within PARENT_DIR |
80 GENERATED_IMAGES_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-images') | 76 GENERATED_IMAGES_SUBDIR = 'generated-images' # within STATIC_CONTENTS_SUBDIR |
81 GENERATED_JSON_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-json') | |
82 | 77 |
83 # How often (in seconds) clients should reload while waiting for initial | 78 # How often (in seconds) clients should reload while waiting for initial |
84 # results to load. | 79 # results to load. |
85 RELOAD_INTERVAL_UNTIL_READY = 10 | 80 RELOAD_INTERVAL_UNTIL_READY = 10 |
86 | 81 |
87 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 82 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
88 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 83 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
89 | 84 |
90 SUMMARY_TYPES = [ | |
91 results_mod.KEY__HEADER__RESULTS_ALL, | |
92 results_mod.KEY__HEADER__RESULTS_FAILURES, | |
93 ] | |
94 | |
95 _SERVER = None # This gets filled in by main() | 85 _SERVER = None # This gets filled in by main() |
96 | 86 |
97 | 87 |
98 def _run_command(args, directory): | 88 def _run_command(args, directory): |
99 """Runs a command and returns stdout as a single string. | 89 """Runs a command and returns stdout as a single string. |
100 | 90 |
101 Args: | 91 Args: |
102 args: the command to run, as a list of arguments | 92 args: the command to run, as a list of arguments |
103 directory: directory within which to run the command | 93 directory: directory within which to run the command |
104 | 94 |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
170 self._actuals_dir = actuals_dir | 160 self._actuals_dir = actuals_dir |
171 self._actuals_repo_revision = actuals_repo_revision | 161 self._actuals_repo_revision = actuals_repo_revision |
172 self._actuals_repo_url = actuals_repo_url | 162 self._actuals_repo_url = actuals_repo_url |
173 self._port = port | 163 self._port = port |
174 self._export = export | 164 self._export = export |
175 self._editable = editable | 165 self._editable = editable |
176 self._reload_seconds = reload_seconds | 166 self._reload_seconds = reload_seconds |
177 self._actuals_repo = _create_svn_checkout( | 167 self._actuals_repo = _create_svn_checkout( |
178 dir_path=actuals_dir, repo_url=actuals_repo_url) | 168 dir_path=actuals_dir, repo_url=actuals_repo_url) |
179 | 169 |
180 # Since we don't have any results ready yet, prepare a dummy results file | |
181 # telling any clients that we're still working on the results. | |
182 response_dict = { | |
183 results_mod.KEY__HEADER: { | |
184 results_mod.KEY__HEADER__SCHEMA_VERSION: ( | |
185 results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER), | |
186 results_mod.KEY__HEADER__IS_STILL_LOADING: True, | |
187 results_mod.KEY__HEADER__TIME_UPDATED: 0, | |
188 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( | |
189 RELOAD_INTERVAL_UNTIL_READY), | |
190 }, | |
191 } | |
192 if not os.path.isdir(GENERATED_JSON_DIR): | |
193 os.makedirs(GENERATED_JSON_DIR) | |
194 for summary_type in SUMMARY_TYPES: | |
195 gm_json.WriteToFile( | |
196 response_dict, | |
197 os.path.join(GENERATED_JSON_DIR, '%s.json' % summary_type)) | |
198 | |
199 # Reentrant lock that must be held whenever updating EITHER of: | 170 # Reentrant lock that must be held whenever updating EITHER of: |
200 # 1. self._results | 171 # 1. self._results |
201 # 2. the expected or actual results on local disk | 172 # 2. the expected or actual results on local disk |
202 self.results_rlock = threading.RLock() | 173 self.results_rlock = threading.RLock() |
203 # self._results will be filled in by calls to update_results() | 174 # self._results will be filled in by calls to update_results() |
204 self._results = None | 175 self._results = None |
205 | 176 |
206 @property | 177 @property |
207 def results(self): | 178 def results(self): |
208 """ Returns the most recently generated results, or None if we don't have | 179 """ Returns the most recently generated results, or None if we don't have |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
258 # | 229 # |
259 # Because Skia uses depot_tools, we have to update using "gclient sync" | 230 # Because Skia uses depot_tools, we have to update using "gclient sync" |
260 # instead of raw git (or SVN) update. Happily, this will work whether | 231 # instead of raw git (or SVN) update. Happily, this will work whether |
261 # the checkout was created using git or SVN. | 232 # the checkout was created using git or SVN. |
262 if self._reload_seconds: | 233 if self._reload_seconds: |
263 logging.info( | 234 logging.info( |
264 'Updating expected GM results in %s by syncing Skia repo ...' % | 235 'Updating expected GM results in %s by syncing Skia repo ...' % |
265 results_mod.DEFAULT_EXPECTATIONS_DIR) | 236 results_mod.DEFAULT_EXPECTATIONS_DIR) |
266 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | 237 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) |
267 | 238 |
268 new_results = results_mod.Results( | 239 self._results = results_mod.Results( |
269 actuals_root=self._actuals_dir, | 240 actuals_root=self._actuals_dir, |
270 generated_images_root=GENERATED_IMAGES_DIR, | 241 generated_images_root=os.path.join( |
271 diff_base_url=os.path.relpath( | 242 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, |
272 GENERATED_IMAGES_DIR, GENERATED_JSON_DIR)) | 243 GENERATED_IMAGES_SUBDIR), |
273 | 244 diff_base_url=posixpath.join( |
274 if not os.path.isdir(GENERATED_JSON_DIR): | 245 os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR)) |
275 os.makedirs(GENERATED_JSON_DIR) | |
276 for summary_type in SUMMARY_TYPES: | |
277 gm_json.WriteToFile( | |
278 new_results.get_packaged_results_of_type(results_type=summary_type), | |
279 os.path.join(GENERATED_JSON_DIR, '%s.json' % summary_type)) | |
280 | |
281 self._results = new_results | |
282 | 246 |
283 def _result_loader(self, reload_seconds=0): | 247 def _result_loader(self, reload_seconds=0): |
284 """ Call self.update_results(), either once or periodically. | 248 """ Call self.update_results(), either once or periodically. |
285 | 249 |
286 Params: | 250 Params: |
287 reload_seconds: integer; if nonzero, reload results at this interval | 251 reload_seconds: integer; if nonzero, reload results at this interval |
288 (in which case, this method will never return!) | 252 (in which case, this method will never return!) |
289 """ | 253 """ |
290 self.update_results() | 254 self.update_results() |
291 logging.info('Initial results loaded. Ready for requests on %s' % self._url) | 255 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
(...skipping 30 matching lines...) Expand all Loading... |
322 def do_GET(self): | 286 def do_GET(self): |
323 """ | 287 """ |
324 Handles all GET requests, forwarding them to the appropriate | 288 Handles all GET requests, forwarding them to the appropriate |
325 do_GET_* dispatcher. | 289 do_GET_* dispatcher. |
326 | 290 |
327 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 | 291 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 |
328 """ | 292 """ |
329 try: | 293 try: |
330 logging.debug('do_GET: path="%s"' % self.path) | 294 logging.debug('do_GET: path="%s"' % self.path) |
331 if self.path == '' or self.path == '/' or self.path == '/index.html' : | 295 if self.path == '' or self.path == '/' or self.path == '/index.html' : |
332 self.redirect_to('/static/index.html') | 296 self.redirect_to('/%s/index.html' % STATIC_CONTENTS_SUBDIR) |
333 return | 297 return |
334 if self.path == '/favicon.ico' : | 298 if self.path == '/favicon.ico' : |
335 self.redirect_to('/static/favicon.ico') | 299 self.redirect_to('/%s/favicon.ico' % STATIC_CONTENTS_SUBDIR) |
336 return | 300 return |
337 | 301 |
338 # All requests must be of this form: | 302 # All requests must be of this form: |
339 # /dispatcher/remainder | 303 # /dispatcher/remainder |
340 # where 'dispatcher' indicates which do_GET_* dispatcher to run | 304 # where 'dispatcher' indicates which do_GET_* dispatcher to run |
341 # and 'remainder' is the remaining path sent to that dispatcher. | 305 # and 'remainder' is the remaining path sent to that dispatcher. |
342 normpath = posixpath.normpath(self.path) | 306 normpath = posixpath.normpath(self.path) |
343 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() | 307 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() |
344 dispatchers = { | 308 dispatchers = { |
345 'static': self.do_GET_static, | 309 'results': self.do_GET_results, |
| 310 STATIC_CONTENTS_SUBDIR: self.do_GET_static, |
346 } | 311 } |
347 dispatcher = dispatchers[dispatcher_name] | 312 dispatcher = dispatchers[dispatcher_name] |
348 dispatcher(remainder) | 313 dispatcher(remainder) |
349 except: | 314 except: |
350 self.send_error(404) | 315 self.send_error(404) |
351 raise | 316 raise |
352 | 317 |
| 318 def do_GET_results(self, results_type): |
| 319 """ Handle a GET request for GM results. |
| 320 |
| 321 Args: |
| 322 results_type: string indicating which set of results to return; |
| 323 must be one of the results_mod.RESULTS_* constants |
| 324 """ |
| 325 logging.debug('do_GET_results: sending results of type "%s"' % results_type) |
| 326 # Since we must make multiple calls to the Results object, grab a |
| 327 # reference to it in case it is updated to point at a new Results |
| 328 # object within another thread. |
| 329 # |
| 330 # TODO(epoger): Rather than using a global variable for the handler |
| 331 # to refer to the Server object, make Server a subclass of |
| 332 # HTTPServer, and then it could be available to the handler via |
| 333 # the handler's .server instance variable. |
| 334 results_obj = _SERVER.results |
| 335 if results_obj: |
| 336 response_dict = results_obj.get_packaged_results_of_type( |
| 337 results_type=results_type, reload_seconds=_SERVER.reload_seconds, |
| 338 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) |
| 339 else: |
| 340 now = int(time.time()) |
| 341 response_dict = { |
| 342 results_mod.KEY__HEADER: { |
| 343 results_mod.KEY__HEADER__SCHEMA_VERSION: ( |
| 344 results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER), |
| 345 results_mod.KEY__HEADER__IS_STILL_LOADING: True, |
| 346 results_mod.KEY__HEADER__TIME_UPDATED: now, |
| 347 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( |
| 348 now + RELOAD_INTERVAL_UNTIL_READY), |
| 349 }, |
| 350 } |
| 351 self.send_json_dict(response_dict) |
| 352 |
353 def do_GET_static(self, path): | 353 def do_GET_static(self, path): |
354 """ Handle a GET request for a file under the 'static' directory. | 354 """ Handle a GET request for a file under STATIC_CONTENTS_SUBDIR . |
355 Only allow serving of files within the 'static' directory that is a | 355 Only allow serving of files within STATIC_CONTENTS_SUBDIR that is a |
356 filesystem sibling of this script. | 356 filesystem sibling of this script. |
357 | 357 |
358 Args: | 358 Args: |
359 path: path to file (under static directory) to retrieve | 359 path: path to file (within STATIC_CONTENTS_SUBDIR) to retrieve |
360 """ | 360 """ |
361 # Strip arguments ('?resultsToLoad=all') from the path | 361 # Strip arguments ('?resultsToLoad=all') from the path |
362 path = urlparse.urlparse(path).path | 362 path = urlparse.urlparse(path).path |
363 | 363 |
364 logging.debug('do_GET_static: sending file "%s"' % path) | 364 logging.debug('do_GET_static: sending file "%s"' % path) |
365 full_path = os.path.realpath(os.path.join(STATIC_CONTENTS_DIR, path)) | 365 static_dir = os.path.realpath(os.path.join( |
366 if full_path.startswith(STATIC_CONTENTS_DIR): | 366 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR)) |
| 367 full_path = os.path.realpath(os.path.join(static_dir, path)) |
| 368 if full_path.startswith(static_dir): |
367 self.send_file(full_path) | 369 self.send_file(full_path) |
368 else: | 370 else: |
369 logging.error( | 371 logging.error( |
370 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' | 372 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' |
371 % (full_path, STATIC_CONTENTS_DIR)) | 373 % (full_path, static_dir)) |
372 self.send_error(404) | 374 self.send_error(404) |
373 | 375 |
374 def do_POST(self): | 376 def do_POST(self): |
375 """ Handles all POST requests, forwarding them to the appropriate | 377 """ Handles all POST requests, forwarding them to the appropriate |
376 do_POST_* dispatcher. """ | 378 do_POST_* dispatcher. """ |
377 # All requests must be of this form: | 379 # All requests must be of this form: |
378 # /dispatcher | 380 # /dispatcher |
379 # where 'dispatcher' indicates which do_POST_* dispatcher to run. | 381 # where 'dispatcher' indicates which do_POST_* dispatcher to run. |
380 logging.debug('do_POST: path="%s"' % self.path) | 382 logging.debug('do_POST: path="%s"' % self.path) |
381 normpath = posixpath.normpath(self.path) | 383 normpath = posixpath.normpath(self.path) |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
473 # Open the file and send it over HTTP | 475 # Open the file and send it over HTTP |
474 if os.path.isfile(path): | 476 if os.path.isfile(path): |
475 with open(path, 'rb') as sending_file: | 477 with open(path, 'rb') as sending_file: |
476 self.send_response(200) | 478 self.send_response(200) |
477 self.send_header('Content-type', mime_type) | 479 self.send_header('Content-type', mime_type) |
478 self.end_headers() | 480 self.end_headers() |
479 self.wfile.write(sending_file.read()) | 481 self.wfile.write(sending_file.read()) |
480 else: | 482 else: |
481 self.send_error(404) | 483 self.send_error(404) |
482 | 484 |
| 485 def send_json_dict(self, json_dict): |
| 486 """ Send the contents of this dictionary in JSON format, with a JSON |
| 487 mimetype. |
| 488 |
| 489 Args: |
| 490 json_dict: dictionary to send |
| 491 """ |
| 492 self.send_response(200) |
| 493 self.send_header('Content-type', 'application/json') |
| 494 self.end_headers() |
| 495 json.dump(json_dict, self.wfile) |
| 496 |
483 | 497 |
484 def main(): | 498 def main(): |
485 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', | 499 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', |
486 datefmt='%m/%d/%Y %H:%M:%S', | 500 datefmt='%m/%d/%Y %H:%M:%S', |
487 level=logging.INFO) | 501 level=logging.INFO) |
488 parser = argparse.ArgumentParser() | 502 parser = argparse.ArgumentParser() |
489 parser.add_argument('--actuals-dir', | 503 parser.add_argument('--actuals-dir', |
490 help=('Directory into which we will check out the latest ' | 504 help=('Directory into which we will check out the latest ' |
491 'actual GM results. If this directory does not ' | 505 'actual GM results. If this directory does not ' |
492 'exist, it will be created. Defaults to %(default)s'), | 506 'exist, it will be created. Defaults to %(default)s'), |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
526 _SERVER = Server(actuals_dir=args.actuals_dir, | 540 _SERVER = Server(actuals_dir=args.actuals_dir, |
527 actuals_repo_revision=args.actuals_revision, | 541 actuals_repo_revision=args.actuals_revision, |
528 actuals_repo_url=args.actuals_repo, | 542 actuals_repo_url=args.actuals_repo, |
529 port=args.port, export=args.export, editable=args.editable, | 543 port=args.port, export=args.export, editable=args.editable, |
530 reload_seconds=args.reload) | 544 reload_seconds=args.reload) |
531 _SERVER.run() | 545 _SERVER.run() |
532 | 546 |
533 | 547 |
534 if __name__ == '__main__': | 548 if __name__ == '__main__': |
535 main() | 549 main() |
OLD | NEW |