| 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 |