Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """A git command for managing a local cache of git repositories.""" | 6 """A git command for managing a local cache of git repositories.""" |
| 7 | 7 |
| 8 from __future__ import print_function | 8 from __future__ import print_function |
| 9 import errno | 9 import errno |
| 10 import logging | 10 import logging |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 37 | 37 |
| 38 class LockError(Exception): | 38 class LockError(Exception): |
| 39 pass | 39 pass |
| 40 | 40 |
| 41 class RefsHeadsFailedToFetch(Exception): | 41 class RefsHeadsFailedToFetch(Exception): |
| 42 pass | 42 pass |
| 43 | 43 |
| 44 class Lockfile(object): | 44 class Lockfile(object): |
| 45 """Class to represent a cross-platform process-specific lockfile.""" | 45 """Class to represent a cross-platform process-specific lockfile.""" |
| 46 | 46 |
| 47 def __init__(self, path): | 47 def __init__(self, path, timeout=0): |
| 48 self.path = os.path.abspath(path) | 48 self.path = os.path.abspath(path) |
| 49 self.timeout = timeout | |
| 49 self.lockfile = self.path + ".lock" | 50 self.lockfile = self.path + ".lock" |
| 50 self.pid = os.getpid() | 51 self.pid = os.getpid() |
| 51 | 52 |
| 52 def _read_pid(self): | 53 def _read_pid(self): |
| 53 """Read the pid stored in the lockfile. | 54 """Read the pid stored in the lockfile. |
| 54 | 55 |
| 55 Note: This method is potentially racy. By the time it returns the lockfile | 56 Note: This method is potentially racy. By the time it returns the lockfile |
| 56 may have been unlocked, removed, or stolen by some other process. | 57 may have been unlocked, removed, or stolen by some other process. |
| 57 """ | 58 """ |
| 58 try: | 59 try: |
| (...skipping 25 matching lines...) Expand all Loading... | |
| 84 if exitcode == 0: | 85 if exitcode == 0: |
| 85 return | 86 return |
| 86 time.sleep(3) | 87 time.sleep(3) |
| 87 raise LockError('Failed to remove lock: %s' % lockfile) | 88 raise LockError('Failed to remove lock: %s' % lockfile) |
| 88 else: | 89 else: |
| 89 os.remove(self.lockfile) | 90 os.remove(self.lockfile) |
| 90 | 91 |
| 91 def lock(self): | 92 def lock(self): |
| 92 """Acquire the lock. | 93 """Acquire the lock. |
| 93 | 94 |
| 94 Note: This is a NON-BLOCKING FAIL-FAST operation. | 95 This will block with a deadline of self.timeout seconds. |
| 95 Do. Or do not. There is no try. | 96 If self.timeout is zero, a NON-BLOCKING FAIL-FAST operation. |
| 96 """ | 97 """ |
| 97 try: | 98 elapsed = 0 |
| 98 self._make_lockfile() | 99 while True: |
| 99 except OSError as e: | 100 try: |
| 100 if e.errno == errno.EEXIST: | 101 self._make_lockfile() |
| 101 raise LockError("%s is already locked" % self.path) | 102 return |
| 102 else: | 103 except OSError as e: |
| 103 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno)) | 104 if elapsed < self.timeout: |
| 105 sleep_time = min(3, self.timeout - elapsed) | |
| 106 elapsed += sleep_time | |
|
tandrii(chromium)
2016/02/02 22:01:00
maybe also print here that it's waiting on a lock?
szager1
2016/02/02 22:33:11
Done.
| |
| 107 time.sleep(sleep_time) | |
| 108 continue | |
| 109 if e.errno == errno.EEXIST: | |
| 110 raise LockError("%s is already locked" % self.path) | |
| 111 else: | |
| 112 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno)) | |
| 104 | 113 |
| 105 def unlock(self): | 114 def unlock(self): |
| 106 """Release the lock.""" | 115 """Release the lock.""" |
| 107 try: | 116 try: |
| 108 if not self.is_locked(): | 117 if not self.is_locked(): |
| 109 raise LockError("%s is not locked" % self.path) | 118 raise LockError("%s is not locked" % self.path) |
| 110 if not self.i_am_locking(): | 119 if not self.i_am_locking(): |
| 111 raise LockError("%s is locked, but not by me" % self.path) | 120 raise LockError("%s is locked, but not by me" % self.path) |
| 112 self._remove_lockfile() | 121 self._remove_lockfile() |
| 113 except WinErr: | 122 except WinErr: |
| (...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 394 for spec in fetch_specs: | 403 for spec in fetch_specs: |
| 395 try: | 404 try: |
| 396 self.print('Fetching %s' % spec) | 405 self.print('Fetching %s' % spec) |
| 397 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True) | 406 self.RunGit(fetch_cmd + [spec], cwd=rundir, retry=True) |
| 398 except subprocess.CalledProcessError: | 407 except subprocess.CalledProcessError: |
| 399 if spec == '+refs/heads/*:refs/heads/*': | 408 if spec == '+refs/heads/*:refs/heads/*': |
| 400 raise RefsHeadsFailedToFetch | 409 raise RefsHeadsFailedToFetch |
| 401 logging.warn('Fetch of %s failed' % spec) | 410 logging.warn('Fetch of %s failed' % spec) |
| 402 | 411 |
| 403 def populate(self, depth=None, shallow=False, bootstrap=False, | 412 def populate(self, depth=None, shallow=False, bootstrap=False, |
| 404 verbose=False, ignore_lock=False): | 413 verbose=False, ignore_lock=False, lock_timeout=0): |
| 405 assert self.GetCachePath() | 414 assert self.GetCachePath() |
| 406 if shallow and not depth: | 415 if shallow and not depth: |
| 407 depth = 10000 | 416 depth = 10000 |
| 408 gclient_utils.safe_makedirs(self.GetCachePath()) | 417 gclient_utils.safe_makedirs(self.GetCachePath()) |
| 409 | 418 |
| 410 lockfile = Lockfile(self.mirror_path) | 419 lockfile = Lockfile(self.mirror_path, lock_timeout) |
| 411 if not ignore_lock: | 420 if not ignore_lock: |
| 412 lockfile.lock() | 421 lockfile.lock() |
| 413 | 422 |
| 414 tempdir = None | 423 tempdir = None |
| 415 try: | 424 try: |
| 416 tempdir = self._ensure_bootstrapped(depth, bootstrap) | 425 tempdir = self._ensure_bootstrapped(depth, bootstrap) |
| 417 rundir = tempdir or self.mirror_path | 426 rundir = tempdir or self.mirror_path |
| 418 self._fetch(rundir, verbose, depth) | 427 self._fetch(rundir, verbose, depth) |
| 419 except RefsHeadsFailedToFetch: | 428 except RefsHeadsFailedToFetch: |
| 420 # This is a major failure, we need to clean and force a bootstrap. | 429 # This is a major failure, we need to clean and force a bootstrap. |
| (...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 575 if not len(args) == 1: | 584 if not len(args) == 1: |
| 576 parser.error('git cache populate only takes exactly one repo url.') | 585 parser.error('git cache populate only takes exactly one repo url.') |
| 577 url = args[0] | 586 url = args[0] |
| 578 | 587 |
| 579 mirror = Mirror(url, refs=options.ref) | 588 mirror = Mirror(url, refs=options.ref) |
| 580 kwargs = { | 589 kwargs = { |
| 581 'verbose': options.verbose, | 590 'verbose': options.verbose, |
| 582 'shallow': options.shallow, | 591 'shallow': options.shallow, |
| 583 'bootstrap': not options.no_bootstrap, | 592 'bootstrap': not options.no_bootstrap, |
| 584 'ignore_lock': options.ignore_locks, | 593 'ignore_lock': options.ignore_locks, |
| 594 'lock_timeout': options.timeout, | |
| 585 } | 595 } |
| 586 if options.depth: | 596 if options.depth: |
| 587 kwargs['depth'] = options.depth | 597 kwargs['depth'] = options.depth |
| 588 mirror.populate(**kwargs) | 598 mirror.populate(**kwargs) |
| 589 | 599 |
| 590 | 600 |
| 591 @subcommand.usage('Fetch new commits into cache and current checkout') | 601 @subcommand.usage('Fetch new commits into cache and current checkout') |
| 592 def CMDfetch(parser, args): | 602 def CMDfetch(parser, args): |
| 593 """Update mirror, and fetch in cwd.""" | 603 """Update mirror, and fetch in cwd.""" |
| 594 parser.add_option('--all', action='store_true', help='Fetch all remotes') | 604 parser.add_option('--all', action='store_true', help='Fetch all remotes') |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 618 remotes = [upstream] | 628 remotes = [upstream] |
| 619 if not remotes: | 629 if not remotes: |
| 620 remotes = ['origin'] | 630 remotes = ['origin'] |
| 621 | 631 |
| 622 cachepath = Mirror.GetCachePath() | 632 cachepath = Mirror.GetCachePath() |
| 623 git_dir = os.path.abspath(subprocess.check_output( | 633 git_dir = os.path.abspath(subprocess.check_output( |
| 624 [Mirror.git_exe, 'rev-parse', '--git-dir'])) | 634 [Mirror.git_exe, 'rev-parse', '--git-dir'])) |
| 625 git_dir = os.path.abspath(git_dir) | 635 git_dir = os.path.abspath(git_dir) |
| 626 if git_dir.startswith(cachepath): | 636 if git_dir.startswith(cachepath): |
| 627 mirror = Mirror.FromPath(git_dir) | 637 mirror = Mirror.FromPath(git_dir) |
| 628 mirror.populate(bootstrap=not options.no_bootstrap) | 638 mirror.populate( |
| 639 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout) | |
| 629 return 0 | 640 return 0 |
| 630 for remote in remotes: | 641 for remote in remotes: |
| 631 remote_url = subprocess.check_output( | 642 remote_url = subprocess.check_output( |
| 632 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip() | 643 [Mirror.git_exe, 'config', 'remote.%s.url' % remote]).strip() |
| 633 if remote_url.startswith(cachepath): | 644 if remote_url.startswith(cachepath): |
| 634 mirror = Mirror.FromPath(remote_url) | 645 mirror = Mirror.FromPath(remote_url) |
| 635 mirror.print = lambda *args: None | 646 mirror.print = lambda *args: None |
| 636 print('Updating git cache...') | 647 print('Updating git cache...') |
| 637 mirror.populate(bootstrap=not options.no_bootstrap) | 648 mirror.populate( |
| 649 bootstrap=not options.no_bootstrap, lock_timeout=options.timeout) | |
| 638 subprocess.check_call([Mirror.git_exe, 'fetch', remote]) | 650 subprocess.check_call([Mirror.git_exe, 'fetch', remote]) |
| 639 return 0 | 651 return 0 |
| 640 | 652 |
| 641 | 653 |
| 642 @subcommand.usage('[url of repo to unlock, or -a|--all]') | 654 @subcommand.usage('[url of repo to unlock, or -a|--all]') |
| 643 def CMDunlock(parser, args): | 655 def CMDunlock(parser, args): |
| 644 """Unlock one or all repos if their lock files are still around.""" | 656 """Unlock one or all repos if their lock files are still around.""" |
| 645 parser.add_option('--force', '-f', action='store_true', | 657 parser.add_option('--force', '-f', action='store_true', |
| 646 help='Actually perform the action') | 658 help='Actually perform the action') |
| 647 parser.add_option('--all', '-a', action='store_true', | 659 parser.add_option('--all', '-a', action='store_true', |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 676 """Wrapper class for OptionParser to handle global options.""" | 688 """Wrapper class for OptionParser to handle global options.""" |
| 677 | 689 |
| 678 def __init__(self, *args, **kwargs): | 690 def __init__(self, *args, **kwargs): |
| 679 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs) | 691 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs) |
| 680 self.add_option('-c', '--cache-dir', | 692 self.add_option('-c', '--cache-dir', |
| 681 help='Path to the directory containing the cache') | 693 help='Path to the directory containing the cache') |
| 682 self.add_option('-v', '--verbose', action='count', default=1, | 694 self.add_option('-v', '--verbose', action='count', default=1, |
| 683 help='Increase verbosity (can be passed multiple times)') | 695 help='Increase verbosity (can be passed multiple times)') |
| 684 self.add_option('-q', '--quiet', action='store_true', | 696 self.add_option('-q', '--quiet', action='store_true', |
| 685 help='Suppress all extraneous output') | 697 help='Suppress all extraneous output') |
| 698 self.add_option('--timeout', type='int', default=0, | |
| 699 help='Timeout for acquiring cache lock, in seconds') | |
| 686 | 700 |
| 687 def parse_args(self, args=None, values=None): | 701 def parse_args(self, args=None, values=None): |
| 688 options, args = optparse.OptionParser.parse_args(self, args, values) | 702 options, args = optparse.OptionParser.parse_args(self, args, values) |
| 689 if options.quiet: | 703 if options.quiet: |
| 690 options.verbose = 0 | 704 options.verbose = 0 |
| 691 | 705 |
| 692 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] | 706 levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] |
| 693 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)]) | 707 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)]) |
| 694 | 708 |
| 695 try: | 709 try: |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 710 dispatcher = subcommand.CommandDispatcher(__name__) | 724 dispatcher = subcommand.CommandDispatcher(__name__) |
| 711 return dispatcher.execute(OptionParser(), argv) | 725 return dispatcher.execute(OptionParser(), argv) |
| 712 | 726 |
| 713 | 727 |
| 714 if __name__ == '__main__': | 728 if __name__ == '__main__': |
| 715 try: | 729 try: |
| 716 sys.exit(main(sys.argv[1:])) | 730 sys.exit(main(sys.argv[1:])) |
| 717 except KeyboardInterrupt: | 731 except KeyboardInterrupt: |
| 718 sys.stderr.write('interrupted\n') | 732 sys.stderr.write('interrupted\n') |
| 719 sys.exit(1) | 733 sys.exit(1) |
| OLD | NEW |