| 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 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 55 'html': 'text/html', | 55 'html': 'text/html', |
| 56 'css': 'text/css', | 56 'css': 'text/css', |
| 57 'png': 'image/png', | 57 'png': 'image/png', |
| 58 'js': 'application/javascript', | 58 'js': 'application/javascript', |
| 59 'json': 'application/json' | 59 'json': 'application/json' |
| 60 } | 60 } |
| 61 | 61 |
| 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
| 63 DEFAULT_PORT = 8888 | 63 DEFAULT_PORT = 8888 |
| 64 | 64 |
| 65 # How often (in seconds) clients should reload while waiting for initial |
| 66 # results to load. |
| 67 RELOAD_INTERVAL_UNTIL_READY = 10 |
| 68 |
| 65 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 69 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 66 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 70 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 67 | 71 |
| 68 _SERVER = None # This gets filled in by main() | 72 _SERVER = None # This gets filled in by main() |
| 69 | 73 |
| 70 | 74 |
| 71 def _run_command(args, directory): | 75 def _run_command(args, directory): |
| 72 """Runs a command and returns stdout as a single string. | 76 """Runs a command and returns stdout as a single string. |
| 73 | 77 |
| 74 Args: | 78 Args: |
| (...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 206 | 210 |
| 207 logging.info( | 211 logging.info( |
| 208 ('Parsing results from actuals in %s and expectations in %s, ' | 212 ('Parsing results from actuals in %s and expectations in %s, ' |
| 209 + 'and generating pixel diffs (may take a while) ...') % ( | 213 + 'and generating pixel diffs (may take a while) ...') % ( |
| 210 self._actuals_dir, EXPECTATIONS_DIR)) | 214 self._actuals_dir, EXPECTATIONS_DIR)) |
| 211 self._results = results.Results( | 215 self._results = results.Results( |
| 212 actuals_root=self._actuals_dir, | 216 actuals_root=self._actuals_dir, |
| 213 expected_root=EXPECTATIONS_DIR, | 217 expected_root=EXPECTATIONS_DIR, |
| 214 generated_images_root=GENERATED_IMAGES_ROOT) | 218 generated_images_root=GENERATED_IMAGES_ROOT) |
| 215 | 219 |
| 216 def _result_reloader(self): | 220 def _result_loader(self, reload_seconds=0): |
| 217 """ Reload results at the appropriate interval. This never exits, so it | 221 """ Call self.update_results(), either once or periodically. |
| 218 should be run in its own thread. | 222 |
| 223 Params: |
| 224 reload_seconds: integer; if nonzero, reload results at this interval |
| 225 (in which case, this method will never return!) |
| 219 """ | 226 """ |
| 220 while True: | 227 self.update_results() |
| 221 time.sleep(self._reload_seconds) | 228 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
| 222 self.update_results() | 229 if reload_seconds: |
| 230 while True: |
| 231 time.sleep(reload_seconds) |
| 232 self.update_results() |
| 223 | 233 |
| 224 def run(self): | 234 def run(self): |
| 225 self.update_results() | 235 arg_tuple = (self._reload_seconds,) # start_new_thread needs a tuple, |
| 226 if self._reload_seconds: | 236 # even though it holds just one param |
| 227 thread.start_new_thread(self._result_reloader, ()) | 237 thread.start_new_thread(self._result_loader, arg_tuple) |
| 228 | 238 |
| 229 if self._export: | 239 if self._export: |
| 230 server_address = ('', self._port) | 240 server_address = ('', self._port) |
| 231 host = _get_routable_ip_address() | 241 host = _get_routable_ip_address() |
| 232 if self._editable: | 242 if self._editable: |
| 233 logging.warning('Running with combination of "export" and "editable" ' | 243 logging.warning('Running with combination of "export" and "editable" ' |
| 234 'flags. Users on other machines will ' | 244 'flags. Users on other machines will ' |
| 235 'be able to modify your GM expectations!') | 245 'be able to modify your GM expectations!') |
| 236 else: | 246 else: |
| 237 host = '127.0.0.1' | 247 host = '127.0.0.1' |
| 238 server_address = (host, self._port) | 248 server_address = (host, self._port) |
| 239 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) | 249 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) |
| 240 logging.info('Ready for requests on http://%s:%d' % ( | 250 self._url = 'http://%s:%d' % (host, http_server.server_port) |
| 241 host, http_server.server_port)) | 251 logging.info('Listening for requests on %s' % self._url) |
| 242 http_server.serve_forever() | 252 http_server.serve_forever() |
| 243 | 253 |
| 244 | 254 |
| 245 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 255 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 246 """ HTTP request handlers for various types of queries this server knows | 256 """ HTTP request handlers for various types of queries this server knows |
| 247 how to handle (static HTML and Javascript, expected/actual results, etc.) | 257 how to handle (static HTML and Javascript, expected/actual results, etc.) |
| 248 """ | 258 """ |
| 249 def do_GET(self): | 259 def do_GET(self): |
| 250 """ Handles all GET requests, forwarding them to the appropriate | 260 """ Handles all GET requests, forwarding them to the appropriate |
| 251 do_GET_* dispatcher. """ | 261 do_GET_* dispatcher. """ |
| (...skipping 28 matching lines...) Expand all Loading... |
| 280 try: | 290 try: |
| 281 # Since we must make multiple calls to the Results object, grab a | 291 # Since we must make multiple calls to the Results object, grab a |
| 282 # reference to it in case it is updated to point at a new Results | 292 # reference to it in case it is updated to point at a new Results |
| 283 # object within another thread. | 293 # object within another thread. |
| 284 # | 294 # |
| 285 # TODO(epoger): Rather than using a global variable for the handler | 295 # TODO(epoger): Rather than using a global variable for the handler |
| 286 # to refer to the Server object, make Server a subclass of | 296 # to refer to the Server object, make Server a subclass of |
| 287 # HTTPServer, and then it could be available to the handler via | 297 # HTTPServer, and then it could be available to the handler via |
| 288 # the handler's .server instance variable. | 298 # the handler's .server instance variable. |
| 289 results_obj = _SERVER.results | 299 results_obj = _SERVER.results |
| 290 response_dict = results_obj.get_results_of_type(type) | 300 if results_obj: |
| 291 time_updated = results_obj.get_timestamp() | 301 response_dict = self.package_results(results_obj, type) |
| 302 else: |
| 303 now = int(time.time()) |
| 304 response_dict = { |
| 305 'header': { |
| 306 'resultsStillLoading': True, |
| 307 'timeUpdated': now, |
| 308 'timeNextUpdateAvailable': now + RELOAD_INTERVAL_UNTIL_READY, |
| 309 }, |
| 310 } |
| 311 self.send_json_dict(response_dict) |
| 312 except: |
| 313 self.send_error(404) |
| 314 raise |
| 292 | 315 |
| 293 response_dict['header'] = { | 316 def package_results(self, results_obj, type): |
| 317 """ Given a nonempty "results" object, package it as a response_dict |
| 318 as needed within do_GET_results. |
| 319 |
| 320 Args: |
| 321 results_obj: nonempty "results" object |
| 322 type: string indicating which set of results to return; |
| 323 must be one of the results.RESULTS_* constants |
| 324 """ |
| 325 response_dict = results_obj.get_results_of_type(type) |
| 326 time_updated = results_obj.get_timestamp() |
| 327 response_dict['header'] = { |
| 294 # Timestamps: | 328 # Timestamps: |
| 295 # 1. when this data was last updated | 329 # 1. when this data was last updated |
| 296 # 2. when the caller should check back for new data (if ever) | 330 # 2. when the caller should check back for new data (if ever) |
| 297 # | 331 # |
| 298 # We only return these timestamps if the --reload argument was passed; | 332 # We only return these timestamps if the --reload argument was passed; |
| 299 # otherwise, we have no idea when the expectations were last updated | 333 # otherwise, we have no idea when the expectations were last updated |
| 300 # (we allow the user to maintain her own expectations as she sees fit). | 334 # (we allow the user to maintain her own expectations as she sees fit). |
| 301 'timeUpdated': time_updated if _SERVER.reload_seconds else None, | 335 'timeUpdated': time_updated if _SERVER.reload_seconds else None, |
| 302 'timeNextUpdateAvailable': ( | 336 'timeNextUpdateAvailable': ( |
| 303 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds | 337 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds |
| 304 else None), | 338 else None), |
| 305 | 339 |
| 306 # The type we passed to get_results_of_type() | 340 # The type we passed to get_results_of_type() |
| 307 'type': type, | 341 'type': type, |
| 308 | 342 |
| 309 # Hash of testData, which the client must return with any edits-- | 343 # Hash of testData, which the client must return with any edits-- |
| 310 # this ensures that the edits were made to a particular dataset. | 344 # this ensures that the edits were made to a particular dataset. |
| 311 'dataHash': str(hash(repr(response_dict['testData']))), | 345 'dataHash': str(hash(repr(response_dict['testData']))), |
| 312 | 346 |
| 313 # Whether the server will accept edits back. | 347 # Whether the server will accept edits back. |
| 314 'isEditable': _SERVER.is_editable, | 348 'isEditable': _SERVER.is_editable, |
| 315 | 349 |
| 316 # Whether the service is accessible from other hosts. | 350 # Whether the service is accessible from other hosts. |
| 317 'isExported': _SERVER.is_exported, | 351 'isExported': _SERVER.is_exported, |
| 318 } | 352 } |
| 319 self.send_json_dict(response_dict) | 353 return response_dict |
| 320 except: | |
| 321 self.send_error(404) | |
| 322 raise | |
| 323 | 354 |
| 324 def do_GET_static(self, path): | 355 def do_GET_static(self, path): |
| 325 """ Handle a GET request for a file under the 'static' directory. | 356 """ Handle a GET request for a file under the 'static' directory. |
| 326 Only allow serving of files within the 'static' directory that is a | 357 Only allow serving of files within the 'static' directory that is a |
| 327 filesystem sibling of this script. | 358 filesystem sibling of this script. |
| 328 | 359 |
| 329 Args: | 360 Args: |
| 330 path: path to file (under static directory) to retrieve | 361 path: path to file (under static directory) to retrieve |
| 331 """ | 362 """ |
| 332 # Strip arguments ('?resultsToLoad=all') from the path | 363 # Strip arguments ('?resultsToLoad=all') from the path |
| (...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 496 args = parser.parse_args() | 527 args = parser.parse_args() |
| 497 global _SERVER | 528 global _SERVER |
| 498 _SERVER = Server(actuals_dir=args.actuals_dir, | 529 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 499 port=args.port, export=args.export, editable=args.editable, | 530 port=args.port, export=args.export, editable=args.editable, |
| 500 reload_seconds=args.reload) | 531 reload_seconds=args.reload) |
| 501 _SERVER.run() | 532 _SERVER.run() |
| 502 | 533 |
| 503 | 534 |
| 504 if __name__ == '__main__': | 535 if __name__ == '__main__': |
| 505 main() | 536 main() |
| OLD | NEW |