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 |