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

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

Issue 208473004: rebaseline_server: fix --editable mode (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 6 years, 9 months 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
« no previous file with comments | « no previous file | gm/rebaseline_server/static/index.html » ('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 10 matching lines...) Expand all
21 import socket 21 import socket
22 import subprocess 22 import subprocess
23 import sys 23 import sys
24 import thread 24 import thread
25 import threading 25 import threading
26 import time 26 import time
27 import urlparse 27 import urlparse
28 28
29 # Imports from within Skia 29 # Imports from within Skia
30 # 30 #
31 # We need to add the 'tools' directory for svn.py, and the 'gm' directory for 31 # We need to add the 'tools' directory, so that we can import svn.py within
32 # gm_json.py . 32 # that directory.
33 # Make sure that these dirs are in the PYTHONPATH, but add them 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 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY))
37 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY)
38 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools')
39 if TOOLS_DIRECTORY not in sys.path: 38 if TOOLS_DIRECTORY not in sys.path:
40 sys.path.append(TOOLS_DIRECTORY) 39 sys.path.append(TOOLS_DIRECTORY)
41 import svn 40 import svn
42 if GM_DIRECTORY not in sys.path:
43 sys.path.append(GM_DIRECTORY)
44 import gm_json
45 41
46 # Imports from local dir 42 # Imports from local dir
47 # 43 #
48 # Note: we import results under a different name, to avoid confusion with the 44 # Note: we import results under a different name, to avoid confusion with the
49 # Server.results() property. See discussion at 45 # Server.results() property. See discussion at
50 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44 46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44
51 import imagepairset 47 import imagepairset
52 import results as results_mod 48 import results as results_mod
53 49
54 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 50 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
(...skipping 14 matching lines...) Expand all
69 KEY__EDITS__MODIFICATIONS = 'modifications' 65 KEY__EDITS__MODIFICATIONS = 'modifications'
70 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
71 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' 67 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType'
72 68
73 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR 69 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
74 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' 70 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD'
75 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'
76 DEFAULT_PORT = 8888 72 DEFAULT_PORT = 8888
77 73
78 # Directory within which the server will serve out static files. 74 # Directory within which the server will serve out static files.
79 STATIC_CONTENTS_DIR = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) 75 STATIC_CONTENTS_SUBDIR = 'static' # within PARENT_DIR
80 GENERATED_IMAGES_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-images') 76 GENERATED_IMAGES_SUBDIR = 'generated-images' # within STATIC_CONTENTS_SUBDIR
81 GENERATED_JSON_DIR = os.path.join(STATIC_CONTENTS_DIR, 'generated-json')
82 77
83 # How often (in seconds) clients should reload while waiting for initial 78 # How often (in seconds) clients should reload while waiting for initial
84 # results to load. 79 # results to load.
85 RELOAD_INTERVAL_UNTIL_READY = 10 80 RELOAD_INTERVAL_UNTIL_READY = 10
86 81
87 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' 82 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
88 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' 83 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
89 84
90 SUMMARY_TYPES = [
91 results_mod.KEY__HEADER__RESULTS_ALL,
92 results_mod.KEY__HEADER__RESULTS_FAILURES,
93 ]
94
95 _SERVER = None # This gets filled in by main() 85 _SERVER = None # This gets filled in by main()
96 86
97 87
98 def _run_command(args, directory): 88 def _run_command(args, directory):
99 """Runs a command and returns stdout as a single string. 89 """Runs a command and returns stdout as a single string.
100 90
101 Args: 91 Args:
102 args: the command to run, as a list of arguments 92 args: the command to run, as a list of arguments
103 directory: directory within which to run the command 93 directory: directory within which to run the command
104 94
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
170 self._actuals_dir = actuals_dir 160 self._actuals_dir = actuals_dir
171 self._actuals_repo_revision = actuals_repo_revision 161 self._actuals_repo_revision = actuals_repo_revision
172 self._actuals_repo_url = actuals_repo_url 162 self._actuals_repo_url = actuals_repo_url
173 self._port = port 163 self._port = port
174 self._export = export 164 self._export = export
175 self._editable = editable 165 self._editable = editable
176 self._reload_seconds = reload_seconds 166 self._reload_seconds = reload_seconds
177 self._actuals_repo = _create_svn_checkout( 167 self._actuals_repo = _create_svn_checkout(
178 dir_path=actuals_dir, repo_url=actuals_repo_url) 168 dir_path=actuals_dir, repo_url=actuals_repo_url)
179 169
180 # Since we don't have any results ready yet, prepare a dummy results file
181 # telling any clients that we're still working on the results.
182 response_dict = {
183 results_mod.KEY__HEADER: {
184 results_mod.KEY__HEADER__SCHEMA_VERSION: (
185 results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER),
186 results_mod.KEY__HEADER__IS_STILL_LOADING: True,
187 results_mod.KEY__HEADER__TIME_UPDATED: 0,
188 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
189 RELOAD_INTERVAL_UNTIL_READY),
190 },
191 }
192 if not os.path.isdir(GENERATED_JSON_DIR):
193 os.makedirs(GENERATED_JSON_DIR)
194 for summary_type in SUMMARY_TYPES:
195 gm_json.WriteToFile(
196 response_dict,
197 os.path.join(GENERATED_JSON_DIR, '%s.json' % summary_type))
198
199 # Reentrant lock that must be held whenever updating EITHER of: 170 # Reentrant lock that must be held whenever updating EITHER of:
200 # 1. self._results 171 # 1. self._results
201 # 2. the expected or actual results on local disk 172 # 2. the expected or actual results on local disk
202 self.results_rlock = threading.RLock() 173 self.results_rlock = threading.RLock()
203 # self._results will be filled in by calls to update_results() 174 # self._results will be filled in by calls to update_results()
204 self._results = None 175 self._results = None
205 176
206 @property 177 @property
207 def results(self): 178 def results(self):
208 """ Returns the most recently generated results, or None if we don't have 179 """ Returns the most recently generated results, or None if we don't have
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
258 # 229 #
259 # Because Skia uses depot_tools, we have to update using "gclient sync" 230 # Because Skia uses depot_tools, we have to update using "gclient sync"
260 # instead of raw git (or SVN) update. Happily, this will work whether 231 # instead of raw git (or SVN) update. Happily, this will work whether
261 # the checkout was created using git or SVN. 232 # the checkout was created using git or SVN.
262 if self._reload_seconds: 233 if self._reload_seconds:
263 logging.info( 234 logging.info(
264 'Updating expected GM results in %s by syncing Skia repo ...' % 235 'Updating expected GM results in %s by syncing Skia repo ...' %
265 results_mod.DEFAULT_EXPECTATIONS_DIR) 236 results_mod.DEFAULT_EXPECTATIONS_DIR)
266 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) 237 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
267 238
268 new_results = results_mod.Results( 239 self._results = results_mod.Results(
269 actuals_root=self._actuals_dir, 240 actuals_root=self._actuals_dir,
270 generated_images_root=GENERATED_IMAGES_DIR, 241 generated_images_root=os.path.join(
271 diff_base_url=os.path.relpath( 242 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR,
272 GENERATED_IMAGES_DIR, GENERATED_JSON_DIR)) 243 GENERATED_IMAGES_SUBDIR),
273 244 diff_base_url=posixpath.join(
274 if not os.path.isdir(GENERATED_JSON_DIR): 245 os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR))
275 os.makedirs(GENERATED_JSON_DIR)
276 for summary_type in SUMMARY_TYPES:
277 gm_json.WriteToFile(
278 new_results.get_packaged_results_of_type(results_type=summary_type),
279 os.path.join(GENERATED_JSON_DIR, '%s.json' % summary_type))
280
281 self._results = new_results
282 246
283 def _result_loader(self, reload_seconds=0): 247 def _result_loader(self, reload_seconds=0):
284 """ Call self.update_results(), either once or periodically. 248 """ Call self.update_results(), either once or periodically.
285 249
286 Params: 250 Params:
287 reload_seconds: integer; if nonzero, reload results at this interval 251 reload_seconds: integer; if nonzero, reload results at this interval
288 (in which case, this method will never return!) 252 (in which case, this method will never return!)
289 """ 253 """
290 self.update_results() 254 self.update_results()
291 logging.info('Initial results loaded. Ready for requests on %s' % self._url) 255 logging.info('Initial results loaded. Ready for requests on %s' % self._url)
(...skipping 30 matching lines...) Expand all
322 def do_GET(self): 286 def do_GET(self):
323 """ 287 """
324 Handles all GET requests, forwarding them to the appropriate 288 Handles all GET requests, forwarding them to the appropriate
325 do_GET_* dispatcher. 289 do_GET_* dispatcher.
326 290
327 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 291 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147
328 """ 292 """
329 try: 293 try:
330 logging.debug('do_GET: path="%s"' % self.path) 294 logging.debug('do_GET: path="%s"' % self.path)
331 if self.path == '' or self.path == '/' or self.path == '/index.html' : 295 if self.path == '' or self.path == '/' or self.path == '/index.html' :
332 self.redirect_to('/static/index.html') 296 self.redirect_to('/%s/index.html' % STATIC_CONTENTS_SUBDIR)
333 return 297 return
334 if self.path == '/favicon.ico' : 298 if self.path == '/favicon.ico' :
335 self.redirect_to('/static/favicon.ico') 299 self.redirect_to('/%s/favicon.ico' % STATIC_CONTENTS_SUBDIR)
336 return 300 return
337 301
338 # All requests must be of this form: 302 # All requests must be of this form:
339 # /dispatcher/remainder 303 # /dispatcher/remainder
340 # where 'dispatcher' indicates which do_GET_* dispatcher to run 304 # where 'dispatcher' indicates which do_GET_* dispatcher to run
341 # and 'remainder' is the remaining path sent to that dispatcher. 305 # and 'remainder' is the remaining path sent to that dispatcher.
342 normpath = posixpath.normpath(self.path) 306 normpath = posixpath.normpath(self.path)
343 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() 307 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups()
344 dispatchers = { 308 dispatchers = {
345 'static': self.do_GET_static, 309 'results': self.do_GET_results,
310 STATIC_CONTENTS_SUBDIR: self.do_GET_static,
346 } 311 }
347 dispatcher = dispatchers[dispatcher_name] 312 dispatcher = dispatchers[dispatcher_name]
348 dispatcher(remainder) 313 dispatcher(remainder)
349 except: 314 except:
350 self.send_error(404) 315 self.send_error(404)
351 raise 316 raise
352 317
318 def do_GET_results(self, results_type):
319 """ Handle a GET request for GM results.
320
321 Args:
322 results_type: string indicating which set of results to return;
323 must be one of the results_mod.RESULTS_* constants
324 """
325 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
327 # reference to it in case it is updated to point at a new Results
328 # object within another thread.
329 #
330 # TODO(epoger): Rather than using a global variable for the handler
331 # to refer to the Server object, make Server a subclass of
332 # HTTPServer, and then it could be available to the handler via
333 # the handler's .server instance variable.
334 results_obj = _SERVER.results
335 if results_obj:
336 response_dict = results_obj.get_packaged_results_of_type(
337 results_type=results_type, reload_seconds=_SERVER.reload_seconds,
338 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported)
339 else:
340 now = int(time.time())
341 response_dict = {
342 results_mod.KEY__HEADER: {
343 results_mod.KEY__HEADER__SCHEMA_VERSION: (
344 results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER),
345 results_mod.KEY__HEADER__IS_STILL_LOADING: True,
346 results_mod.KEY__HEADER__TIME_UPDATED: now,
347 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
348 now + RELOAD_INTERVAL_UNTIL_READY),
349 },
350 }
351 self.send_json_dict(response_dict)
352
353 def do_GET_static(self, path): 353 def do_GET_static(self, path):
354 """ Handle a GET request for a file under the 'static' directory. 354 """ Handle a GET request for a file under STATIC_CONTENTS_SUBDIR .
355 Only allow serving of files within the 'static' directory that is a 355 Only allow serving of files within STATIC_CONTENTS_SUBDIR that is a
356 filesystem sibling of this script. 356 filesystem sibling of this script.
357 357
358 Args: 358 Args:
359 path: path to file (under static directory) to retrieve 359 path: path to file (within STATIC_CONTENTS_SUBDIR) to retrieve
360 """ 360 """
361 # Strip arguments ('?resultsToLoad=all') from the path 361 # Strip arguments ('?resultsToLoad=all') from the path
362 path = urlparse.urlparse(path).path 362 path = urlparse.urlparse(path).path
363 363
364 logging.debug('do_GET_static: sending file "%s"' % path) 364 logging.debug('do_GET_static: sending file "%s"' % path)
365 full_path = os.path.realpath(os.path.join(STATIC_CONTENTS_DIR, path)) 365 static_dir = os.path.realpath(os.path.join(
366 if full_path.startswith(STATIC_CONTENTS_DIR): 366 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR))
367 full_path = os.path.realpath(os.path.join(static_dir, path))
368 if full_path.startswith(static_dir):
367 self.send_file(full_path) 369 self.send_file(full_path)
368 else: 370 else:
369 logging.error( 371 logging.error(
370 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' 372 'Attempted do_GET_static() of path [%s] outside of static dir [%s]'
371 % (full_path, STATIC_CONTENTS_DIR)) 373 % (full_path, static_dir))
372 self.send_error(404) 374 self.send_error(404)
373 375
374 def do_POST(self): 376 def do_POST(self):
375 """ Handles all POST requests, forwarding them to the appropriate 377 """ Handles all POST requests, forwarding them to the appropriate
376 do_POST_* dispatcher. """ 378 do_POST_* dispatcher. """
377 # All requests must be of this form: 379 # All requests must be of this form:
378 # /dispatcher 380 # /dispatcher
379 # where 'dispatcher' indicates which do_POST_* dispatcher to run. 381 # where 'dispatcher' indicates which do_POST_* dispatcher to run.
380 logging.debug('do_POST: path="%s"' % self.path) 382 logging.debug('do_POST: path="%s"' % self.path)
381 normpath = posixpath.normpath(self.path) 383 normpath = posixpath.normpath(self.path)
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after
473 # Open the file and send it over HTTP 475 # Open the file and send it over HTTP
474 if os.path.isfile(path): 476 if os.path.isfile(path):
475 with open(path, 'rb') as sending_file: 477 with open(path, 'rb') as sending_file:
476 self.send_response(200) 478 self.send_response(200)
477 self.send_header('Content-type', mime_type) 479 self.send_header('Content-type', mime_type)
478 self.end_headers() 480 self.end_headers()
479 self.wfile.write(sending_file.read()) 481 self.wfile.write(sending_file.read())
480 else: 482 else:
481 self.send_error(404) 483 self.send_error(404)
482 484
485 def send_json_dict(self, json_dict):
486 """ Send the contents of this dictionary in JSON format, with a JSON
487 mimetype.
488
489 Args:
490 json_dict: dictionary to send
491 """
492 self.send_response(200)
493 self.send_header('Content-type', 'application/json')
494 self.end_headers()
495 json.dump(json_dict, self.wfile)
496
483 497
484 def main(): 498 def main():
485 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', 499 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
486 datefmt='%m/%d/%Y %H:%M:%S', 500 datefmt='%m/%d/%Y %H:%M:%S',
487 level=logging.INFO) 501 level=logging.INFO)
488 parser = argparse.ArgumentParser() 502 parser = argparse.ArgumentParser()
489 parser.add_argument('--actuals-dir', 503 parser.add_argument('--actuals-dir',
490 help=('Directory into which we will check out the latest ' 504 help=('Directory into which we will check out the latest '
491 'actual GM results. If this directory does not ' 505 'actual GM results. If this directory does not '
492 'exist, it will be created. Defaults to %(default)s'), 506 'exist, it will be created. Defaults to %(default)s'),
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
526 _SERVER = Server(actuals_dir=args.actuals_dir, 540 _SERVER = Server(actuals_dir=args.actuals_dir,
527 actuals_repo_revision=args.actuals_revision, 541 actuals_repo_revision=args.actuals_revision,
528 actuals_repo_url=args.actuals_repo, 542 actuals_repo_url=args.actuals_repo,
529 port=args.port, export=args.export, editable=args.editable, 543 port=args.port, export=args.export, editable=args.editable,
530 reload_seconds=args.reload) 544 reload_seconds=args.reload)
531 _SERVER.run() 545 _SERVER.run()
532 546
533 547
534 if __name__ == '__main__': 548 if __name__ == '__main__':
535 main() 549 main()
OLDNEW
« no previous file with comments | « no previous file | gm/rebaseline_server/static/index.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698