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 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
61 | 61 |
62 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
63 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 63 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
64 DEFAULT_PORT = 8888 | 64 DEFAULT_PORT = 8888 |
65 | 65 |
66 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 66 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
67 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 67 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
68 | 68 |
69 _SERVER = None # This gets filled in by main() | 69 _SERVER = None # This gets filled in by main() |
70 | 70 |
71 def get_routable_ip_address(): | 71 def _get_routable_ip_address(): |
epoger
2013/11/20 19:51:34
made this function private (it's only used interna
| |
72 """Returns routable IP address of this host (the IP address of its network | 72 """Returns routable IP address of this host (the IP address of its network |
73 interface that would be used for most traffic, not its localhost | 73 interface that would be used for most traffic, not its localhost |
74 interface). See http://stackoverflow.com/a/166589 """ | 74 interface). See http://stackoverflow.com/a/166589 """ |
75 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | 75 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
76 sock.connect(('8.8.8.8', 80)) | 76 sock.connect(('8.8.8.8', 80)) |
77 host = sock.getsockname()[0] | 77 host = sock.getsockname()[0] |
78 sock.close() | 78 sock.close() |
79 return host | 79 return host |
80 | 80 |
81 def _create_svn_checkout(dir_path, repo_url): | |
82 """Creates local checkout of an SVN repository at the specified directory | |
83 path, returning an svn.Svn object referring to the local checkout. | |
84 | |
85 Args: | |
86 dir_path: path to the local checkout; if this directory does not yet exist, | |
87 it will be created and the repo will be checked out into it | |
88 repo_url: URL of SVN repo to check out into dir_path (unless the local | |
89 checkout already exists) | |
90 Returns: an svn.Svn object referring to the local checkout. | |
91 """ | |
92 local_checkout = svn.Svn(dir_path) | |
93 if not os.path.isdir(dir_path): | |
94 os.makedirs(dir_path) | |
95 local_checkout.Checkout(repo_url, '.') | |
96 return local_checkout | |
97 | |
81 | 98 |
82 class Server(object): | 99 class Server(object): |
83 """ HTTP server for our HTML rebaseline viewer. """ | 100 """ HTTP server for our HTML rebaseline viewer. """ |
84 | 101 |
85 def __init__(self, | 102 def __init__(self, |
86 actuals_dir=DEFAULT_ACTUALS_DIR, | 103 actuals_dir=DEFAULT_ACTUALS_DIR, |
87 expectations_dir=DEFAULT_EXPECTATIONS_DIR, | 104 expectations_dir=DEFAULT_EXPECTATIONS_DIR, |
88 port=DEFAULT_PORT, export=False, editable=True, | 105 port=DEFAULT_PORT, export=False, editable=True, |
89 reload_seconds=0): | 106 reload_seconds=0): |
90 """ | 107 """ |
91 Args: | 108 Args: |
92 actuals_dir: directory under which we will check out the latest actual | 109 actuals_dir: directory under which we will check out the latest actual |
93 GM results | 110 GM results |
94 expectations_dir: directory under which to find GM expectations (they | 111 expectations_dir: directory under which to find GM expectations (they |
95 must already be in that directory) | 112 must already be in that directory) |
96 port: which TCP port to listen on for HTTP requests | 113 port: which TCP port to listen on for HTTP requests |
97 export: whether to allow HTTP clients on other hosts to access this server | 114 export: whether to allow HTTP clients on other hosts to access this server |
98 editable: whether HTTP clients are allowed to submit new baselines | 115 editable: whether HTTP clients are allowed to submit new baselines |
99 reload_seconds: polling interval with which to check for new results; | 116 reload_seconds: polling interval with which to check for new results; |
100 if 0, don't check for new results at all | 117 if 0, don't check for new results at all |
101 """ | 118 """ |
102 self._actuals_dir = actuals_dir | 119 self._actuals_dir = actuals_dir |
103 self._expectations_dir = expectations_dir | 120 self._expectations_dir = expectations_dir |
104 self._port = port | 121 self._port = port |
105 self._export = export | 122 self._export = export |
106 self._editable = editable | 123 self._editable = editable |
107 self._reload_seconds = reload_seconds | 124 self._reload_seconds = reload_seconds |
125 self._actuals_repo = _create_svn_checkout( | |
126 dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO) | |
127 | |
128 # We only update the expectations dir if the server was run with a | |
129 # nonzero --reload argument; otherwise, we expect the user to maintain | |
130 # her own expectations as she sees fit. | |
131 # | |
132 # TODO(epoger): Use git instead of svn to check out expectations, since | |
133 # the Skia repo is moving to git. | |
134 if reload_seconds: | |
135 self._expectations_repo = _create_svn_checkout( | |
136 dir_path=expectations_dir, repo_url=EXPECTATIONS_SVN_REPO) | |
137 else: | |
138 self._expectations_repo = None | |
108 | 139 |
109 def is_exported(self): | 140 def is_exported(self): |
110 """ Returns true iff HTTP clients on other hosts are allowed to access | 141 """ Returns true iff HTTP clients on other hosts are allowed to access |
111 this server. """ | 142 this server. """ |
112 return self._export | 143 return self._export |
113 | 144 |
114 def is_editable(self): | 145 def is_editable(self): |
115 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 146 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
116 return self._editable | 147 return self._editable |
117 | 148 |
118 def reload_seconds(self): | 149 def reload_seconds(self): |
119 """ Returns the result reload period in seconds, or 0 if we don't reload | 150 """ Returns the result reload period in seconds, or 0 if we don't reload |
120 results. """ | 151 results. """ |
121 return self._reload_seconds | 152 return self._reload_seconds |
122 | 153 |
123 def update_results(self): | 154 def update_results(self): |
124 """ Create or update self.results, based on the expectations in | 155 """ Create or update self.results, based on the expectations in |
125 self._expectations_dir and the latest actuals from skia-autogen. | 156 self._expectations_dir and the latest actuals from skia-autogen. |
126 """ | 157 """ |
127 with self._svn_update_lock: | 158 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
128 # self._svn_update_lock prevents us from updating the actual GM results | 159 self._actuals_dir, ACTUALS_SVN_REPO)) |
129 # in multiple threads simultaneously | 160 self._actuals_repo.Update('.') |
130 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( | 161 |
131 self._actuals_dir, ACTUALS_SVN_REPO)) | 162 if self._expectations_repo: |
132 actuals_repo = svn.Svn(self._actuals_dir) | 163 logging.info( |
133 if not os.path.isdir(self._actuals_dir): | 164 'Updating expected GM results in %s from SVN repo %s ...' % ( |
134 os.makedirs(self._actuals_dir) | 165 self._expectations_dir, EXPECTATIONS_SVN_REPO)) |
135 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') | 166 self._expectations_repo.Update('.') |
136 else: | |
137 actuals_repo.Update('.') | |
138 # We only update the expectations dir if the server was run with a | |
139 # nonzero --reload argument; otherwise, we expect the user to maintain | |
140 # her own expectations as she sees fit. | |
141 # | |
142 # self._svn_update_lock prevents us from updating the expected GM results | |
143 # in multiple threads simultaneously | |
144 # | |
145 # TODO(epoger): Use git instead of svn to check out expectations, since | |
146 # the Skia repo is moving to git. | |
147 if self._reload_seconds: | |
148 logging.info( | |
149 'Updating expected GM results in %s from SVN repo %s ...' % ( | |
150 self._expectations_dir, EXPECTATIONS_SVN_REPO)) | |
151 expectations_repo = svn.Svn(self._expectations_dir) | |
152 if not os.path.isdir(self._expectations_dir): | |
153 os.makedirs(self._expectations_dir) | |
154 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') | |
155 else: | |
156 expectations_repo.Update('.') | |
157 # end of "with self._svn_update_lock:" | |
158 | 167 |
159 logging.info( | 168 logging.info( |
160 ('Parsing results from actuals in %s and expectations in %s, ' | 169 ('Parsing results from actuals in %s and expectations in %s, ' |
161 + 'and generating pixel diffs (may take a while) ...') % ( | 170 + 'and generating pixel diffs (may take a while) ...') % ( |
162 self._actuals_dir, self._expectations_dir)) | 171 self._actuals_dir, self._expectations_dir)) |
163 new_results = results.Results( | 172 self.results = results.Results( |
164 actuals_root=self._actuals_dir, | 173 actuals_root=self._actuals_dir, |
165 expected_root=self._expectations_dir, | 174 expected_root=self._expectations_dir, |
166 generated_images_root=GENERATED_IMAGES_ROOT) | 175 generated_images_root=GENERATED_IMAGES_ROOT) |
167 | 176 |
168 # Make sure we don't update self.results while a client is in the middle | |
169 # of reading from it. | |
170 with self.results_lock: | |
171 self.results = new_results | |
172 | |
173 def _result_reloader(self): | 177 def _result_reloader(self): |
174 """ If --reload argument was specified, reload results at the appropriate | 178 """ If --reload argument was specified, reload results at the appropriate |
175 interval. | 179 interval. |
176 """ | 180 """ |
177 while self._reload_seconds: | 181 while self._reload_seconds: |
178 time.sleep(self._reload_seconds) | 182 time.sleep(self._reload_seconds) |
179 self.update_results() | 183 self.update_results() |
180 | 184 |
181 def run(self): | 185 def run(self): |
182 self.results_lock = thread.allocate_lock() | |
183 self._svn_update_lock = thread.allocate_lock() | |
184 self.update_results() | 186 self.update_results() |
185 thread.start_new_thread(self._result_reloader, ()) | 187 thread.start_new_thread(self._result_reloader, ()) |
186 | 188 |
187 if self._export: | 189 if self._export: |
188 server_address = ('', self._port) | 190 server_address = ('', self._port) |
189 host = get_routable_ip_address() | 191 host = _get_routable_ip_address() |
190 if self._editable: | 192 if self._editable: |
191 logging.warning('Running with combination of "export" and "editable" ' | 193 logging.warning('Running with combination of "export" and "editable" ' |
192 'flags. Users on other machines will ' | 194 'flags. Users on other machines will ' |
193 'be able to modify your GM expectations!') | 195 'be able to modify your GM expectations!') |
194 else: | 196 else: |
195 host = '127.0.0.1' | 197 host = '127.0.0.1' |
196 server_address = (host, self._port) | 198 server_address = (host, self._port) |
197 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) | 199 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) |
198 logging.info('Ready for requests on http://%s:%d' % ( | 200 logging.info('Ready for requests on http://%s:%d' % ( |
199 host, http_server.server_port)) | 201 host, http_server.server_port)) |
(...skipping 29 matching lines...) Expand all Loading... | |
229 | 231 |
230 def do_GET_results(self, type): | 232 def do_GET_results(self, type): |
231 """ Handle a GET request for GM results. | 233 """ Handle a GET request for GM results. |
232 | 234 |
233 Args: | 235 Args: |
234 type: string indicating which set of results to return; | 236 type: string indicating which set of results to return; |
235 must be one of the results.RESULTS_* constants | 237 must be one of the results.RESULTS_* constants |
236 """ | 238 """ |
237 logging.debug('do_GET_results: sending results of type "%s"' % type) | 239 logging.debug('do_GET_results: sending results of type "%s"' % type) |
238 try: | 240 try: |
241 # Since we must make multiple calls to the Results object, grab a | |
242 # reference to it in case it is updated to point at a new Results | |
243 # object within another thread. | |
244 # | |
239 # TODO(epoger): Rather than using a global variable for the handler | 245 # TODO(epoger): Rather than using a global variable for the handler |
240 # to refer to the Server object, make Server a subclass of | 246 # to refer to the Server object, make Server a subclass of |
241 # HTTPServer, and then it could be available to the handler via | 247 # HTTPServer, and then it could be available to the handler via |
242 # the handler's .server instance variable. | 248 # the handler's .server instance variable. |
249 results_obj = _SERVER.results | |
250 response_dict = results_obj.get_results_of_type(type) | |
251 time_updated = results_obj.get_timestamp() | |
243 | 252 |
244 with _SERVER.results_lock: | |
245 response_dict = _SERVER.results.get_results_of_type(type) | |
246 time_updated = _SERVER.results.get_timestamp() | |
247 response_dict['header'] = { | 253 response_dict['header'] = { |
248 # Timestamps: | 254 # Timestamps: |
249 # 1. when this data was last updated | 255 # 1. when this data was last updated |
250 # 2. when the caller should check back for new data (if ever) | 256 # 2. when the caller should check back for new data (if ever) |
251 # | 257 # |
252 # We only return these timestamps if the --reload argument was passed; | 258 # We only return these timestamps if the --reload argument was passed; |
253 # otherwise, we have no idea when the expectations were last updated | 259 # otherwise, we have no idea when the expectations were last updated |
254 # (we allow the user to maintain her own expectations as she sees fit). | 260 # (we allow the user to maintain her own expectations as she sees fit). |
255 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, | 261 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, |
256 'timeNextUpdateAvailable': ( | 262 'timeNextUpdateAvailable': ( |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
346 if content_type != 'application/json;charset=UTF-8': | 352 if content_type != 'application/json;charset=UTF-8': |
347 raise Exception('unsupported %s [%s]' % ( | 353 raise Exception('unsupported %s [%s]' % ( |
348 _HTTP_HEADER_CONTENT_TYPE, content_type)) | 354 _HTTP_HEADER_CONTENT_TYPE, content_type)) |
349 | 355 |
350 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) | 356 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) |
351 json_data = self.rfile.read(content_length) | 357 json_data = self.rfile.read(content_length) |
352 data = json.loads(json_data) | 358 data = json.loads(json_data) |
353 logging.debug('do_POST_edits: received new GM expectations data [%s]' % | 359 logging.debug('do_POST_edits: received new GM expectations data [%s]' % |
354 data) | 360 data) |
355 | 361 |
356 with _SERVER.results_lock: | 362 # Since we must make multiple calls to the Results object, grab a |
357 oldResultsType = data['oldResultsType'] | 363 # reference to it in case it is updated to point at a new Results |
358 oldResults = _SERVER.results.get_results_of_type(oldResultsType) | 364 # object within another thread. |
359 oldResultsHash = str(hash(repr(oldResults['testData']))) | 365 results_obj = _SERVER.results |
360 if oldResultsHash != data['oldResultsHash']: | 366 oldResultsType = data['oldResultsType'] |
361 raise Exception('results of type "%s" changed while the client was ' | 367 oldResults = results_obj.get_results_of_type(oldResultsType) |
362 'making modifications. The client should reload the ' | 368 oldResultsHash = str(hash(repr(oldResults['testData']))) |
363 'results and submit the modifications again.' % | 369 if oldResultsHash != data['oldResultsHash']: |
364 oldResultsType) | 370 raise Exception('results of type "%s" changed while the client was ' |
365 _SERVER.results.edit_expectations(data['modifications']) | 371 'making modifications. The client should reload the ' |
372 'results and submit the modifications again.' % | |
373 oldResultsType) | |
374 results_obj.edit_expectations(data['modifications']) | |
366 | 375 |
367 # Now that the edits have been committed, update results to reflect them. | 376 # Now that the edits have been committed, update results to reflect them. |
368 _SERVER.update_results() | 377 _SERVER.update_results() |
369 | 378 |
370 def redirect_to(self, url): | 379 def redirect_to(self, url): |
371 """ Redirect the HTTP client to a different url. | 380 """ Redirect the HTTP client to a different url. |
372 | 381 |
373 Args: | 382 Args: |
374 url: URL to redirect the HTTP client to | 383 url: URL to redirect the HTTP client to |
375 """ | 384 """ |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
449 args = parser.parse_args() | 458 args = parser.parse_args() |
450 global _SERVER | 459 global _SERVER |
451 _SERVER = Server(actuals_dir=args.actuals_dir, | 460 _SERVER = Server(actuals_dir=args.actuals_dir, |
452 expectations_dir=args.expectations_dir, | 461 expectations_dir=args.expectations_dir, |
453 port=args.port, export=args.export, editable=args.editable, | 462 port=args.port, export=args.export, editable=args.editable, |
454 reload_seconds=args.reload) | 463 reload_seconds=args.reload) |
455 _SERVER.run() | 464 _SERVER.run() |
456 | 465 |
457 if __name__ == '__main__': | 466 if __name__ == '__main__': |
458 main() | 467 main() |
OLD | NEW |