| 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 22 matching lines...) Expand all Loading... |
| 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it 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 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) |
| 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
| 38 if TOOLS_DIRECTORY not in sys.path: | 38 if TOOLS_DIRECTORY not in sys.path: |
| 39 sys.path.append(TOOLS_DIRECTORY) | 39 sys.path.append(TOOLS_DIRECTORY) |
| 40 import svn | 40 import svn |
| 41 | 41 |
| 42 # Imports from local dir | 42 # Imports from local dir |
| 43 # |
| 44 # Note: we import results under a different name, to avoid confusion with the |
| 45 # Server.results() property. See discussion at |
| 46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p
y#newcode44 |
| 43 import imagepairset | 47 import imagepairset |
| 44 import results | 48 import results as results_mod |
| 45 | 49 |
| 46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 50 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
| 47 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | |
| 48 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', | |
| 49 'generated-images') | |
| 50 | 51 |
| 51 # A simple dictionary of file name extensions to MIME types. The empty string | 52 # A simple dictionary of file name extensions to MIME types. The empty string |
| 52 # entry is used as the default when no extension was given or if the extension | 53 # entry is used as the default when no extension was given or if the extension |
| 53 # has no entry in this dictionary. | 54 # has no entry in this dictionary. |
| 54 MIME_TYPE_MAP = {'': 'application/octet-stream', | 55 MIME_TYPE_MAP = {'': 'application/octet-stream', |
| 55 'html': 'text/html', | 56 'html': 'text/html', |
| 56 'css': 'text/css', | 57 'css': 'text/css', |
| 57 'png': 'image/png', | 58 'png': 'image/png', |
| 58 'js': 'application/javascript', | 59 'js': 'application/javascript', |
| 59 'json': 'application/json' | 60 'json': 'application/json' |
| 60 } | 61 } |
| 61 | 62 |
| 62 # Keys that server.py uses to create the toplevel content header. | 63 # Keys that server.py uses to create the toplevel content header. |
| 63 # NOTE: Keep these in sync with static/constants.js | 64 # NOTE: Keep these in sync with static/constants.js |
| 64 KEY__EDITS__MODIFICATIONS = 'modifications' | 65 KEY__EDITS__MODIFICATIONS = 'modifications' |
| 65 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
| 66 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 67 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
| 67 KEY__HEADER = 'header' | |
| 68 KEY__HEADER__DATAHASH = 'dataHash' | |
| 69 KEY__HEADER__IS_EDITABLE = 'isEditable' | |
| 70 KEY__HEADER__IS_EXPORTED = 'isExported' | |
| 71 KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading' | |
| 72 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable' | |
| 73 KEY__HEADER__TIME_UPDATED = 'timeUpdated' | |
| 74 KEY__HEADER__TYPE = 'type' | |
| 75 | 68 |
| 76 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 69 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
| 77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 70 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' |
| 78 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' |
| 79 DEFAULT_PORT = 8888 | 72 DEFAULT_PORT = 8888 |
| 80 | 73 |
| 81 # How often (in seconds) clients should reload while waiting for initial | 74 # How often (in seconds) clients should reload while waiting for initial |
| 82 # results to load. | 75 # results to load. |
| 83 RELOAD_INTERVAL_UNTIL_READY = 10 | 76 RELOAD_INTERVAL_UNTIL_READY = 10 |
| 84 | 77 |
| 85 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 78 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 86 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 79 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| (...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 194 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 187 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
| 195 return self._editable | 188 return self._editable |
| 196 | 189 |
| 197 @property | 190 @property |
| 198 def reload_seconds(self): | 191 def reload_seconds(self): |
| 199 """ Returns the result reload period in seconds, or 0 if we don't reload | 192 """ Returns the result reload period in seconds, or 0 if we don't reload |
| 200 results. """ | 193 results. """ |
| 201 return self._reload_seconds | 194 return self._reload_seconds |
| 202 | 195 |
| 203 def update_results(self, invalidate=False): | 196 def update_results(self, invalidate=False): |
| 204 """ Create or update self._results, based on the expectations in | 197 """ Create or update self._results, based on the latest expectations and |
| 205 EXPECTATIONS_DIR and the latest actuals from skia-autogen. | 198 actuals. |
| 206 | 199 |
| 207 We hold self.results_rlock while we do this, to guarantee that no other | 200 We hold self.results_rlock while we do this, to guarantee that no other |
| 208 thread attempts to update either self._results or the underlying files at | 201 thread attempts to update either self._results or the underlying files at |
| 209 the same time. | 202 the same time. |
| 210 | 203 |
| 211 Args: | 204 Args: |
| 212 invalidate: if True, invalidate self._results immediately upon entry; | 205 invalidate: if True, invalidate self._results immediately upon entry; |
| 213 otherwise, we will let readers see those results until we | 206 otherwise, we will let readers see those results until we |
| 214 replace them | 207 replace them |
| 215 """ | 208 """ |
| (...skipping 13 matching lines...) Expand all Loading... |
| 229 # Because the Skia repo is moving from SVN to git, and git does not | 222 # Because the Skia repo is moving from SVN to git, and git does not |
| 230 # support updating a single directory tree, we have to update the entire | 223 # support updating a single directory tree, we have to update the entire |
| 231 # repo checkout. | 224 # repo checkout. |
| 232 # | 225 # |
| 233 # Because Skia uses depot_tools, we have to update using "gclient sync" | 226 # Because Skia uses depot_tools, we have to update using "gclient sync" |
| 234 # instead of raw git (or SVN) update. Happily, this will work whether | 227 # instead of raw git (or SVN) update. Happily, this will work whether |
| 235 # the checkout was created using git or SVN. | 228 # the checkout was created using git or SVN. |
| 236 if self._reload_seconds: | 229 if self._reload_seconds: |
| 237 logging.info( | 230 logging.info( |
| 238 'Updating expected GM results in %s by syncing Skia repo ...' % | 231 'Updating expected GM results in %s by syncing Skia repo ...' % |
| 239 EXPECTATIONS_DIR) | 232 results_mod.DEFAULT_EXPECTATIONS_DIR) |
| 240 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | 233 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) |
| 241 | 234 |
| 242 self._results = results.Results( | 235 self._results = results_mod.Results(actuals_root=self._actuals_dir) |
| 243 actuals_root=self._actuals_dir, | |
| 244 expected_root=EXPECTATIONS_DIR, | |
| 245 generated_images_root=GENERATED_IMAGES_ROOT) | |
| 246 | 236 |
| 247 def _result_loader(self, reload_seconds=0): | 237 def _result_loader(self, reload_seconds=0): |
| 248 """ Call self.update_results(), either once or periodically. | 238 """ Call self.update_results(), either once or periodically. |
| 249 | 239 |
| 250 Params: | 240 Params: |
| 251 reload_seconds: integer; if nonzero, reload results at this interval | 241 reload_seconds: integer; if nonzero, reload results at this interval |
| 252 (in which case, this method will never return!) | 242 (in which case, this method will never return!) |
| 253 """ | 243 """ |
| 254 self.update_results() | 244 self.update_results() |
| 255 logging.info('Initial results loaded. Ready for requests on %s' % self._url) | 245 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 308 dispatchers = { | 298 dispatchers = { |
| 309 'results': self.do_GET_results, | 299 'results': self.do_GET_results, |
| 310 'static': self.do_GET_static, | 300 'static': self.do_GET_static, |
| 311 } | 301 } |
| 312 dispatcher = dispatchers[dispatcher_name] | 302 dispatcher = dispatchers[dispatcher_name] |
| 313 dispatcher(remainder) | 303 dispatcher(remainder) |
| 314 except: | 304 except: |
| 315 self.send_error(404) | 305 self.send_error(404) |
| 316 raise | 306 raise |
| 317 | 307 |
| 318 def do_GET_results(self, type): | 308 def do_GET_results(self, results_type): |
| 319 """ Handle a GET request for GM results. | 309 """ Handle a GET request for GM results. |
| 320 | 310 |
| 321 Args: | 311 Args: |
| 322 type: string indicating which set of results to return; | 312 results_type: string indicating which set of results to return; |
| 323 must be one of the results.RESULTS_* constants | 313 must be one of the results_mod.RESULTS_* constants |
| 324 """ | 314 """ |
| 325 logging.debug('do_GET_results: sending results of type "%s"' % type) | 315 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 | 316 # 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 | 317 # reference to it in case it is updated to point at a new Results |
| 328 # object within another thread. | 318 # object within another thread. |
| 329 # | 319 # |
| 330 # TODO(epoger): Rather than using a global variable for the handler | 320 # TODO(epoger): Rather than using a global variable for the handler |
| 331 # to refer to the Server object, make Server a subclass of | 321 # to refer to the Server object, make Server a subclass of |
| 332 # HTTPServer, and then it could be available to the handler via | 322 # HTTPServer, and then it could be available to the handler via |
| 333 # the handler's .server instance variable. | 323 # the handler's .server instance variable. |
| 334 results_obj = _SERVER.results | 324 results_obj = _SERVER.results |
| 335 if results_obj: | 325 if results_obj: |
| 336 response_dict = self.package_results(results_obj, type) | 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) |
| 337 else: | 329 else: |
| 338 now = int(time.time()) | 330 now = int(time.time()) |
| 339 response_dict = { | 331 response_dict = { |
| 340 KEY__HEADER: { | 332 results_mod.KEY__HEADER: { |
| 341 KEY__HEADER__IS_STILL_LOADING: True, | 333 results_mod.KEY__HEADER__IS_STILL_LOADING: True, |
| 342 KEY__HEADER__TIME_UPDATED: now, | 334 results_mod.KEY__HEADER__TIME_UPDATED: now, |
| 343 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( | 335 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( |
| 344 now + RELOAD_INTERVAL_UNTIL_READY), | 336 now + RELOAD_INTERVAL_UNTIL_READY), |
| 345 }, | 337 }, |
| 346 } | 338 } |
| 347 self.send_json_dict(response_dict) | 339 self.send_json_dict(response_dict) |
| 348 | 340 |
| 349 def package_results(self, results_obj, type): | |
| 350 """ Given a nonempty "results" object, package it as a response_dict | |
| 351 as needed within do_GET_results. | |
| 352 | |
| 353 Args: | |
| 354 results_obj: nonempty "results" object | |
| 355 type: string indicating which set of results to return; | |
| 356 must be one of the results.RESULTS_* constants | |
| 357 """ | |
| 358 response_dict = results_obj.get_results_of_type(type) | |
| 359 time_updated = results_obj.get_timestamp() | |
| 360 response_dict[KEY__HEADER] = { | |
| 361 # Timestamps: | |
| 362 # 1. when this data was last updated | |
| 363 # 2. when the caller should check back for new data (if ever) | |
| 364 # | |
| 365 # We only return these timestamps if the --reload argument was passed; | |
| 366 # otherwise, we have no idea when the expectations were last updated | |
| 367 # (we allow the user to maintain her own expectations as she sees fit). | |
| 368 KEY__HEADER__TIME_UPDATED: | |
| 369 time_updated if _SERVER.reload_seconds else None, | |
| 370 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: | |
| 371 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds | |
| 372 else None, | |
| 373 | |
| 374 # The type we passed to get_results_of_type() | |
| 375 KEY__HEADER__TYPE: type, | |
| 376 | |
| 377 # Hash of dataset, which the client must return with any edits-- | |
| 378 # this ensures that the edits were made to a particular dataset. | |
| 379 KEY__HEADER__DATAHASH: str(hash(repr( | |
| 380 response_dict[imagepairset.KEY__IMAGEPAIRS]))), | |
| 381 | |
| 382 # Whether the server will accept edits back. | |
| 383 KEY__HEADER__IS_EDITABLE: _SERVER.is_editable, | |
| 384 | |
| 385 # Whether the service is accessible from other hosts. | |
| 386 KEY__HEADER__IS_EXPORTED: _SERVER.is_exported, | |
| 387 } | |
| 388 return response_dict | |
| 389 | |
| 390 def do_GET_static(self, path): | 341 def do_GET_static(self, path): |
| 391 """ Handle a GET request for a file under the 'static' directory. | 342 """ Handle a GET request for a file under the 'static' directory. |
| 392 Only allow serving of files within the 'static' directory that is a | 343 Only allow serving of files within the 'static' directory that is a |
| 393 filesystem sibling of this script. | 344 filesystem sibling of this script. |
| 394 | 345 |
| 395 Args: | 346 Args: |
| 396 path: path to file (under static directory) to retrieve | 347 path: path to file (under static directory) to retrieve |
| 397 """ | 348 """ |
| 398 # Strip arguments ('?resultsToLoad=all') from the path | 349 # Strip arguments ('?resultsToLoad=all') from the path |
| 399 path = urlparse.urlparse(path).path | 350 path = urlparse.urlparse(path).path |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 434 | 385 |
| 435 { | 386 { |
| 436 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client | 387 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client |
| 437 # loaded and then made | 388 # loaded and then made |
| 438 # modifications to | 389 # modifications to |
| 439 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client | 390 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client |
| 440 # loaded them (ensures that the | 391 # loaded them (ensures that the |
| 441 # client and server apply | 392 # client and server apply |
| 442 # modifications to the same base) | 393 # modifications to the same base) |
| 443 KEY__EDITS__MODIFICATIONS: [ | 394 KEY__EDITS__MODIFICATIONS: [ |
| 444 # as needed by results.edit_expectations() | 395 # as needed by results_mod.edit_expectations() |
| 445 ... | 396 ... |
| 446 ], | 397 ], |
| 447 } | 398 } |
| 448 | 399 |
| 449 Raises an Exception if there were any problems. | 400 Raises an Exception if there were any problems. |
| 450 """ | 401 """ |
| 451 if not _SERVER.is_editable: | 402 if not _SERVER.is_editable: |
| 452 raise Exception('this server is not running in --editable mode') | 403 raise Exception('this server is not running in --editable mode') |
| 453 | 404 |
| 454 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] | 405 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] |
| (...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 576 _SERVER = Server(actuals_dir=args.actuals_dir, | 527 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 577 actuals_repo_revision=args.actuals_revision, | 528 actuals_repo_revision=args.actuals_revision, |
| 578 actuals_repo_url=args.actuals_repo, | 529 actuals_repo_url=args.actuals_repo, |
| 579 port=args.port, export=args.export, editable=args.editable, | 530 port=args.port, export=args.export, editable=args.editable, |
| 580 reload_seconds=args.reload) | 531 reload_seconds=args.reload) |
| 581 _SERVER.run() | 532 _SERVER.run() |
| 582 | 533 |
| 583 | 534 |
| 584 if __name__ == '__main__': | 535 if __name__ == '__main__': |
| 585 main() | 536 main() |
| OLD | NEW |