Chromium Code Reviews| 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, so that we can import svn.py within | 31 # We need to add the 'tools' directory for svn.py, and the 'gm' directory for |
| 32 # that directory. | 32 # gm_json.py . |
| 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | 33 # Make sure that these dirs are in the PYTHONPATH, but add them 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 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 36 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) |
| 37 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) | |
| 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 38 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
| 38 if TOOLS_DIRECTORY not in sys.path: | 39 if TOOLS_DIRECTORY not in sys.path: |
| 39 sys.path.append(TOOLS_DIRECTORY) | 40 sys.path.append(TOOLS_DIRECTORY) |
| 40 import svn | 41 import svn |
| 42 if GM_DIRECTORY not in sys.path: | |
| 43 sys.path.append(GM_DIRECTORY) | |
| 44 import gm_json | |
| 41 | 45 |
| 42 # Imports from local dir | 46 # Imports from local dir |
| 43 # | 47 # |
| 44 # Note: we import results under a different name, to avoid confusion with the | 48 # Note: we import results under a different name, to avoid confusion with the |
| 45 # Server.results() property. See discussion at | 49 # Server.results() property. See discussion at |
| 46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44 | 50 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44 |
| 47 import imagepairset | 51 import imagepairset |
| 48 import results as results_mod | 52 import results as results_mod |
| 49 | 53 |
| 50 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 54 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 64 # NOTE: Keep these in sync with static/constants.js | 68 # NOTE: Keep these in sync with static/constants.js |
| 65 KEY__EDITS__MODIFICATIONS = 'modifications' | 69 KEY__EDITS__MODIFICATIONS = 'modifications' |
| 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 70 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
| 67 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 71 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
| 68 | 72 |
| 69 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR | 73 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
| 70 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 74 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' |
| 71 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 75 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
| 72 DEFAULT_PORT = 8888 | 76 DEFAULT_PORT = 8888 |
| 73 | 77 |
| 78 # Directory within which the server will serve out static files. | |
| 79 STATIC_CONTENTS_DIR = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | |
| 80 GENERATED_IMAGES_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-images') | |
| 81 GENERATED_JSON_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-json') | |
|
epoger
2014/03/19 21:13:51
FOR NOW we still need the server running so the cl
| |
| 82 | |
| 74 # How often (in seconds) clients should reload while waiting for initial | 83 # How often (in seconds) clients should reload while waiting for initial |
| 75 # results to load. | 84 # results to load. |
| 76 RELOAD_INTERVAL_UNTIL_READY = 10 | 85 RELOAD_INTERVAL_UNTIL_READY = 10 |
| 77 | 86 |
| 78 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 87 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 79 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 88 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 80 | 89 |
| 81 _SERVER = None # This gets filled in by main() | 90 _SERVER = None # This gets filled in by main() |
| 82 | 91 |
| 83 | 92 |
| (...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 225 # | 234 # |
| 226 # Because Skia uses depot_tools, we have to update using "gclient sync" | 235 # Because Skia uses depot_tools, we have to update using "gclient sync" |
| 227 # instead of raw git (or SVN) update. Happily, this will work whether | 236 # instead of raw git (or SVN) update. Happily, this will work whether |
| 228 # the checkout was created using git or SVN. | 237 # the checkout was created using git or SVN. |
| 229 if self._reload_seconds: | 238 if self._reload_seconds: |
| 230 logging.info( | 239 logging.info( |
| 231 'Updating expected GM results in %s by syncing Skia repo ...' % | 240 'Updating expected GM results in %s by syncing Skia repo ...' % |
| 232 results_mod.DEFAULT_EXPECTATIONS_DIR) | 241 results_mod.DEFAULT_EXPECTATIONS_DIR) |
| 233 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | 242 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) |
| 234 | 243 |
| 235 self._results = results_mod.Results(actuals_root=self._actuals_dir) | 244 new_results = results_mod.Results( |
| 245 actuals_root=self._actuals_dir, | |
| 246 generated_images_root=GENERATED_IMAGES_DIR, | |
| 247 diff_base_url=os.path.relpath( | |
|
epoger
2014/03/19 21:13:51
Because this uses a relative diff_base_url, this C
| |
| 248 GENERATED_IMAGES_DIR, GENERATED_JSON_DIR)) | |
| 249 if not os.path.isdir(GENERATED_JSON_DIR): | |
| 250 os.makedirs(GENERATED_JSON_DIR) | |
| 251 for summary_type in [results_mod.KEY__HEADER__RESULTS_ALL, | |
| 252 results_mod.KEY__HEADER__RESULTS_FAILURES]: | |
| 253 gm_json.WriteToFile( | |
| 254 new_results.get_packaged_results_of_type(results_type=summary_type), | |
| 255 os.path.join(GENERATED_JSON_DIR, '%s.json' % summary_type)) | |
| 256 | |
| 257 self._results = new_results | |
|
epoger
2014/03/19 21:13:51
Even though we no longer serve the JSON from the l
| |
| 236 | 258 |
| 237 def _result_loader(self, reload_seconds=0): | 259 def _result_loader(self, reload_seconds=0): |
| 238 """ Call self.update_results(), either once or periodically. | 260 """ Call self.update_results(), either once or periodically. |
| 239 | 261 |
| 240 Params: | 262 Params: |
| 241 reload_seconds: integer; if nonzero, reload results at this interval | 263 reload_seconds: integer; if nonzero, reload results at this interval |
| 242 (in which case, this method will never return!) | 264 (in which case, this method will never return!) |
| 243 """ | 265 """ |
| 244 self.update_results() | 266 self.update_results() |
| 245 logging.info('Initial results loaded. Ready for requests on %s' % self._url) | 267 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 289 self.redirect_to('/static/favicon.ico') | 311 self.redirect_to('/static/favicon.ico') |
| 290 return | 312 return |
| 291 | 313 |
| 292 # All requests must be of this form: | 314 # All requests must be of this form: |
| 293 # /dispatcher/remainder | 315 # /dispatcher/remainder |
| 294 # where 'dispatcher' indicates which do_GET_* dispatcher to run | 316 # where 'dispatcher' indicates which do_GET_* dispatcher to run |
| 295 # and 'remainder' is the remaining path sent to that dispatcher. | 317 # and 'remainder' is the remaining path sent to that dispatcher. |
| 296 normpath = posixpath.normpath(self.path) | 318 normpath = posixpath.normpath(self.path) |
| 297 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() | 319 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() |
| 298 dispatchers = { | 320 dispatchers = { |
| 299 'results': self.do_GET_results, | |
| 300 'static': self.do_GET_static, | 321 'static': self.do_GET_static, |
| 301 } | 322 } |
| 302 dispatcher = dispatchers[dispatcher_name] | 323 dispatcher = dispatchers[dispatcher_name] |
| 303 dispatcher(remainder) | 324 dispatcher(remainder) |
| 304 except: | 325 except: |
| 305 self.send_error(404) | 326 self.send_error(404) |
| 306 raise | 327 raise |
| 307 | 328 |
| 308 def do_GET_results(self, results_type): | |
| 309 """ Handle a GET request for GM results. | |
| 310 | |
| 311 Args: | |
| 312 results_type: string indicating which set of results to return; | |
| 313 must be one of the results_mod.RESULTS_* constants | |
| 314 """ | |
| 315 logging.debug('do_GET_results: sending results of type "%s"' % results_type) | |
| 316 # Since we must make multiple calls to the Results object, grab a | |
| 317 # reference to it in case it is updated to point at a new Results | |
| 318 # object within another thread. | |
| 319 # | |
| 320 # TODO(epoger): Rather than using a global variable for the handler | |
| 321 # to refer to the Server object, make Server a subclass of | |
| 322 # HTTPServer, and then it could be available to the handler via | |
| 323 # the handler's .server instance variable. | |
| 324 results_obj = _SERVER.results | |
| 325 if results_obj: | |
| 326 response_dict = results_obj.get_packaged_results_of_type( | |
| 327 results_type=results_type, reload_seconds=_SERVER.reload_seconds, | |
| 328 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) | |
| 329 else: | |
| 330 now = int(time.time()) | |
| 331 response_dict = { | |
| 332 results_mod.KEY__HEADER: { | |
| 333 results_mod.KEY__HEADER__SCHEMA_VERSION: ( | |
| 334 results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER), | |
| 335 results_mod.KEY__HEADER__IS_STILL_LOADING: True, | |
| 336 results_mod.KEY__HEADER__TIME_UPDATED: now, | |
| 337 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( | |
| 338 now + RELOAD_INTERVAL_UNTIL_READY), | |
| 339 }, | |
| 340 } | |
| 341 self.send_json_dict(response_dict) | |
| 342 | |
| 343 def do_GET_static(self, path): | 329 def do_GET_static(self, path): |
| 344 """ Handle a GET request for a file under the 'static' directory. | 330 """ Handle a GET request for a file under the 'static' directory. |
| 345 Only allow serving of files within the 'static' directory that is a | 331 Only allow serving of files within the 'static' directory that is a |
| 346 filesystem sibling of this script. | 332 filesystem sibling of this script. |
| 347 | 333 |
| 348 Args: | 334 Args: |
| 349 path: path to file (under static directory) to retrieve | 335 path: path to file (under static directory) to retrieve |
| 350 """ | 336 """ |
| 351 # Strip arguments ('?resultsToLoad=all') from the path | 337 # Strip arguments ('?resultsToLoad=all') from the path |
| 352 path = urlparse.urlparse(path).path | 338 path = urlparse.urlparse(path).path |
| 353 | 339 |
| 354 logging.debug('do_GET_static: sending file "%s"' % path) | 340 logging.debug('do_GET_static: sending file "%s"' % path) |
| 355 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | 341 full_path = os.path.realpath(os.path.join(STATIC_CONTENTS_DIR, path)) |
| 356 full_path = os.path.realpath(os.path.join(static_dir, path)) | 342 if full_path.startswith(STATIC_CONTENTS_DIR): |
| 357 if full_path.startswith(static_dir): | |
| 358 self.send_file(full_path) | 343 self.send_file(full_path) |
| 359 else: | 344 else: |
| 360 logging.error( | 345 logging.error( |
| 361 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' | 346 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' |
| 362 % (full_path, static_dir)) | 347 % (full_path, STATIC_CONTENTS_DIR)) |
| 363 self.send_error(404) | 348 self.send_error(404) |
| 364 | 349 |
| 365 def do_POST(self): | 350 def do_POST(self): |
| 366 """ Handles all POST requests, forwarding them to the appropriate | 351 """ Handles all POST requests, forwarding them to the appropriate |
| 367 do_POST_* dispatcher. """ | 352 do_POST_* dispatcher. """ |
| 368 # All requests must be of this form: | 353 # All requests must be of this form: |
| 369 # /dispatcher | 354 # /dispatcher |
| 370 # where 'dispatcher' indicates which do_POST_* dispatcher to run. | 355 # where 'dispatcher' indicates which do_POST_* dispatcher to run. |
| 371 logging.debug('do_POST: path="%s"' % self.path) | 356 logging.debug('do_POST: path="%s"' % self.path) |
| 372 normpath = posixpath.normpath(self.path) | 357 normpath = posixpath.normpath(self.path) |
| (...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 464 # Open the file and send it over HTTP | 449 # Open the file and send it over HTTP |
| 465 if os.path.isfile(path): | 450 if os.path.isfile(path): |
| 466 with open(path, 'rb') as sending_file: | 451 with open(path, 'rb') as sending_file: |
| 467 self.send_response(200) | 452 self.send_response(200) |
| 468 self.send_header('Content-type', mime_type) | 453 self.send_header('Content-type', mime_type) |
| 469 self.end_headers() | 454 self.end_headers() |
| 470 self.wfile.write(sending_file.read()) | 455 self.wfile.write(sending_file.read()) |
| 471 else: | 456 else: |
| 472 self.send_error(404) | 457 self.send_error(404) |
| 473 | 458 |
| 474 def send_json_dict(self, json_dict): | |
| 475 """ Send the contents of this dictionary in JSON format, with a JSON | |
| 476 mimetype. | |
| 477 | |
| 478 Args: | |
| 479 json_dict: dictionary to send | |
| 480 """ | |
| 481 self.send_response(200) | |
| 482 self.send_header('Content-type', 'application/json') | |
| 483 self.end_headers() | |
| 484 json.dump(json_dict, self.wfile) | |
| 485 | |
| 486 | 459 |
| 487 def main(): | 460 def main(): |
| 488 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', | 461 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', |
| 489 datefmt='%m/%d/%Y %H:%M:%S', | 462 datefmt='%m/%d/%Y %H:%M:%S', |
| 490 level=logging.INFO) | 463 level=logging.INFO) |
| 491 parser = argparse.ArgumentParser() | 464 parser = argparse.ArgumentParser() |
| 492 parser.add_argument('--actuals-dir', | 465 parser.add_argument('--actuals-dir', |
| 493 help=('Directory into which we will check out the latest ' | 466 help=('Directory into which we will check out the latest ' |
| 494 'actual GM results. If this directory does not ' | 467 'actual GM results. If this directory does not ' |
| 495 'exist, it will be created. Defaults to %(default)s'), | 468 'exist, it will be created. Defaults to %(default)s'), |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 529 _SERVER = Server(actuals_dir=args.actuals_dir, | 502 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 530 actuals_repo_revision=args.actuals_revision, | 503 actuals_repo_revision=args.actuals_revision, |
| 531 actuals_repo_url=args.actuals_repo, | 504 actuals_repo_url=args.actuals_repo, |
| 532 port=args.port, export=args.export, editable=args.editable, | 505 port=args.port, export=args.export, editable=args.editable, |
| 533 reload_seconds=args.reload) | 506 reload_seconds=args.reload) |
| 534 _SERVER.run() | 507 _SERVER.run() |
| 535 | 508 |
| 536 | 509 |
| 537 if __name__ == '__main__': | 510 if __name__ == '__main__': |
| 538 main() | 511 main() |
| OLD | NEW |