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 |