server.py revision c0df2fb5d0421a649d1dff9133874e440300fa7c
1f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com#!/usr/bin/python 2f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 39fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com""" 4f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comCopyright 2013 Google Inc. 5f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 6f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comUse of this source code is governed by a BSD-style license that can be 7f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comfound in the LICENSE file. 8f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 9f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comHTTP server for our HTML rebaseline viewer. 109fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com""" 11f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 12f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# System-level imports 13f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport argparse 14f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport BaseHTTPServer 15f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport json 16dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.comimport logging 17f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport os 18f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport posixpath 19f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport re 20f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport shutil 21b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.comimport socket 22d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comimport subprocess 23f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport sys 24542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.comimport thread 25d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comimport threading 26542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.comimport time 27dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.comimport urlparse 28f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 29f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# Imports from within Skia 30f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# 31a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org# We need to add the 'tools' directory, so that we can import svn.py within 32a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org# that directory. 33a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org# Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* 34f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# so any dirs that are already in the PYTHONPATH will be preferred. 35cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.comPARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 36a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.orgTRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) 37f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comTOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') 38f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comif TOOLS_DIRECTORY not in sys.path: 39f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com sys.path.append(TOOLS_DIRECTORY) 40f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport svn 41f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 42f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# Imports from local dir 437498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# 447498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# Note: we import results under a different name, to avoid confusion with the 457498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# Server.results() property. See discussion at 467498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.py#newcode44 47b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.orgimport compare_to_expectations 4816f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgimport imagepairset 497498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.orgimport results as results_mod 50f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 51f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comPATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 52f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 53f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# A simple dictionary of file name extensions to MIME types. The empty string 54f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# entry is used as the default when no extension was given or if the extension 55f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# has no entry in this dictionary. 56f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comMIME_TYPE_MAP = {'': 'application/octet-stream', 57f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'html': 'text/html', 58f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'css': 'text/css', 59f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'png': 'image/png', 60f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'js': 'application/javascript', 61f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'json': 'application/json' 62f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com } 63f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 6416f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org# Keys that server.py uses to create the toplevel content header. 6516f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org# NOTE: Keep these in sync with static/constants.js 6616f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__MODIFICATIONS = 'modifications' 6716f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' 6816f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' 697498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org 70b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.orgDEFAULT_ACTUALS_DIR = compare_to_expectations.DEFAULT_ACTUALS_DIR 715865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.orgDEFAULT_ACTUALS_REPO_REVISION = 'HEAD' 725865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.orgDEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' 73f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comDEFAULT_PORT = 8888 74f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 75579942387bed9259b03419c593503022e1399931commit-bot@chromium.org# Directory within which the server will serve out static files. 76a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.orgSTATIC_CONTENTS_SUBDIR = 'static' # within PARENT_DIR 77a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.orgGENERATED_IMAGES_SUBDIR = 'generated-images' # within STATIC_CONTENTS_SUBDIR 78579942387bed9259b03419c593503022e1399931commit-bot@chromium.org 792682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com# How often (in seconds) clients should reload while waiting for initial 802682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com# results to load. 812682c90860655f6c25c61f97c8d8db309d03087aepoger@google.comRELOAD_INTERVAL_UNTIL_READY = 10 822682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com 83eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com_HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' 84eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com_HTTP_HEADER_CONTENT_TYPE = 'Content-Type' 85eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 86f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com_SERVER = None # This gets filled in by main() 87f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 88d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 89d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comdef _run_command(args, directory): 90d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com """Runs a command and returns stdout as a single string. 91d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 92d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com Args: 93d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com args: the command to run, as a list of arguments 94d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com directory: directory within which to run the command 95d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 96d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com Returns: stdout, as a string 97d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 98d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com Raises an Exception if the command failed (exited with nonzero return code). 99d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com """ 100d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com logging.debug('_run_command: %s in directory %s' % (args, directory)) 101d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com proc = subprocess.Popen(args, cwd=directory, 102d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com stdout=subprocess.PIPE, 103d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com stderr=subprocess.PIPE) 104d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com (stdout, stderr) = proc.communicate() 105d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com if proc.returncode is not 0: 106d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com raise Exception('command "%s" failed in dir "%s": %s' % 107d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com (args, directory, stderr)) 108d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com return stdout 109d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 110d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 111591469b1e93f72172cef13a2f0675699994d7848epoger@google.comdef _get_routable_ip_address(): 112b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com """Returns routable IP address of this host (the IP address of its network 113b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com interface that would be used for most traffic, not its localhost 114b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com interface). See http://stackoverflow.com/a/166589 """ 115b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 116b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com sock.connect(('8.8.8.8', 80)) 117b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com host = sock.getsockname()[0] 118b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com sock.close() 119b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com return host 120b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com 121d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 122591469b1e93f72172cef13a2f0675699994d7848epoger@google.comdef _create_svn_checkout(dir_path, repo_url): 123591469b1e93f72172cef13a2f0675699994d7848epoger@google.com """Creates local checkout of an SVN repository at the specified directory 124591469b1e93f72172cef13a2f0675699994d7848epoger@google.com path, returning an svn.Svn object referring to the local checkout. 125591469b1e93f72172cef13a2f0675699994d7848epoger@google.com 126591469b1e93f72172cef13a2f0675699994d7848epoger@google.com Args: 127591469b1e93f72172cef13a2f0675699994d7848epoger@google.com dir_path: path to the local checkout; if this directory does not yet exist, 128591469b1e93f72172cef13a2f0675699994d7848epoger@google.com it will be created and the repo will be checked out into it 129591469b1e93f72172cef13a2f0675699994d7848epoger@google.com repo_url: URL of SVN repo to check out into dir_path (unless the local 130591469b1e93f72172cef13a2f0675699994d7848epoger@google.com checkout already exists) 131591469b1e93f72172cef13a2f0675699994d7848epoger@google.com Returns: an svn.Svn object referring to the local checkout. 132591469b1e93f72172cef13a2f0675699994d7848epoger@google.com """ 133591469b1e93f72172cef13a2f0675699994d7848epoger@google.com local_checkout = svn.Svn(dir_path) 134591469b1e93f72172cef13a2f0675699994d7848epoger@google.com if not os.path.isdir(dir_path): 135591469b1e93f72172cef13a2f0675699994d7848epoger@google.com os.makedirs(dir_path) 136591469b1e93f72172cef13a2f0675699994d7848epoger@google.com local_checkout.Checkout(repo_url, '.') 137591469b1e93f72172cef13a2f0675699994d7848epoger@google.com return local_checkout 138591469b1e93f72172cef13a2f0675699994d7848epoger@google.com 139b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com 140f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comclass Server(object): 1419fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ HTTP server for our HTML rebaseline viewer. """ 1429fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com 143f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def __init__(self, 144f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com actuals_dir=DEFAULT_ACTUALS_DIR, 1455865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org actuals_repo_revision=DEFAULT_ACTUALS_REPO_REVISION, 1465865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org actuals_repo_url=DEFAULT_ACTUALS_REPO_URL, 147542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com port=DEFAULT_PORT, export=False, editable=True, 148542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com reload_seconds=0): 1499fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ 1509fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com Args: 1519fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com actuals_dir: directory under which we will check out the latest actual 152c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org GM results 1535865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org actuals_repo_revision: revision of actual-results.json files to process 154c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org actuals_repo_url: SVN repo to download actual-results.json files from; 155c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org if None or '', don't fetch new actual-results files at all, 156c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org just compare to whatever files are already in actuals_dir 1579fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com port: which TCP port to listen on for HTTP requests 1589fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com export: whether to allow HTTP clients on other hosts to access this server 159542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com editable: whether HTTP clients are allowed to submit new baselines 160542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com reload_seconds: polling interval with which to check for new results; 161c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org if 0, don't check for new results at all 1629fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ 163f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self._actuals_dir = actuals_dir 1645865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org self._actuals_repo_revision = actuals_repo_revision 1655865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org self._actuals_repo_url = actuals_repo_url 166f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self._port = port 167f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self._export = export 168542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com self._editable = editable 169542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com self._reload_seconds = reload_seconds 170c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org if actuals_repo_url: 171c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org self._actuals_repo = _create_svn_checkout( 172c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org dir_path=actuals_dir, repo_url=actuals_repo_url) 173591469b1e93f72172cef13a2f0675699994d7848epoger@google.com 174d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # Reentrant lock that must be held whenever updating EITHER of: 175d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # 1. self._results 176d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # 2. the expected or actual results on local disk 177d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com self.results_rlock = threading.RLock() 178d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # self._results will be filled in by calls to update_results() 179d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com self._results = None 180d7255b6ae43f1931d5ec086fe4e92a664e28ab6fepoger@google.com 181d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com @property 182d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com def results(self): 18350ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org """ Returns the most recently generated results, or None if we don't have 18450ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org any valid results (update_results() has not completed yet). """ 185d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com return self._results 186d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 187d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com @property 1889fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com def is_exported(self): 1899fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ Returns true iff HTTP clients on other hosts are allowed to access 1909fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com this server. """ 1919fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com return self._export 1929fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com 193d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com @property 194542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com def is_editable(self): 195542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com """ Returns true iff HTTP clients are allowed to submit new baselines. """ 196542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com return self._editable 197542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 198d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com @property 199542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com def reload_seconds(self): 200542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com """ Returns the result reload period in seconds, or 0 if we don't reload 201542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com results. """ 202542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com return self._reload_seconds 203f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 20450ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org def update_results(self, invalidate=False): 2057498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org """ Create or update self._results, based on the latest expectations and 2067498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org actuals. 207d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 208d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com We hold self.results_rlock while we do this, to guarantee that no other 209d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com thread attempts to update either self._results or the underlying files at 210d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com the same time. 21150ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org 21250ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org Args: 21350ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org invalidate: if True, invalidate self._results immediately upon entry; 21450ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org otherwise, we will let readers see those results until we 21550ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org replace them 216f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com """ 217d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com with self.results_rlock: 21850ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org if invalidate: 21950ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org self._results = None 220c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org if self._actuals_repo_url: 221c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org logging.info( 222c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org 'Updating actual GM results in %s to revision %s from repo %s ...' 223c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org % ( 224c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org self._actuals_dir, self._actuals_repo_revision, 225c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org self._actuals_repo_url)) 226c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org self._actuals_repo.Update( 227c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org path='.', revision=self._actuals_repo_revision) 228d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 229d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # We only update the expectations dir if the server was run with a 230d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # nonzero --reload argument; otherwise, we expect the user to maintain 231d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # her own expectations as she sees fit. 232d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # 233d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # Because the Skia repo is moving from SVN to git, and git does not 234d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # support updating a single directory tree, we have to update the entire 235d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # repo checkout. 236d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # 237d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # Because Skia uses depot_tools, we have to update using "gclient sync" 238d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # instead of raw git (or SVN) update. Happily, this will work whether 239d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # the checkout was created using git or SVN. 240d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com if self._reload_seconds: 241d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com logging.info( 242d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 'Updating expected GM results in %s by syncing Skia repo ...' % 243b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.org compare_to_expectations.DEFAULT_EXPECTATIONS_DIR) 244d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) 245591469b1e93f72172cef13a2f0675699994d7848epoger@google.com 246b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.org self._results = compare_to_expectations.Results( 247579942387bed9259b03419c593503022e1399931commit-bot@chromium.org actuals_root=self._actuals_dir, 248a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org generated_images_root=os.path.join( 249a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, 250a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org GENERATED_IMAGES_SUBDIR), 251a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org diff_base_url=posixpath.join( 252a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR)) 253f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 2542682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com def _result_loader(self, reload_seconds=0): 2552682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com """ Call self.update_results(), either once or periodically. 2562682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com 2572682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com Params: 2582682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com reload_seconds: integer; if nonzero, reload results at this interval 2592682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com (in which case, this method will never return!) 260542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com """ 2612682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com self.update_results() 2622682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com logging.info('Initial results loaded. Ready for requests on %s' % self._url) 2632682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com if reload_seconds: 2642682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com while True: 2652682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com time.sleep(reload_seconds) 2662682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com self.update_results() 267542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 268f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def run(self): 2692682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com arg_tuple = (self._reload_seconds,) # start_new_thread needs a tuple, 2702682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com # even though it holds just one param 2712682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com thread.start_new_thread(self._result_loader, arg_tuple) 272542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 273f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com if self._export: 274f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com server_address = ('', self._port) 275591469b1e93f72172cef13a2f0675699994d7848epoger@google.com host = _get_routable_ip_address() 276542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com if self._editable: 277542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com logging.warning('Running with combination of "export" and "editable" ' 278542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 'flags. Users on other machines will ' 279542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 'be able to modify your GM expectations!') 280f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com else: 281b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com host = '127.0.0.1' 282b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com server_address = (host, self._port) 283f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) 2842682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com self._url = 'http://%s:%d' % (host, http_server.server_port) 2852682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com logging.info('Listening for requests on %s' % self._url) 286f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com http_server.serve_forever() 287f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 288f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 289f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comclass HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 290f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com """ HTTP request handlers for various types of queries this server knows 291f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com how to handle (static HTML and Javascript, expected/actual results, etc.) 292f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com """ 293f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def do_GET(self): 294e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org """ 295e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org Handles all GET requests, forwarding them to the appropriate 296e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org do_GET_* dispatcher. 297f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 298e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 299e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org """ 300e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org try: 301e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org logging.debug('do_GET: path="%s"' % self.path) 302e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org if self.path == '' or self.path == '/' or self.path == '/index.html' : 303a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.redirect_to('/%s/index.html' % STATIC_CONTENTS_SUBDIR) 304e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org return 305e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org if self.path == '/favicon.ico' : 306a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.redirect_to('/%s/favicon.ico' % STATIC_CONTENTS_SUBDIR) 307e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org return 308e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org 309e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org # All requests must be of this form: 310e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org # /dispatcher/remainder 311e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org # where 'dispatcher' indicates which do_GET_* dispatcher to run 312e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org # and 'remainder' is the remaining path sent to that dispatcher. 313e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org normpath = posixpath.normpath(self.path) 314e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() 315e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org dispatchers = { 316a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org 'results': self.do_GET_results, 317a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org STATIC_CONTENTS_SUBDIR: self.do_GET_static, 318e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org } 319e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org dispatcher = dispatchers[dispatcher_name] 320e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org dispatcher(remainder) 321e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org except: 322e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org self.send_error(404) 323e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org raise 324f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 325a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org def do_GET_results(self, results_type): 326a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org """ Handle a GET request for GM results. 327a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org 328a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org Args: 329a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_type: string indicating which set of results to return; 330a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org must be one of the results_mod.RESULTS_* constants 331a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org """ 332a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org logging.debug('do_GET_results: sending results of type "%s"' % results_type) 333a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # Since we must make multiple calls to the Results object, grab a 334a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # reference to it in case it is updated to point at a new Results 335a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # object within another thread. 336a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # 337a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # TODO(epoger): Rather than using a global variable for the handler 338a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # to refer to the Server object, make Server a subclass of 339a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # HTTPServer, and then it could be available to the handler via 340a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org # the handler's .server instance variable. 341a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_obj = _SERVER.results 342a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org if results_obj: 343a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org response_dict = results_obj.get_packaged_results_of_type( 344a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_type=results_type, reload_seconds=_SERVER.reload_seconds, 345a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) 346a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org else: 347a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org now = int(time.time()) 348a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org response_dict = { 349a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.KEY__HEADER: { 350a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.KEY__HEADER__SCHEMA_VERSION: ( 351a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.REBASELINE_SERVER_SCHEMA_VERSION_NUMBER), 352a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.KEY__HEADER__IS_STILL_LOADING: True, 353a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.KEY__HEADER__TIME_UPDATED: now, 354a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( 355a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org now + RELOAD_INTERVAL_UNTIL_READY), 356a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org }, 357a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org } 358a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.send_json_dict(response_dict) 359a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org 360f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def do_GET_static(self, path): 361a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org """ Handle a GET request for a file under STATIC_CONTENTS_SUBDIR . 362a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org Only allow serving of files within STATIC_CONTENTS_SUBDIR that is a 3639fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com filesystem sibling of this script. 3649fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com 3659fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com Args: 366a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org path: path to file (within STATIC_CONTENTS_SUBDIR) to retrieve 3679fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ 368dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com # Strip arguments ('?resultsToLoad=all') from the path 369dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com path = urlparse.urlparse(path).path 370dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com 371dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com logging.debug('do_GET_static: sending file "%s"' % path) 372a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org static_dir = os.path.realpath(os.path.join( 373a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR)) 374a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org full_path = os.path.realpath(os.path.join(static_dir, path)) 375a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org if full_path.startswith(static_dir): 376cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com self.send_file(full_path) 377cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com else: 378dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com logging.error( 379dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' 380a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org % (full_path, static_dir)) 381cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com self.send_error(404) 382f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 383eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com def do_POST(self): 384eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com """ Handles all POST requests, forwarding them to the appropriate 385eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com do_POST_* dispatcher. """ 386eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com # All requests must be of this form: 387eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com # /dispatcher 388eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com # where 'dispatcher' indicates which do_POST_* dispatcher to run. 389e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org logging.debug('do_POST: path="%s"' % self.path) 390eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com normpath = posixpath.normpath(self.path) 391eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com dispatchers = { 392eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com '/edits': self.do_POST_edits, 393eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com } 394eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com try: 395eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com dispatcher = dispatchers[normpath] 396eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com dispatcher() 397eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com self.send_response(200) 398eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com except: 399eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com self.send_error(404) 400eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com raise 401eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 402eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com def do_POST_edits(self): 403eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com """ Handle a POST request with modifications to GM expectations, in this 404eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com format: 405eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 406eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com { 40716f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client 40816f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org # loaded and then made 40916f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org # modifications to 41016f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client 41116f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org # loaded them (ensures that the 41216f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org # client and server apply 41316f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org # modifications to the same base) 41416f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org KEY__EDITS__MODIFICATIONS: [ 415b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.org # as needed by compare_to_expectations.edit_expectations() 416eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com ... 417eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com ], 418eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com } 419eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 420eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com Raises an Exception if there were any problems. 421eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com """ 422d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com if not _SERVER.is_editable: 423eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com raise Exception('this server is not running in --editable mode') 424eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 425eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] 426eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com if content_type != 'application/json;charset=UTF-8': 427eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com raise Exception('unsupported %s [%s]' % ( 428eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com _HTTP_HEADER_CONTENT_TYPE, content_type)) 429eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 430eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) 431eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com json_data = self.rfile.read(content_length) 432eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com data = json.loads(json_data) 433eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com logging.debug('do_POST_edits: received new GM expectations data [%s]' % 434eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com data) 435eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 436d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # Update the results on disk with the information we received from the 437d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # client. 438d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # We must hold _SERVER.results_rlock while we do this, to guarantee that 439d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # no other thread updates expectations (from the Skia repo) while we are 440d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com # updating them (using the info we received from the client). 441d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com with _SERVER.results_rlock: 44216f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org oldResultsType = data[KEY__EDITS__OLD_RESULTS_TYPE] 443d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com oldResults = _SERVER.results.get_results_of_type(oldResultsType) 44416f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org oldResultsHash = str(hash(repr(oldResults[imagepairset.KEY__IMAGEPAIRS]))) 44516f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org if oldResultsHash != data[KEY__EDITS__OLD_RESULTS_HASH]: 446d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com raise Exception('results of type "%s" changed while the client was ' 447d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 'making modifications. The client should reload the ' 448d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 'results and submit the modifications again.' % 449d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com oldResultsType) 45016f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org _SERVER.results.edit_expectations(data[KEY__EDITS__MODIFICATIONS]) 45150ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org 45250ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org # Read the updated results back from disk. 45350ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org # We can do this in a separate thread; we should return our success message 45450ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org # to the UI as soon as possible. 45550ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org thread.start_new_thread(_SERVER.update_results, (True,)) 456eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com 457f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def redirect_to(self, url): 4589fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ Redirect the HTTP client to a different url. 4599fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com 4609fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com Args: 4619fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com url: URL to redirect the HTTP client to 4629fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ 463f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.send_response(301) 464f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.send_header('Location', url) 465f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.end_headers() 466f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 467f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com def send_file(self, path): 468f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com """ Send the contents of the file at this path, with a mimetype based 4699fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com on the filename extension. 4709fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com 4719fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com Args: 4729fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com path: path of file whose contents to send to the HTTP client 4739fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com """ 474f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com # Grab the extension if there is one 475f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com extension = os.path.splitext(path)[1] 476f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com if len(extension) >= 1: 477f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com extension = extension[1:] 478f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 479f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com # Determine the MIME type of the file from its extension 480f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) 481f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 482f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com # Open the file and send it over HTTP 483f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com if os.path.isfile(path): 484f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com with open(path, 'rb') as sending_file: 485f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.send_response(200) 486f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.send_header('Content-type', mime_type) 487f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.end_headers() 488f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.wfile.write(sending_file.read()) 489f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com else: 490f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com self.send_error(404) 491f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 492a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org def send_json_dict(self, json_dict): 493a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org """ Send the contents of this dictionary in JSON format, with a JSON 494a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org mimetype. 495a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org 496a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org Args: 497a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org json_dict: dictionary to send 498a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org """ 499a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.send_response(200) 500a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.send_header('Content-type', 'application/json') 501a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org self.end_headers() 502a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org json.dump(json_dict, self.wfile) 503a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org 504f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 505f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comdef main(): 506a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', 507a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org datefmt='%m/%d/%Y %H:%M:%S', 508a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org level=logging.INFO) 509f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com parser = argparse.ArgumentParser() 510f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com parser.add_argument('--actuals-dir', 511f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com help=('Directory into which we will check out the latest ' 512f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'actual GM results. If this directory does not ' 513f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'exist, it will be created. Defaults to %(default)s'), 514f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com default=DEFAULT_ACTUALS_DIR) 5155865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org parser.add_argument('--actuals-repo', 5165865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org help=('URL of SVN repo to download actual-results.json ' 517c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org 'files from. Defaults to %(default)s ; if set to ' 518c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org 'empty string, just compare to actual-results ' 519c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org 'already found in ACTUALS_DIR.'), 5205865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org default=DEFAULT_ACTUALS_REPO_URL) 5215865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org parser.add_argument('--actuals-revision', 5225865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org help=('revision of actual-results.json files to process. ' 5235865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org 'Defaults to %(default)s . Beware of setting this ' 5245865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org 'argument in conjunction with --editable; you ' 5255865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org 'probably only want to edit results at HEAD.'), 5265865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org default=DEFAULT_ACTUALS_REPO_REVISION) 527542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com parser.add_argument('--editable', action='store_true', 528eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com help=('Allow HTTP clients to submit new baselines.')) 529f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com parser.add_argument('--export', action='store_true', 530f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com help=('Instead of only allowing access from HTTP clients ' 531f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'on localhost, allow HTTP clients on other hosts ' 532f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'to access this server. WARNING: doing so will ' 533f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 'allow users on other hosts to modify your ' 534542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 'GM expectations, if combined with --editable.')) 535afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com parser.add_argument('--port', type=int, 536afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com help=('Which TCP port to listen on for HTTP requests; ' 537afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com 'defaults to %(default)s'), 538afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com default=DEFAULT_PORT) 539542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com parser.add_argument('--reload', type=int, 540542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com help=('How often (a period in seconds) to update the ' 541b063e13428cef96abda9a80b273c92e2ed9c8287epoger@google.com 'results. If specified, both expected and actual ' 542d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 'results will be updated by running "gclient sync" ' 543d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 'on your Skia checkout as a whole. ' 544542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 'By default, we do not reload at all, and you ' 545542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com 'must restart the server to pick up new data.'), 546542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com default=0) 547f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com args = parser.parse_args() 548f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com global _SERVER 549542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com _SERVER = Server(actuals_dir=args.actuals_dir, 5505865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org actuals_repo_revision=args.actuals_revision, 5515865ec5f3a367e0398ba6c322916d12a08c5002bcommit-bot@chromium.org actuals_repo_url=args.actuals_repo, 552542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com port=args.port, export=args.export, editable=args.editable, 553542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com reload_seconds=args.reload) 554f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com _SERVER.run() 555f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com 556d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com 557f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comif __name__ == '__main__': 558f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com main() 559