Index: commit-queue/commit_queue.py |
=================================================================== |
--- commit-queue/commit_queue.py (revision 249146) |
+++ commit-queue/commit_queue.py (working copy) |
@@ -1,397 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
-"""Commit queue executable. |
- |
-Reuse Rietveld and the Chromium Try Server to process and automatically commit |
-patches. |
-""" |
- |
-import logging |
-import logging.handlers |
-import optparse |
-import os |
-import shutil |
-import signal |
-import socket |
-import sys |
-import tempfile |
-import time |
- |
-import find_depot_tools # pylint: disable=W0611 |
-import checkout |
-import fix_encoding |
-import rietveld |
-import subprocess2 |
- |
-import async_push |
-import cq_alerts |
-import creds |
-import errors |
-import projects |
-import sig_handler |
- |
- |
-ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
- |
- |
-class OnlyIssueRietveld(rietveld.Rietveld): |
- """Returns a single issue for end-to-end in prod testing.""" |
- def __init__(self, url, email, password, extra_headers, only_issue): |
- super(OnlyIssueRietveld, self).__init__(url, email, password, extra_headers) |
- self._only_issue = only_issue |
- |
- def get_pending_issues(self): |
- """If it's set to return a single issue, only return this one.""" |
- if self._only_issue: |
- return [self._only_issue] |
- return [] |
- |
- def get_issue_properties(self, issue, messages): |
- """Hacks the result to fake that the issue has the commit bit set.""" |
- data = super(OnlyIssueRietveld, self).get_issue_properties(issue, messages) |
- if issue == self._only_issue: |
- data['commit'] = True |
- return data |
- |
- def set_flag(self, issue, patchset, flag, value): |
- if issue == self._only_issue and flag == 'commit' and value == 'False': |
- self._only_issue = None |
- return super(OnlyIssueRietveld, self).set_flag(issue, patchset, flag, value) |
- |
- |
-class FakeCheckout(object): |
- def __init__(self): |
- self.project_path = os.getcwd() |
- self.project_name = os.path.basename(self.project_path) |
- |
- @staticmethod |
- def prepare(_revision): |
- logging.info('FakeCheckout is syncing') |
- return unicode('FAKE') |
- |
- @staticmethod |
- def apply_patch(*_args): |
- logging.info('FakeCheckout is applying a patch') |
- |
- @staticmethod |
- def commit(*_args): |
- logging.info('FakeCheckout is committing patch') |
- return 'FAKED' |
- |
- @staticmethod |
- def get_settings(_key): |
- return None |
- |
- @staticmethod |
- def revisions(*_args): |
- return None |
- |
- |
-def AlertOnUncleanCheckout(): |
- """Sends an alert if the cq is running live with local edits.""" |
- diff = subprocess2.capture(['gclient', 'diff'], cwd=ROOT_DIR).strip() |
- if diff: |
- cq_alerts.SendAlert( |
- 'CQ running with local diff.', |
- ('Ruh-roh! Commit queue was started with an unclean checkout.\n\n' |
- '$ gclient diff\n%s' % diff)) |
- |
- |
-def SetupLogging(options): |
- """Configures the logging module.""" |
- logging.getLogger().setLevel(logging.DEBUG) |
- if options.verbose: |
- level = logging.DEBUG |
- else: |
- level = logging.INFO |
- console_logging = logging.StreamHandler() |
- console_logging.setFormatter(logging.Formatter( |
- '%(asctime)s %(levelname)7s %(message)s')) |
- console_logging.setLevel(level) |
- logging.getLogger().addHandler(console_logging) |
- |
- log_directory = 'logs-' + options.project |
- if not os.path.exists(log_directory): |
- os.mkdir(log_directory) |
- |
- logging_rotating_file = logging.handlers.RotatingFileHandler( |
- filename=os.path.join(log_directory, 'commit_queue.log'), |
- maxBytes= 10*1024*1024, |
- backupCount=50) |
- logging_rotating_file.setLevel(logging.DEBUG) |
- logging_rotating_file.setFormatter(logging.Formatter( |
- '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)4d): %(message)s')) |
- logging.getLogger().addHandler(logging_rotating_file) |
- |
- |
-class SignalInterrupt(Exception): |
- """Exception that indicates being interrupted by a caught signal.""" |
- |
- def __init__(self, signal_set=None, *args, **kwargs): |
- super(SignalInterrupt, self).__init__(*args, **kwargs) |
- self.signal_set = signal_set |
- |
- |
-def SaveDatabaseCopyForDebugging(db_path): |
- """Saves database file for debugging. Returns name of the saved file.""" |
- with tempfile.NamedTemporaryFile( |
- dir=os.path.dirname(db_path), |
- prefix='db.debug.', |
- suffix='.json', |
- delete=False) as tmp_file: |
- with open(db_path) as db_file: |
- shutil.copyfileobj(db_file, tmp_file) |
- return tmp_file.name |
- |
- |
-def main(): |
- # Set a default timeout for sockets. This is critical when talking to remote |
- # services like AppEngine and buildbot. |
- # TODO(phajdan.jr): This used to be 70s. Investigate lowering it again. |
- socket.setdefaulttimeout(60.0 * 15) |
- |
- parser = optparse.OptionParser( |
- description=sys.modules['__main__'].__doc__) |
- project_choices = projects.supported_projects() |
- parser.add_option('-v', '--verbose', action='store_true') |
- parser.add_option( |
- '--no-dry-run', |
- action='store_false', |
- dest='dry_run', |
- default=True, |
- help='Run for real instead of dry-run mode which is the default. ' |
- 'WARNING: while the CQ won\'t touch rietveld in dry-run mode, the ' |
- 'Try Server will. So it is recommended to use --only-issue') |
- parser.add_option( |
- '--only-issue', |
- type='int', |
- help='Limits to a single issue. Useful for live testing; WARNING: it ' |
- 'will fake that the issue has the CQ bit set, so only try with an ' |
- 'issue you don\'t mind about.') |
- parser.add_option( |
- '--fake', |
- action='store_true', |
- help='Run with a fake checkout to speed up testing') |
- parser.add_option( |
- '--no-try', |
- action='store_true', |
- help='Don\'t send try jobs.') |
- parser.add_option( |
- '-p', |
- '--poll-interval', |
- type='int', |
- default=10, |
- help='Minimum delay between each polling loop, default: %default') |
- parser.add_option( |
- '--query-only', |
- action='store_true', |
- help='Return internal state') |
- parser.add_option( |
- '--project', |
- choices=project_choices, |
- help='Project to run the commit queue against: %s' % |
- ', '.join(project_choices)) |
- parser.add_option( |
- '-u', |
- '--user', |
- default='commit-bot@chromium.org', |
- help='User to use instead of %default') |
- parser.add_option( |
- '--rietveld', |
- default='https://codereview.chromium.org', |
- help='Rietveld server to use instead of %default') |
- options, args = parser.parse_args() |
- if args: |
- parser.error('Unsupported args: %s' % args) |
- if not options.project: |
- parser.error('Need to pass a valid project to --project.\nOptions are: %s' % |
- ', '.join(project_choices)) |
- |
- SetupLogging(options) |
- try: |
- work_dir = os.path.join(ROOT_DIR, 'workdir') |
- # Use our specific subversion config. |
- checkout.SvnMixIn.svn_config = checkout.SvnConfig( |
- os.path.join(ROOT_DIR, 'subversion_config')) |
- |
- url = options.rietveld |
- gaia_creds = creds.Credentials(os.path.join(work_dir, '.gaia_pwd')) |
- if options.dry_run: |
- logging.debug('Dry run - skipping SCM check.') |
- if options.only_issue: |
- parser.error('--only-issue is not supported with dry run') |
- else: |
- print('Using read-only Rietveld') |
- # Make sure rietveld is not modified. Pass empty email and |
- # password to bypass authentication; this additionally |
- # guarantees rietveld will not allow any changes. |
- rietveld_obj = rietveld.ReadOnlyRietveld(url, email='', password='') |
- else: |
- AlertOnUncleanCheckout() |
- print('WARNING: The Commit Queue is going to commit stuff') |
- if options.only_issue: |
- print('Using only issue %d' % options.only_issue) |
- rietveld_obj = OnlyIssueRietveld( |
- url, |
- options.user, |
- gaia_creds.get(options.user), |
- None, |
- options.only_issue) |
- else: |
- rietveld_obj = rietveld.Rietveld( |
- url, |
- options.user, |
- gaia_creds.get(options.user), |
- None) |
- |
- pc = projects.load_project( |
- options.project, |
- options.user, |
- work_dir, |
- rietveld_obj, |
- options.no_try) |
- |
- if options.dry_run: |
- if options.fake: |
- # Disable the checkout. |
- print 'Using no checkout' |
- pc.context.checkout = FakeCheckout() |
- else: |
- print 'Using read-only checkout' |
- pc.context.checkout = checkout.ReadOnlyCheckout(pc.context.checkout) |
- # Save pushed events on disk. |
- print 'Using read-only chromium-status interface' |
- pc.context.status = async_push.AsyncPushStore() |
- |
- landmine_path = os.path.join(work_dir, |
- pc.context.checkout.project_name + '.landmine') |
- db_path = os.path.join(work_dir, pc.context.checkout.project_name + '.json') |
- if os.path.isfile(db_path): |
- if os.path.isfile(landmine_path): |
- debugging_path = SaveDatabaseCopyForDebugging(db_path) |
- os.remove(db_path) |
- logging.warning(('Deleting database because previous shutdown ' |
- 'was unclean. The copy of the database is saved ' |
- 'as %s.') % debugging_path) |
- else: |
- try: |
- pc.load(db_path) |
- except ValueError as e: |
- debugging_path = SaveDatabaseCopyForDebugging(db_path) |
- os.remove(db_path) |
- logging.warning(('Failed to parse database (%r), deleting it. ' |
- 'The copy of the database is saved as %s.') % |
- (e, debugging_path)) |
- raise e |
- |
- # Create a file to indicate unclean shutdown. |
- with open(landmine_path, 'w'): |
- pass |
- |
- sig_handler.installHandlers( |
- signal.SIGINT, |
- signal.SIGHUP |
- ) |
- |
- # Sync every 5 minutes. |
- SYNC_DELAY = 5*60 |
- try: |
- if options.query_only: |
- pc.look_for_new_pending_commit() |
- pc.update_status() |
- print(str(pc.queue)) |
- os.remove(landmine_path) |
- return 0 |
- |
- now = time.time() |
- next_loop = now + options.poll_interval |
- # First sync is on second loop. |
- next_sync = now + options.poll_interval * 2 |
- while True: |
- # In theory, we would gain in performance to parallelize these tasks. In |
- # practice I'm not sure it matters. |
- pc.look_for_new_pending_commit() |
- pc.process_new_pending_commit() |
- pc.update_status() |
- pc.scan_results() |
- if sig_handler.getTriggeredSignals(): |
- raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals()) |
- # Save the db at each loop. The db can easily be in the 1mb range so |
- # it's slowing down the CQ a tad but it in the 100ms range even for that |
- # size. |
- pc.save(db_path) |
- |
- # More than a second to wait and due to sync. |
- now = time.time() |
- if (next_loop - now) >= 1 and (next_sync - now) <= 0: |
- if sys.stdout.isatty(): |
- sys.stdout.write('Syncing while waiting \r') |
- sys.stdout.flush() |
- try: |
- pc.context.checkout.prepare(None) |
- except subprocess2.CalledProcessError as e: |
- # Don't crash, most of the time it's the svn server that is dead. |
- # How fun. Send a stack trace to annoy the maintainer. |
- errors.send_stack(e) |
- next_sync = time.time() + SYNC_DELAY |
- |
- now = time.time() |
- next_loop = max(now, next_loop) |
- while True: |
- # Abort if any signals are set |
- if sig_handler.getTriggeredSignals(): |
- raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals()) |
- delay = next_loop - now |
- if delay <= 0: |
- break |
- if sys.stdout.isatty(): |
- sys.stdout.write('Sleeping for %1.1f seconds \r' % delay) |
- sys.stdout.flush() |
- time.sleep(min(delay, 0.1)) |
- now = time.time() |
- if sys.stdout.isatty(): |
- sys.stdout.write('Running (please do not interrupt) \r') |
- sys.stdout.flush() |
- next_loop = time.time() + options.poll_interval |
- except: # Catch all fatal exit conditions. |
- logging.exception('CQ loop terminating') |
- raise |
- finally: |
- logging.warning('Saving db...') |
- pc.save(db_path) |
- pc.close() |
- logging.warning('db save successful.') |
- except SignalInterrupt: |
- # This is considered a clean shutdown: we only throw this exception |
- # from selected places in the code where the database should be |
- # in a known and consistent state. |
- os.remove(landmine_path) |
- |
- print 'Bye bye (SignalInterrupt)' |
- # 23 is an arbitrary value to signal loop.sh that it must stop looping. |
- return 23 |
- except KeyboardInterrupt: |
- # This is actually an unclean shutdown. Do not remove the landmine file. |
- # One example of this is user hitting ctrl-c twice at an arbitrary point |
- # inside the CQ loop. There are no guarantees about consistent state |
- # of the database then. |
- |
- print 'Bye bye (KeyboardInterrupt - this is considered unclean shutdown)' |
- # 23 is an arbitrary value to signal loop.sh that it must stop looping. |
- return 23 |
- except errors.ConfigurationError as e: |
- parser.error(str(e)) |
- return 1 |
- |
- # CQ generally doesn't exit by itself, but if we ever get here, it looks |
- # like a clean shutdown so remove the landmine file. |
- # TODO(phajdan.jr): Do we ever get here? |
- os.remove(landmine_path) |
- return 0 |
- |
- |
-if __name__ == '__main__': |
- fix_encoding.fix_encoding() |
- sys.exit(main()) |