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() |