blob: dde73def678e8b0a798ab060d837595e4cb6df0f [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Raul Tambreb946b232019-03-26 14:48:46 +000013import functools
14import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000015import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020016import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000018import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000019import platform
Raul Tambreb946b232019-03-26 14:48:46 +000020
21try:
22 import Queue as queue
23except ImportError: # For Py3 compatibility
24 import queue
25
msb@chromium.orgac915bb2009-11-13 17:03:01 +000026import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000027import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000028import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000031import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000032import time
Raul Tambreb946b232019-03-26 14:48:46 +000033
34try:
35 import urlparse
36except ImportError: # For Py3 compatibility
37 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000038
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000039import subprocess2
40
Raul Tambreb946b232019-03-26 14:48:46 +000041if sys.version_info.major == 2:
42 from cStringIO import StringIO
43else:
44 from io import StringIO
45
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000046
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000047RETRY_MAX = 3
48RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000049START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000050
51
borenet@google.com6a9b1682014-03-24 18:35:23 +000052_WARNINGS = []
53
54
szager@chromium.orgff113292014-03-25 06:02:08 +000055# These repos are known to cause OOM errors on 32-bit platforms, due the the
56# very large objects they contain. It is not safe to use threaded index-pack
57# when cloning/fetching them.
58THREADED_INDEX_PACK_BLACKLIST = [
59 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
60]
61
Raul Tambreb946b232019-03-26 14:48:46 +000062"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
63if sys.version_info.major == 2:
64 # We have to use exec to avoid a SyntaxError in Python 3.
65 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
66else:
67 def reraise(typ, value, tb=None):
68 if value is None:
69 value = typ()
70 if value.__traceback__ is not tb:
71 raise value.with_traceback(tb)
72 raise value
73
szager@chromium.orgff113292014-03-25 06:02:08 +000074
maruel@chromium.org66c83e62010-09-07 14:18:45 +000075class Error(Exception):
76 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000077 def __init__(self, msg, *args, **kwargs):
78 index = getattr(threading.currentThread(), 'index', 0)
79 if index:
80 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
81 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000082
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000083
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000084def Elapsed(until=None):
85 if until is None:
86 until = datetime.datetime.now()
87 return str(until - START).partition('.')[0]
88
89
borenet@google.com6a9b1682014-03-24 18:35:23 +000090def PrintWarnings():
91 """Prints any accumulated warnings."""
92 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000093 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000094 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000095 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000096
97
98def AddWarning(msg):
99 """Adds the given warning message to the list of accumulated warnings."""
100 _WARNINGS.append(msg)
101
102
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103def SplitUrlRevision(url):
104 """Splits url and returns a two-tuple: url, rev"""
105 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000106 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000107 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000108 components = re.search(regex, url).groups()
109 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000110 components = url.rsplit('@', 1)
111 if re.match(r'^\w+\@', url) and '@' not in components[0]:
112 components = [url]
113
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000114 if len(components) == 1:
115 components += [None]
116 return tuple(components)
117
118
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000119def IsGitSha(revision):
120 """Returns true if the given string is a valid hex-encoded sha"""
121 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
122
123
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200124def IsFullGitSha(revision):
125 """Returns true if the given string is a valid hex-encoded full sha"""
126 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
127
128
floitsch@google.comeaab7842011-04-28 09:07:58 +0000129def IsDateRevision(revision):
130 """Returns true if the given revision is of the form "{ ... }"."""
131 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
132
133
134def MakeDateRevision(date):
135 """Returns a revision representing the latest revision before the given
136 date."""
137 return "{" + date + "}"
138
139
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000140def SyntaxErrorToError(filename, e):
141 """Raises a gclient_utils.Error exception with the human readable message"""
142 try:
143 # Try to construct a human readable error message
144 if filename:
145 error_message = 'There is a syntax error in %s\n' % filename
146 else:
147 error_message = 'There is a syntax error\n'
148 error_message += 'Line #%s, character %s: "%s"' % (
149 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
150 except:
151 # Something went wrong, re-raise the original exception
152 raise e
153 else:
154 raise Error(error_message)
155
156
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157class PrintableObject(object):
158 def __str__(self):
159 output = ''
160 for i in dir(self):
161 if i.startswith('__'):
162 continue
163 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
164 return output
165
166
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000167def FileRead(filename, mode='rU'):
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000168 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
169 if mode == 'rU' and sys.version_info.major == 3:
170 mode = 'r'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000171 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000172 # codecs.open() has different behavior than open() on python 2.6 so use
173 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000174 s = f.read()
175 try:
176 return s.decode('utf-8')
Raul Tambreb946b232019-03-26 14:48:46 +0000177 # AttributeError is for Py3 compatibility
178 except (UnicodeDecodeError, AttributeError):
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000179 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000180
181
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000182def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000183 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000184 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185
186
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000187@contextlib.contextmanager
188def temporary_directory(**kwargs):
189 tdir = tempfile.mkdtemp(**kwargs)
190 try:
191 yield tdir
192 finally:
193 if tdir:
194 rmtree(tdir)
195
196
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000197def safe_rename(old, new):
198 """Renames a file reliably.
199
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000200 Sometimes os.rename does not work because a dying git process keeps a handle
201 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000202 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000203 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000204 """
205 # roughly 10s
206 retries = 100
207 for i in range(retries):
208 try:
209 os.rename(old, new)
210 break
211 except OSError:
212 if i == (retries - 1):
213 # Give up.
214 raise
215 # retry
216 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
217 time.sleep(0.1)
218
219
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000220def rm_file_or_tree(path):
221 if os.path.isfile(path):
222 os.remove(path)
223 else:
224 rmtree(path)
225
226
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227def rmtree(path):
228 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000229
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000230 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000231
232 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700233 are read-only. We need to be able to force the files to be writable (i.e.,
234 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000235
236 Even with all this, Windows still sometimes fails to delete a file, citing
237 a permission error (maybe something to do with antivirus scans or disk
238 indexing). The best suggestion any of the user forums had was to wait a
239 bit and try again, so we do that too. It's hand-waving, but sometimes it
240 works. :/
241
242 On POSIX systems, things are a little bit simpler. The modes of the files
243 to be deleted doesn't matter, only the modes of the directories containing
244 them are significant. As the directory tree is traversed, each directory
245 has its mode set appropriately before descending into it. This should
246 result in the entire tree being removed, with the possible exception of
247 *path itself, because nothing attempts to change the mode of its parent.
248 Doing so would be hazardous, as it's not a directory slated for removal.
249 In the ordinary case, this is not a problem: for our purposes, the user
250 will never lack write permission on *path's parent.
251 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000253 return
254
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000255 if os.path.islink(path) or not os.path.isdir(path):
256 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000257
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000258 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000259 # Give up and use cmd.exe's rd command.
260 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000261 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000262 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
263 if exitcode == 0:
264 return
265 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000266 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000267 time.sleep(3)
268 raise Exception('Failed to remove path %s' % path)
269
270 # On POSIX systems, we need the x-bit set on the directory to access it,
271 # the r-bit to see its contents, and the w-bit to remove files from it.
272 # The actual modes of the files within the directory is irrelevant.
273 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000275 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000276 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000277
278 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000279 # If fullpath is a symbolic link that points to a directory, isdir will
280 # be True, but we don't want to descend into that as a directory, we just
281 # want to remove the link. Check islink and treat links as ordinary files
282 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000283 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000285 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 # Recurse.
288 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000290 remove(os.rmdir, path)
291
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000292
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000293def safe_makedirs(tree):
294 """Creates the directory in a safe manner.
295
296 Because multiple threads can create these directories concurently, trap the
297 exception and pass on.
298 """
299 count = 0
300 while not os.path.exists(tree):
301 count += 1
302 try:
303 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000304 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000305 # 17 POSIX, 183 Windows
306 if e.errno not in (17, 183):
307 raise
308 if count > 40:
309 # Give up.
310 raise
311
312
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000313def CommandToStr(args):
314 """Converts an arg list into a shell escaped string."""
315 return ' '.join(pipes.quote(arg) for arg in args)
316
317
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318class Wrapper(object):
319 """Wraps an object, acting as a transparent proxy for all properties by
320 default.
321 """
322 def __init__(self, wrapped):
323 self._wrapped = wrapped
324
325 def __getattr__(self, name):
326 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000327
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000328
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000330 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 def __init__(self, wrapped, delay):
332 super(AutoFlush, self).__init__(wrapped)
333 if not hasattr(self, 'lock'):
334 self.lock = threading.Lock()
335 self.__last_flushed_at = time.time()
336 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000337
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 @property
339 def autoflush(self):
340 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000341
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 def write(self, out, *args, **kwargs):
343 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000344 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000346 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000348 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000350 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000352 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000354
355
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000356class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000357 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000358 threads with a NN> prefix.
359 """
360 def __init__(self, wrapped, include_zero=False):
361 super(Annotated, self).__init__(wrapped)
362 if not hasattr(self, 'lock'):
363 self.lock = threading.Lock()
364 self.__output_buffers = {}
365 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000366 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000367
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 @property
369 def annotated(self):
370 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000371
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000372 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000373 # Store as bytes to ensure Unicode characters get output correctly.
374 if not isinstance(out, bytes):
375 out = out.encode('utf-8')
376
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 index = getattr(threading.currentThread(), 'index', 0)
378 if not index and not self.__include_zero:
379 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000380 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000381
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000383 try:
384 # Use a dummy array to hold the string so the code can be lockless.
385 # Strings are immutable, requiring to keep a lock for the whole dictionary
386 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000388 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000389 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000391 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000393
394 # Continue lockless.
395 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000396 while True:
397 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000398 cr_loc = obj[0].find(b'\r')
399 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000400 if cr_loc == lf_loc == -1:
401 break
402 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000403 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000404 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000405 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000406 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000407 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000408 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000409 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000410 obj[0] = remaining
411
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 """Flush buffered output."""
414 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416 try:
417 # Detect threads no longer existing.
418 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000419 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000421 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000423 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000424 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000425 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000426 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000427
428 # Don't keep the lock while writting. Will append \n when it shouldn't.
429 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000430 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000431 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000432 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000433
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000434
435def MakeFileAutoFlush(fileobj, delay=10):
436 autoflush = getattr(fileobj, 'autoflush', None)
437 if autoflush:
438 autoflush.delay = delay
439 return fileobj
440 return AutoFlush(fileobj, delay)
441
442
443def MakeFileAnnotated(fileobj, include_zero=False):
444 if getattr(fileobj, 'annotated', None):
445 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000446 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000447
448
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000449GCLIENT_CHILDREN = []
450GCLIENT_CHILDREN_LOCK = threading.Lock()
451
452
453class GClientChildren(object):
454 @staticmethod
455 def add(popen_obj):
456 with GCLIENT_CHILDREN_LOCK:
457 GCLIENT_CHILDREN.append(popen_obj)
458
459 @staticmethod
460 def remove(popen_obj):
461 with GCLIENT_CHILDREN_LOCK:
462 GCLIENT_CHILDREN.remove(popen_obj)
463
464 @staticmethod
465 def _attemptToKillChildren():
466 global GCLIENT_CHILDREN
467 with GCLIENT_CHILDREN_LOCK:
468 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
469
470 for zombie in zombies:
471 try:
472 zombie.kill()
473 except OSError:
474 pass
475
476 with GCLIENT_CHILDREN_LOCK:
477 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
478
479 @staticmethod
480 def _areZombies():
481 with GCLIENT_CHILDREN_LOCK:
482 return bool(GCLIENT_CHILDREN)
483
484 @staticmethod
485 def KillAllRemainingChildren():
486 GClientChildren._attemptToKillChildren()
487
488 if GClientChildren._areZombies():
489 time.sleep(0.5)
490 GClientChildren._attemptToKillChildren()
491
492 with GCLIENT_CHILDREN_LOCK:
493 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000494 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000495 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000496 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000497
498
Edward Lemur24146be2019-08-01 21:44:52 +0000499def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
500 show_header=False, always_show_header=False, retry=False,
501 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000502 """Runs a command and calls back a filter function if needed.
503
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000504 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000505 print_stdout: If True, the command's stdout is forwarded to stdout.
506 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000507 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000508 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000509 show_header: Whether to display a header before the command output.
510 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000511 retry: If the process exits non-zero, sleep for a brief interval and try
512 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000513
514 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000515
516 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000517 """
Edward Lemur24146be2019-08-01 21:44:52 +0000518 def show_header_if_necessary(needs_header, attempt):
519 """Show the header at most once."""
520 if not needs_header[0]:
521 return
522
523 needs_header[0] = False
524 # Automatically generated header. We only prepend a newline if
525 # always_show_header is false, since it usually indicates there's an
526 # external progress display, and it's better not to clobber it in that case.
527 header = '' if always_show_header else '\n'
528 header += '________ running \'%s\' in \'%s\'' % (
529 ' '.join(args), kwargs.get('cwd', '.'))
530 if attempt:
531 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
532 header += '\n'
533
534 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000535 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
536 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000537 if filter_fn:
538 filter_fn(header)
539
540 def filter_line(command_output, line_start):
541 """Extract the last line from command output and filter it."""
542 if not filter_fn or line_start is None:
543 return
544 command_output.seek(line_start)
545 filter_fn(command_output.read().decode('utf-8'))
546
547 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
548 # byte inputs and sys.stdout.buffer must be used instead.
549 if print_stdout:
550 sys.stdout.flush()
551 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
552 else:
553 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000554
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000555 sleep_interval = RETRY_INITIAL_SLEEP
556 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000557 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000558 kid = subprocess2.Popen(
559 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
560 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000561
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000562 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000563
Edward Lemur24146be2019-08-01 21:44:52 +0000564 # Store the output of the command regardless of the value of print_stdout or
565 # filter_fn.
566 command_output = io.BytesIO()
567
568 # Passed as a list for "by ref" semantics.
569 needs_header = [show_header]
570 if always_show_header:
571 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000572
573 # Also, we need to forward stdout to prevent weird re-ordering of output.
574 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700575 # normally buffering is done for each line, but if the process requests
576 # input, no end-of-line character is output after the prompt and it would
577 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000578 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000579 line_start = None
580 while True:
581 in_byte = kid.stdout.read(1)
582 is_newline = in_byte in (b'\n', b'\r')
583 if not in_byte:
584 break
585
586 show_header_if_necessary(needs_header, attempt)
587
588 if is_newline:
589 filter_line(command_output, line_start)
590 line_start = None
591 elif line_start is None:
592 line_start = command_output.tell()
593
594 stdout_write(in_byte)
595 command_output.write(in_byte)
596
597 # Flush the rest of buffered output.
598 sys.stdout.flush()
599 if line_start is not None:
600 filter_line(command_output, line_start)
601
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000603 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000604
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000605 # Don't put this in a 'finally,' since the child may still run if we get
606 # an exception.
607 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000608
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000609 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000610 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000611 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000612
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000613 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000614 return command_output.getvalue()
615
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000616 if not retry:
617 break
Edward Lemur24146be2019-08-01 21:44:52 +0000618
Raul Tambreb946b232019-03-26 14:48:46 +0000619 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
620 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000621 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000622 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000623
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000624 raise subprocess2.CalledProcessError(
625 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000626
627
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628class GitFilter(object):
629 """A filter_fn implementation for quieting down git output messages.
630
631 Allows a custom function to skip certain lines (predicate), and will throttle
632 the output of percentage completed lines to only output every X seconds.
633 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000634 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000635
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000636 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000637 """
638 Args:
639 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
640 XX% complete messages) to only be printed at least |time_throttle|
641 seconds apart.
642 predicate (f(line)): An optional function which is invoked for every line.
643 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000644 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000645 """
Edward Lemur24146be2019-08-01 21:44:52 +0000646 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647 self.last_time = 0
648 self.time_throttle = time_throttle
649 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000650 self.out_fh = out_fh or sys.stdout
651 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000652
653 def __call__(self, line):
654 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000655 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000656 if esc > -1:
657 line = line[:esc]
658 if self.predicate and not self.predicate(line):
659 return
660 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000661 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000662 if match:
663 if match.group(1) != self.progress_prefix:
664 self.progress_prefix = match.group(1)
665 elif now - self.last_time < self.time_throttle:
666 return
667 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000668 if not self.first_line:
669 self.out_fh.write('[%s] ' % Elapsed())
670 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000671 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000672
673
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000674def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000675 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000676
rcui@google.com13595ff2011-10-13 01:25:07 +0000677 Returns nearest upper-level directory with the passed in file.
678 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000679 if not path:
680 path = os.getcwd()
681 path = os.path.realpath(path)
682 while True:
683 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000684 if os.path.exists(file_path):
685 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000686 (new_path, _) = os.path.split(path)
687 if new_path == path:
688 return None
689 path = new_path
690
691
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000692def GetMacWinOrLinux():
693 """Returns 'mac', 'win', or 'linux', matching the current platform."""
694 if sys.platform.startswith(('cygwin', 'win')):
695 return 'win'
696 elif sys.platform.startswith('linux'):
697 return 'linux'
698 elif sys.platform == 'darwin':
699 return 'mac'
700 raise Error('Unknown platform: ' + sys.platform)
701
702
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000703def GetGClientRootAndEntries(path=None):
704 """Returns the gclient root and the dict of entries."""
705 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000706 root = FindFileUpwards(config_file, path)
707 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000708 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000709 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000710 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000711 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000712 with open(config_path) as config:
713 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000714 config_dir = os.path.dirname(config_path)
715 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000716
717
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000718def lockedmethod(method):
719 """Method decorator that holds self.lock for the duration of the call."""
720 def inner(self, *args, **kwargs):
721 try:
722 try:
723 self.lock.acquire()
724 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000725 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000726 raise
727 return method(self, *args, **kwargs)
728 finally:
729 self.lock.release()
730 return inner
731
732
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000733class WorkItem(object):
734 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000735 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
736 # As a workaround, use a single lock. Yep you read it right. Single lock for
737 # all the 100 objects.
738 lock = threading.Lock()
739
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000740 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000741 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000742 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000743 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000744 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700745 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000746
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000747 def run(self, work_queue):
748 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000749 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000750 pass
751
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000752 @property
753 def name(self):
754 return self._name
755
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000756
757class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000758 """Runs a set of WorkItem that have interdependencies and were WorkItem are
759 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000760
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200761 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000762 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000763
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000764 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000765 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000766 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 """jobs specifies the number of concurrent tasks to allow. progress is a
768 Progress instance."""
769 # Set when a thread is done or a new item is enqueued.
770 self.ready_cond = threading.Condition()
771 # Maximum number of concurrent tasks.
772 self.jobs = jobs
773 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000774 self.queued = []
775 # List of strings representing each Dependency.name that was run.
776 self.ran = []
777 # List of items currently running.
778 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000779 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000780 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000781 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000782 self.progress = progress
783 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000784 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000785
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000786 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000787 self.verbose = verbose
788 self.last_join = None
789 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000790
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791 def enqueue(self, d):
792 """Enqueue one Dependency to be executed later once its requirements are
793 satisfied.
794 """
795 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000796 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000797 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000798 self.queued.append(d)
799 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000800 if self.jobs == 1:
801 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 logging.debug('enqueued(%s)' % d.name)
803 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000804 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000805 self.progress.update(0)
806 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000807 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000808 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000809
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000810 def out_cb(self, _):
811 self.last_subproc_output = datetime.datetime.now()
812 return True
813
814 @staticmethod
815 def format_task_output(task, comment=''):
816 if comment:
817 comment = ' (%s)' % comment
818 if task.start and task.finish:
819 elapsed = ' (Elapsed: %s)' % (
820 str(task.finish - task.start).partition('.')[0])
821 else:
822 elapsed = ''
823 return """
824%s%s%s
825----------------------------------------
826%s
827----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000828 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829
hinoka885e5b12016-06-08 14:40:09 -0700830 def _is_conflict(self, job):
831 """Checks to see if a job will conflict with another running job."""
832 for running_job in self.running:
833 for used_resource in running_job.item.resources:
834 logging.debug('Checking resource %s' % used_resource)
835 if used_resource in job.resources:
836 return True
837 return False
838
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000839 def flush(self, *args, **kwargs):
840 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000841 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000842 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000843 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 while True:
846 # Check for task to run first, then wait.
847 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000848 if not self.exceptions.empty():
849 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000851 self._flush_terminated_threads()
852 if (not self.queued and not self.running or
853 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000854 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000856
857 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000858 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000860 if (self.ignore_requirements or
861 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700862 if not self._is_conflict(self.queued[i]):
863 # Start one work item: all its requirements are satisfied.
864 self._run_one_task(self.queued.pop(i), args, kwargs)
865 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000866 else:
867 # Couldn't find an item that could run. Break out the outher loop.
868 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000869
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000870 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000871 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000872 break
873 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000874 try:
875 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000876 # If we haven't printed to terminal for a while, but we have received
877 # spew from a suprocess, let the user know we're still progressing.
878 now = datetime.datetime.now()
879 if (now - self.last_join > datetime.timedelta(seconds=60) and
880 self.last_subproc_output > self.last_join):
881 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000882 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000883 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000884 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000885 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000886 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000887 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000888 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000889 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000890 except KeyboardInterrupt:
891 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000892 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000893 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000894 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
895 self.ran), len(self.running)),
896 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000897 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000898 print(
899 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
900 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000901 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000902 print(
903 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000904 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000905 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000906 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000907 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000908
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000909 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000910 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000911 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000912 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000913 # To get back the stack location correctly, the raise a, b, c form must be
914 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000915 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000916 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
917 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000918 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000919 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000920
maruel@chromium.org3742c842010-09-09 19:27:14 +0000921 def _flush_terminated_threads(self):
922 """Flush threads that have terminated."""
923 running = self.running
924 self.running = []
925 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000926 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000927 self.running.append(t)
928 else:
929 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000930 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000931 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000932 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000933 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000934 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000935 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000936 if t.item.name in self.ran:
937 raise Error(
938 'gclient is confused, "%s" is already in "%s"' % (
939 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000940 if not t.item.name in self.ran:
941 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000942
943 def _run_one_task(self, task_item, args, kwargs):
944 if self.jobs > 1:
945 # Start the thread.
946 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000947 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000948 self.running.append(new_thread)
949 new_thread.start()
950 else:
951 # Run the 'thread' inside the main thread. Don't try to catch any
952 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 try:
954 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000955 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000956 task_item.run(*args, **kwargs)
957 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000958 print(
959 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000960 self.ran.append(task_item.name)
961 if self.verbose:
962 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000963 print('')
964 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000965 if self.progress:
966 self.progress.update(1, ', '.join(t.item.name for t in self.running))
967 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000968 print(
969 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 raise
971 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000972 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000973 raise
974
maruel@chromium.org3742c842010-09-09 19:27:14 +0000975
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000976 class _Worker(threading.Thread):
977 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000978 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000979 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000980 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000981 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000982 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000983 self.args = args
984 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000985 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000986
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000987 def run(self):
988 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000989 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000990 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000991 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000992 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000993 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000994 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000995 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000996 print(
997 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000998 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000999 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001000 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001001 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001002 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001003 except Exception:
1004 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001005 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001006 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001007 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001008 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001009 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001010 work_queue.ready_cond.acquire()
1011 try:
1012 work_queue.ready_cond.notifyAll()
1013 finally:
1014 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001015
1016
agable92bec4f2016-08-24 09:27:27 -07001017def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001018 """Returns the most plausible editor to use.
1019
1020 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001021 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001022 - core.editor git configuration variable (if supplied by git-cl)
1023 - VISUAL environment variable
1024 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001025 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001026
1027 In the case of git-cl, this matches git's behaviour, except that it does not
1028 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001029 """
agable92bec4f2016-08-24 09:27:27 -07001030 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001031 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001032 editor = os.environ.get('VISUAL')
1033 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001034 editor = os.environ.get('EDITOR')
1035 if not editor:
1036 if sys.platform.startswith('win'):
1037 editor = 'notepad'
1038 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001039 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001040 return editor
1041
1042
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001043def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001044 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001045 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001046 # Make sure CRLF is handled properly by requiring none.
1047 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001048 print(
1049 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001050 fileobj = os.fdopen(file_handle, 'w')
1051 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001052 content = re.sub('\r?\n', '\n', content)
1053 # Some editors complain when the file doesn't end in \n.
1054 if not content.endswith('\n'):
1055 content += '\n'
1056 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001057 fileobj.close()
1058
1059 try:
agable92bec4f2016-08-24 09:27:27 -07001060 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001061 if not editor:
1062 return None
1063 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001064 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1065 # Msysgit requires the usage of 'env' to be present.
1066 cmd = 'env ' + cmd
1067 try:
1068 # shell=True to allow the shell to handle all forms of quotes in
1069 # $EDITOR.
1070 subprocess2.check_call(cmd, shell=True)
1071 except subprocess2.CalledProcessError:
1072 return None
1073 return FileRead(filename)
1074 finally:
1075 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001076
1077
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001078def UpgradeToHttps(url):
1079 """Upgrades random urls to https://.
1080
1081 Do not touch unknown urls like ssh:// or git://.
1082 Do not touch http:// urls with a port number,
1083 Fixes invalid GAE url.
1084 """
1085 if not url:
1086 return url
1087 if not re.match(r'[a-z\-]+\://.*', url):
1088 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1089 # relative url and will use http:///foo. Note that it defaults to http://
1090 # for compatibility with naked url like "localhost:8080".
1091 url = 'http://%s' % url
1092 parsed = list(urlparse.urlparse(url))
1093 # Do not automatically upgrade http to https if a port number is provided.
1094 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1095 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001096 return urlparse.urlunparse(parsed)
1097
1098
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001099def ParseCodereviewSettingsContent(content):
1100 """Process a codereview.settings file properly."""
1101 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1102 try:
1103 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1104 except ValueError:
1105 raise Error(
1106 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001107 def fix_url(key):
1108 if keyvals.get(key):
1109 keyvals[key] = UpgradeToHttps(keyvals[key])
1110 fix_url('CODE_REVIEW_SERVER')
1111 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001112 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001113
1114
1115def NumLocalCpus():
1116 """Returns the number of processors.
1117
dnj@chromium.org530523b2015-01-07 19:54:57 +00001118 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1119 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1120 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001121 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001122 # Surround the entire thing in try/except; no failure here should stop gclient
1123 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001124 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001125 # Use multiprocessing to get CPU count. This may raise
1126 # NotImplementedError.
1127 try:
1128 import multiprocessing
1129 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001130 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001131 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001132 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001133 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1134 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1135
1136 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1137 if 'NUMBER_OF_PROCESSORS' in os.environ:
1138 return int(os.environ['NUMBER_OF_PROCESSORS'])
1139 except Exception as e:
1140 logging.exception("Exception raised while probing CPU count: %s", e)
1141
1142 logging.debug('Failed to get CPU count. Defaulting to 1.')
1143 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001144
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001145
szager@chromium.orgfc616382014-03-18 20:32:04 +00001146def DefaultDeltaBaseCacheLimit():
1147 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1148
1149 The primary constraint is the address space of virtual memory. The cache
1150 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1151 parameter is set too high.
1152 """
1153 if platform.architecture()[0].startswith('64'):
1154 return '2g'
1155 else:
1156 return '512m'
1157
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001158
szager@chromium.orgff113292014-03-25 06:02:08 +00001159def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001160 """Return reasonable default values for configuring git-index-pack.
1161
1162 Experiments suggest that higher values for pack.threads don't improve
1163 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001164 cache_limit = DefaultDeltaBaseCacheLimit()
1165 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1166 if url in THREADED_INDEX_PACK_BLACKLIST:
1167 result.extend(['-c', 'pack.threads=1'])
1168 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001169
1170
1171def FindExecutable(executable):
1172 """This mimics the "which" utility."""
1173 path_folders = os.environ.get('PATH').split(os.pathsep)
1174
1175 for path_folder in path_folders:
1176 target = os.path.join(path_folder, executable)
1177 # Just incase we have some ~/blah paths.
1178 target = os.path.abspath(os.path.expanduser(target))
1179 if os.path.isfile(target) and os.access(target, os.X_OK):
1180 return target
1181 if sys.platform.startswith('win'):
1182 for suffix in ('.bat', '.cmd', '.exe'):
1183 alt_target = target + suffix
1184 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1185 return alt_target
1186 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001187
1188
1189def freeze(obj):
1190 """Takes a generic object ``obj``, and returns an immutable version of it.
1191
1192 Supported types:
1193 * dict / OrderedDict -> FrozenDict
1194 * list -> tuple
1195 * set -> frozenset
1196 * any object with a working __hash__ implementation (assumes that hashable
1197 means immutable)
1198
1199 Will raise TypeError if you pass an object which is not hashable.
1200 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001201 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001202 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001203 elif isinstance(obj, (list, tuple)):
1204 return tuple(freeze(i) for i in obj)
1205 elif isinstance(obj, set):
1206 return frozenset(freeze(i) for i in obj)
1207 else:
1208 hash(obj)
1209 return obj
1210
1211
1212class FrozenDict(collections.Mapping):
1213 """An immutable OrderedDict.
1214
1215 Modified From: http://stackoverflow.com/a/2704866
1216 """
1217 def __init__(self, *args, **kwargs):
1218 self._d = collections.OrderedDict(*args, **kwargs)
1219
1220 # Calculate the hash immediately so that we know all the items are
1221 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001222 self._hash = functools.reduce(
1223 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001224
1225 def __eq__(self, other):
1226 if not isinstance(other, collections.Mapping):
1227 return NotImplemented
1228 if self is other:
1229 return True
1230 if len(self) != len(other):
1231 return False
1232 for k, v in self.iteritems():
1233 if k not in other or other[k] != v:
1234 return False
1235 return True
1236
1237 def __iter__(self):
1238 return iter(self._d)
1239
1240 def __len__(self):
1241 return len(self._d)
1242
1243 def __getitem__(self, key):
1244 return self._d[key]
1245
1246 def __hash__(self):
1247 return self._hash
1248
1249 def __repr__(self):
1250 return 'FrozenDict(%r)' % (self._d.items(),)