| Index: gm/rebaseline_server/server.py
|
| ===================================================================
|
| --- gm/rebaseline_server/server.py (revision 12395)
|
| +++ gm/rebaseline_server/server.py (working copy)
|
| @@ -19,8 +19,10 @@
|
| import re
|
| import shutil
|
| import socket
|
| +import subprocess
|
| import sys
|
| import thread
|
| +import threading
|
| import time
|
| import urlparse
|
|
|
| @@ -42,8 +44,6 @@
|
|
|
| ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual'
|
| PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
|
| -TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(
|
| - os.path.realpath(__file__))))
|
| EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
|
| GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
|
| 'generated-images')
|
| @@ -67,6 +67,29 @@
|
|
|
| _SERVER = None # This gets filled in by main()
|
|
|
| +
|
| +def _run_command(args, directory):
|
| + """Runs a command and returns stdout as a single string.
|
| +
|
| + Args:
|
| + args: the command to run, as a list of arguments
|
| + directory: directory within which to run the command
|
| +
|
| + Returns: stdout, as a string
|
| +
|
| + Raises an Exception if the command failed (exited with nonzero return code).
|
| + """
|
| + logging.debug('_run_command: %s in directory %s' % (args, directory))
|
| + proc = subprocess.Popen(args, cwd=directory,
|
| + stdout=subprocess.PIPE,
|
| + stderr=subprocess.PIPE)
|
| + (stdout, stderr) = proc.communicate()
|
| + if proc.returncode is not 0:
|
| + raise Exception('command "%s" failed in dir "%s": %s' %
|
| + (args, directory, stderr))
|
| + return stdout
|
| +
|
| +
|
| def _get_routable_ip_address():
|
| """Returns routable IP address of this host (the IP address of its network
|
| interface that would be used for most traffic, not its localhost
|
| @@ -77,6 +100,7 @@
|
| sock.close()
|
| return host
|
|
|
| +
|
| def _create_svn_checkout(dir_path, repo_url):
|
| """Creates local checkout of an SVN repository at the specified directory
|
| path, returning an svn.Svn object referring to the local checkout.
|
| @@ -120,69 +144,87 @@
|
| self._actuals_repo = _create_svn_checkout(
|
| dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO)
|
|
|
| - # We only update the expectations dir if the server was run with a
|
| - # nonzero --reload argument; otherwise, we expect the user to maintain
|
| - # her own expectations as she sees fit.
|
| - #
|
| - # TODO(epoger): Use git instead of svn to update the expectations dir, since
|
| - # the Skia repo is moving to git.
|
| - # When we make that change, we will have to update the entire workspace,
|
| - # not just the expectations dir, because git only operates on the repo
|
| - # as a whole.
|
| - # And since Skia uses depot_tools to manage its dependencies, we will have
|
| - # to run "gclient sync" rather than a raw "git pull".
|
| - if reload_seconds:
|
| - self._expectations_repo = svn.Svn(EXPECTATIONS_DIR)
|
| - else:
|
| - self._expectations_repo = None
|
| + # Reentrant lock that must be held whenever updating EITHER of:
|
| + # 1. self._results
|
| + # 2. the expected or actual results on local disk
|
| + self.results_rlock = threading.RLock()
|
| + # self._results will be filled in by calls to update_results()
|
| + self._results = None
|
|
|
| + @property
|
| + def results(self):
|
| + """ Returns the most recently generated results, or None if update_results()
|
| + has not been called yet. """
|
| + return self._results
|
| +
|
| + @property
|
| def is_exported(self):
|
| """ Returns true iff HTTP clients on other hosts are allowed to access
|
| this server. """
|
| return self._export
|
|
|
| + @property
|
| def is_editable(self):
|
| """ Returns true iff HTTP clients are allowed to submit new baselines. """
|
| return self._editable
|
|
|
| + @property
|
| def reload_seconds(self):
|
| """ Returns the result reload period in seconds, or 0 if we don't reload
|
| results. """
|
| return self._reload_seconds
|
|
|
| def update_results(self):
|
| - """ Create or update self.results, based on the expectations in
|
| + """ Create or update self._results, based on the expectations in
|
| EXPECTATIONS_DIR and the latest actuals from skia-autogen.
|
| +
|
| + We hold self.results_rlock while we do this, to guarantee that no other
|
| + thread attempts to update either self._results or the underlying files at
|
| + the same time.
|
| """
|
| - logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
|
| - self._actuals_dir, ACTUALS_SVN_REPO))
|
| - self._actuals_repo.Update('.')
|
| + with self.results_rlock:
|
| + logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
|
| + self._actuals_dir, ACTUALS_SVN_REPO))
|
| + self._actuals_repo.Update('.')
|
|
|
| - if self._expectations_repo:
|
| + # We only update the expectations dir if the server was run with a
|
| + # nonzero --reload argument; otherwise, we expect the user to maintain
|
| + # her own expectations as she sees fit.
|
| + #
|
| + # Because the Skia repo is moving from SVN to git, and git does not
|
| + # support updating a single directory tree, we have to update the entire
|
| + # repo checkout.
|
| + #
|
| + # Because Skia uses depot_tools, we have to update using "gclient sync"
|
| + # instead of raw git (or SVN) update. Happily, this will work whether
|
| + # the checkout was created using git or SVN.
|
| + if self._reload_seconds:
|
| + logging.info(
|
| + 'Updating expected GM results in %s by syncing Skia repo ...' %
|
| + EXPECTATIONS_DIR)
|
| + _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
|
| +
|
| logging.info(
|
| - 'Updating expected GM results in %s ...' % EXPECTATIONS_DIR)
|
| - self._expectations_repo.Update('.')
|
| -
|
| - logging.info(
|
| ('Parsing results from actuals in %s and expectations in %s, '
|
| - + 'and generating pixel diffs (may take a while) ...') % (
|
| - self._actuals_dir, EXPECTATIONS_DIR))
|
| - self.results = results.Results(
|
| - actuals_root=self._actuals_dir,
|
| - expected_root=EXPECTATIONS_DIR,
|
| - generated_images_root=GENERATED_IMAGES_ROOT)
|
| + + 'and generating pixel diffs (may take a while) ...') % (
|
| + self._actuals_dir, EXPECTATIONS_DIR))
|
| + self._results = results.Results(
|
| + actuals_root=self._actuals_dir,
|
| + expected_root=EXPECTATIONS_DIR,
|
| + generated_images_root=GENERATED_IMAGES_ROOT)
|
|
|
| def _result_reloader(self):
|
| - """ If --reload argument was specified, reload results at the appropriate
|
| - interval.
|
| + """ Reload results at the appropriate interval. This never exits, so it
|
| + should be run in its own thread.
|
| """
|
| - while self._reload_seconds:
|
| + while True:
|
| time.sleep(self._reload_seconds)
|
| self.update_results()
|
|
|
| def run(self):
|
| self.update_results()
|
| - thread.start_new_thread(self._result_reloader, ())
|
| + if self._reload_seconds:
|
| + thread.start_new_thread(self._result_reloader, ())
|
|
|
| if self._export:
|
| server_address = ('', self._port)
|
| @@ -256,9 +298,9 @@
|
| # We only return these timestamps if the --reload argument was passed;
|
| # otherwise, we have no idea when the expectations were last updated
|
| # (we allow the user to maintain her own expectations as she sees fit).
|
| - 'timeUpdated': time_updated if _SERVER.reload_seconds() else None,
|
| + 'timeUpdated': time_updated if _SERVER.reload_seconds else None,
|
| 'timeNextUpdateAvailable': (
|
| - (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds()
|
| + (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds
|
| else None),
|
|
|
| # The type we passed to get_results_of_type()
|
| @@ -269,10 +311,10 @@
|
| 'dataHash': str(hash(repr(response_dict['testData']))),
|
|
|
| # Whether the server will accept edits back.
|
| - 'isEditable': _SERVER.is_editable(),
|
| + 'isEditable': _SERVER.is_editable,
|
|
|
| # Whether the service is accessible from other hosts.
|
| - 'isExported': _SERVER.is_exported(),
|
| + 'isExported': _SERVER.is_exported,
|
| }
|
| self.send_json_dict(response_dict)
|
| except:
|
| @@ -343,7 +385,7 @@
|
|
|
| Raises an Exception if there were any problems.
|
| """
|
| - if not _SERVER.is_editable():
|
| + if not _SERVER.is_editable:
|
| raise Exception('this server is not running in --editable mode')
|
|
|
| content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
|
| @@ -357,23 +399,24 @@
|
| logging.debug('do_POST_edits: received new GM expectations data [%s]' %
|
| data)
|
|
|
| - # Since we must make multiple calls to the Results object, grab a
|
| - # reference to it in case it is updated to point at a new Results
|
| - # object within another thread.
|
| - results_obj = _SERVER.results
|
| - oldResultsType = data['oldResultsType']
|
| - oldResults = results_obj.get_results_of_type(oldResultsType)
|
| - oldResultsHash = str(hash(repr(oldResults['testData'])))
|
| - if oldResultsHash != data['oldResultsHash']:
|
| - raise Exception('results of type "%s" changed while the client was '
|
| - 'making modifications. The client should reload the '
|
| - 'results and submit the modifications again.' %
|
| - oldResultsType)
|
| - results_obj.edit_expectations(data['modifications'])
|
| + # Update the results on disk with the information we received from the
|
| + # client.
|
| + # We must hold _SERVER.results_rlock while we do this, to guarantee that
|
| + # no other thread updates expectations (from the Skia repo) while we are
|
| + # updating them (using the info we received from the client).
|
| + with _SERVER.results_rlock:
|
| + oldResultsType = data['oldResultsType']
|
| + oldResults = _SERVER.results.get_results_of_type(oldResultsType)
|
| + oldResultsHash = str(hash(repr(oldResults['testData'])))
|
| + if oldResultsHash != data['oldResultsHash']:
|
| + raise Exception('results of type "%s" changed while the client was '
|
| + 'making modifications. The client should reload the '
|
| + 'results and submit the modifications again.' %
|
| + oldResultsType)
|
| + _SERVER.results.edit_expectations(data['modifications'])
|
| + # Read the updated results back from disk.
|
| + _SERVER.update_results()
|
|
|
| - # Now that the edits have been committed, update results to reflect them.
|
| - _SERVER.update_results()
|
| -
|
| def redirect_to(self, url):
|
| """ Redirect the HTTP client to a different url.
|
|
|
| @@ -445,7 +488,8 @@
|
| parser.add_argument('--reload', type=int,
|
| help=('How often (a period in seconds) to update the '
|
| 'results. If specified, both expected and actual '
|
| - 'results will be updated. '
|
| + 'results will be updated by running "gclient sync" '
|
| + 'on your Skia checkout as a whole. '
|
| 'By default, we do not reload at all, and you '
|
| 'must restart the server to pick up new data.'),
|
| default=0)
|
| @@ -456,5 +500,6 @@
|
| reload_seconds=args.reload)
|
| _SERVER.run()
|
|
|
| +
|
| if __name__ == '__main__':
|
| main()
|
|
|