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 |