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 |