| 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 23 matching lines...) Expand all Loading... |
| 34 except NameError: | 34 except NameError: |
| 35 class WinErr(Exception): | 35 class WinErr(Exception): |
| 36 pass | 36 pass |
| 37 | 37 |
| 38 class LockError(Exception): | 38 class LockError(Exception): |
| 39 pass | 39 pass |
| 40 | 40 |
| 41 class ClobberNeeded(Exception): | 41 class ClobberNeeded(Exception): |
| 42 pass | 42 pass |
| 43 | 43 |
| 44 |
| 45 def exponential_backoff_retry(fn, excs=(Exception,), name=None, count=10, |
| 46 sleep_time=0.25, printerr=None): |
| 47 """Executes |fn| up to |count| times, backing off exponentially. |
| 48 |
| 49 Args: |
| 50 fn (callable): The function to execute. If this raises a handled |
| 51 exception, the function will retry with exponential backoff. |
| 52 excs (tuple): A tuple of Exception types to handle. If one of these is |
| 53 raised by |fn|, a retry will be attempted. If |fn| raises an Exception |
| 54 that is not in this list, it will immediately pass through. If |excs| |
| 55 is empty, the Exception base class will be used. |
| 56 name (str): Optional operation name to print in the retry string. |
| 57 count (int): The number of times to try before allowing the exception to |
| 58 pass through. |
| 59 sleep_time (float): The initial number of seconds to sleep in between |
| 60 retries. This will be doubled each retry. |
| 61 printerr (callable): Function that will be called with the error string upon |
| 62 failures. If None, |logging.warning| will be used. |
| 63 |
| 64 Returns: The return value of the successful fn. |
| 65 """ |
| 66 printerr = printerr or logging.warning |
| 67 for i in xrange(count): |
| 68 try: |
| 69 return fn() |
| 70 except excs as e: |
| 71 if (i+1) >= count: |
| 72 raise |
| 73 |
| 74 printerr('Retrying %s in %.2f second(s) (%d / %d attempts): %s' % ( |
| 75 (name or 'operation'), sleep_time, (i+1), count, e)) |
| 76 time.sleep(sleep_time) |
| 77 sleep_time *= 2 |
| 78 |
| 79 |
| 44 class Lockfile(object): | 80 class Lockfile(object): |
| 45 """Class to represent a cross-platform process-specific lockfile.""" | 81 """Class to represent a cross-platform process-specific lockfile.""" |
| 46 | 82 |
| 47 def __init__(self, path, timeout=0): | 83 def __init__(self, path, timeout=0): |
| 48 self.path = os.path.abspath(path) | 84 self.path = os.path.abspath(path) |
| 49 self.timeout = timeout | 85 self.timeout = timeout |
| 50 self.lockfile = self.path + ".lock" | 86 self.lockfile = self.path + ".lock" |
| 51 self.pid = os.getpid() | 87 self.pid = os.getpid() |
| 52 | 88 |
| 53 def _read_pid(self): | 89 def _read_pid(self): |
| (...skipping 18 matching lines...) Expand all Loading... |
| 72 f.close() | 108 f.close() |
| 73 | 109 |
| 74 def _remove_lockfile(self): | 110 def _remove_lockfile(self): |
| 75 """Delete the lockfile. Complains (implicitly) if it doesn't exist. | 111 """Delete the lockfile. Complains (implicitly) if it doesn't exist. |
| 76 | 112 |
| 77 See gclient_utils.py:rmtree docstring for more explanation on the | 113 See gclient_utils.py:rmtree docstring for more explanation on the |
| 78 windows case. | 114 windows case. |
| 79 """ | 115 """ |
| 80 if sys.platform == 'win32': | 116 if sys.platform == 'win32': |
| 81 lockfile = os.path.normcase(self.lockfile) | 117 lockfile = os.path.normcase(self.lockfile) |
| 82 for _ in xrange(3): | 118 |
| 119 def delete(): |
| 83 exitcode = subprocess.call(['cmd.exe', '/c', | 120 exitcode = subprocess.call(['cmd.exe', '/c', |
| 84 'del', '/f', '/q', lockfile]) | 121 'del', '/f', '/q', lockfile]) |
| 85 if exitcode == 0: | 122 if exitcode != 0: |
| 86 return | 123 raise LockError('Failed to remove lock: %s' % (lockfile,)) |
| 87 time.sleep(3) | 124 exponential_backoff_retry( |
| 88 raise LockError('Failed to remove lock: %s' % lockfile) | 125 delete, |
| 126 excs=(LockError,), |
| 127 name='del [%s]' % (lockfile,)) |
| 89 else: | 128 else: |
| 90 os.remove(self.lockfile) | 129 os.remove(self.lockfile) |
| 91 | 130 |
| 92 def lock(self): | 131 def lock(self): |
| 93 """Acquire the lock. | 132 """Acquire the lock. |
| 94 | 133 |
| 95 This will block with a deadline of self.timeout seconds. | 134 This will block with a deadline of self.timeout seconds. |
| 96 """ | 135 """ |
| 97 elapsed = 0 | 136 elapsed = 0 |
| 98 while True: | 137 while True: |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 174 self.url = url | 213 self.url = url |
| 175 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])]) | 214 self.fetch_specs = set([self.parse_fetch_spec(ref) for ref in (refs or [])]) |
| 176 self.basedir = self.UrlToCacheDir(url) | 215 self.basedir = self.UrlToCacheDir(url) |
| 177 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir) | 216 self.mirror_path = os.path.join(self.GetCachePath(), self.basedir) |
| 178 if print_func: | 217 if print_func: |
| 179 self.print = self.print_without_file | 218 self.print = self.print_without_file |
| 180 self.print_func = print_func | 219 self.print_func = print_func |
| 181 else: | 220 else: |
| 182 self.print = print | 221 self.print = print |
| 183 | 222 |
| 184 def print_without_file(self, message, **kwargs): | 223 def print_without_file(self, message, **_kwargs): |
| 185 self.print_func(message) | 224 self.print_func(message) |
| 186 | 225 |
| 187 @property | 226 @property |
| 188 def bootstrap_bucket(self): | 227 def bootstrap_bucket(self): |
| 189 if 'chrome-internal' in self.url: | 228 if 'chrome-internal' in self.url: |
| 190 return 'chrome-git-cache' | 229 return 'chrome-git-cache' |
| 191 else: | 230 else: |
| 192 return 'chromium-git-cache' | 231 return 'chromium-git-cache' |
| 193 | 232 |
| 194 @classmethod | 233 @classmethod |
| (...skipping 28 matching lines...) Expand all Loading... |
| 223 cachepath = subprocess.check_output( | 262 cachepath = subprocess.check_output( |
| 224 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip() | 263 [cls.git_exe, 'config', '--global', 'cache.cachepath']).strip() |
| 225 except subprocess.CalledProcessError: | 264 except subprocess.CalledProcessError: |
| 226 cachepath = None | 265 cachepath = None |
| 227 if not cachepath: | 266 if not cachepath: |
| 228 raise RuntimeError( | 267 raise RuntimeError( |
| 229 'No global cache.cachepath git configuration found.') | 268 'No global cache.cachepath git configuration found.') |
| 230 setattr(cls, 'cachepath', cachepath) | 269 setattr(cls, 'cachepath', cachepath) |
| 231 return getattr(cls, 'cachepath') | 270 return getattr(cls, 'cachepath') |
| 232 | 271 |
| 272 def Rename(self, src, dst): |
| 273 # This is somehow racy on Windows. |
| 274 # Catching OSError because WindowsError isn't portable and |
| 275 # pylint complains. |
| 276 exponential_backoff_retry( |
| 277 lambda: os.rename(src, dst), |
| 278 excs=(OSError,), |
| 279 name='rename [%s] => [%s]' % (src, dst), |
| 280 printerr=self.print) |
| 281 |
| 233 def RunGit(self, cmd, **kwargs): | 282 def RunGit(self, cmd, **kwargs): |
| 234 """Run git in a subprocess.""" | 283 """Run git in a subprocess.""" |
| 235 cwd = kwargs.setdefault('cwd', self.mirror_path) | 284 cwd = kwargs.setdefault('cwd', self.mirror_path) |
| 236 kwargs.setdefault('print_stdout', False) | 285 kwargs.setdefault('print_stdout', False) |
| 237 kwargs.setdefault('filter_fn', self.print) | 286 kwargs.setdefault('filter_fn', self.print) |
| 238 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy()) | 287 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy()) |
| 239 env.setdefault('GIT_ASKPASS', 'true') | 288 env.setdefault('GIT_ASKPASS', 'true') |
| 240 env.setdefault('SSH_ASKPASS', 'true') | 289 env.setdefault('SSH_ASKPASS', 'true') |
| 241 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd)) | 290 self.print('running "git %s" in "%s"' % (' '.join(cmd), cwd)) |
| 242 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs) | 291 gclient_utils.CheckCallAndFilter([self.git_exe] + cmd, **kwargs) |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 317 with zipfile.ZipFile(filename, 'r') as f: | 366 with zipfile.ZipFile(filename, 'r') as f: |
| 318 f.printdir() | 367 f.printdir() |
| 319 f.extractall(directory) | 368 f.extractall(directory) |
| 320 except Exception as e: | 369 except Exception as e: |
| 321 self.print('Encountered error: %s' % str(e), file=sys.stderr) | 370 self.print('Encountered error: %s' % str(e), file=sys.stderr) |
| 322 retcode = 1 | 371 retcode = 1 |
| 323 else: | 372 else: |
| 324 retcode = 0 | 373 retcode = 0 |
| 325 finally: | 374 finally: |
| 326 # Clean up the downloaded zipfile. | 375 # Clean up the downloaded zipfile. |
| 327 gclient_utils.rm_file_or_tree(tempdir) | 376 # |
| 377 # This is somehow racy on Windows. |
| 378 # Catching OSError because WindowsError isn't portable and |
| 379 # pylint complains. |
| 380 exponential_backoff_retry( |
| 381 lambda: gclient_utils.rm_file_or_tree(tempdir), |
| 382 excs=(OSError,), |
| 383 name='rmtree [%s]' % (tempdir,), |
| 384 printerr=self.print) |
| 328 | 385 |
| 329 if retcode: | 386 if retcode: |
| 330 self.print( | 387 self.print( |
| 331 'Extracting bootstrap zipfile %s failed.\n' | 388 'Extracting bootstrap zipfile %s failed.\n' |
| 332 'Resuming normal operations.' % filename) | 389 'Resuming normal operations.' % filename) |
| 333 return False | 390 return False |
| 334 return True | 391 return True |
| 335 | 392 |
| 336 def exists(self): | 393 def exists(self): |
| 337 return os.path.isfile(os.path.join(self.mirror_path, 'config')) | 394 return os.path.isfile(os.path.join(self.mirror_path, 'config')) |
| (...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 434 # This is a major failure, we need to clean and force a bootstrap. | 491 # This is a major failure, we need to clean and force a bootstrap. |
| 435 gclient_utils.rmtree(rundir) | 492 gclient_utils.rmtree(rundir) |
| 436 self.print(GIT_CACHE_CORRUPT_MESSAGE) | 493 self.print(GIT_CACHE_CORRUPT_MESSAGE) |
| 437 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True) | 494 tempdir = self._ensure_bootstrapped(depth, bootstrap, force=True) |
| 438 assert tempdir | 495 assert tempdir |
| 439 self._fetch(tempdir or self.mirror_path, verbose, depth) | 496 self._fetch(tempdir or self.mirror_path, verbose, depth) |
| 440 finally: | 497 finally: |
| 441 if tempdir: | 498 if tempdir: |
| 442 if os.path.exists(self.mirror_path): | 499 if os.path.exists(self.mirror_path): |
| 443 gclient_utils.rmtree(self.mirror_path) | 500 gclient_utils.rmtree(self.mirror_path) |
| 444 os.rename(tempdir, self.mirror_path) | 501 self.Rename(tempdir, self.mirror_path) |
| 445 if not ignore_lock: | 502 if not ignore_lock: |
| 446 lockfile.unlock() | 503 lockfile.unlock() |
| 447 | 504 |
| 448 def update_bootstrap(self, prune=False): | 505 def update_bootstrap(self, prune=False): |
| 449 # The files are named <git number>.zip | 506 # The files are named <git number>.zip |
| 450 gen_number = subprocess.check_output( | 507 gen_number = subprocess.check_output( |
| 451 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip() | 508 [self.git_exe, 'number', 'master'], cwd=self.mirror_path).strip() |
| 452 # Run Garbage Collect to compress packfile. | 509 # Run Garbage Collect to compress packfile. |
| 453 self.RunGit(['gc', '--prune=all']) | 510 self.RunGit(['gc', '--prune=all']) |
| 454 # Creating a temp file and then deleting it ensures we can use this name. | 511 # Creating a temp file and then deleting it ensures we can use this name. |
| (...skipping 267 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 722 dispatcher = subcommand.CommandDispatcher(__name__) | 779 dispatcher = subcommand.CommandDispatcher(__name__) |
| 723 return dispatcher.execute(OptionParser(), argv) | 780 return dispatcher.execute(OptionParser(), argv) |
| 724 | 781 |
| 725 | 782 |
| 726 if __name__ == '__main__': | 783 if __name__ == '__main__': |
| 727 try: | 784 try: |
| 728 sys.exit(main(sys.argv[1:])) | 785 sys.exit(main(sys.argv[1:])) |
| 729 except KeyboardInterrupt: | 786 except KeyboardInterrupt: |
| 730 sys.stderr.write('interrupted\n') | 787 sys.stderr.write('interrupted\n') |
| 731 sys.exit(1) | 788 sys.exit(1) |
| OLD | NEW |