Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(66)

Side by Side Diff: gm/rebaseline_server/server.py

Issue 64273011: rebaseline_server: clean up thread locks (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: extract_some_common_code Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tools/svn.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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()
OLDNEW
« no previous file with comments | « no previous file | tools/svn.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698