blob: 3a6bc1fa8401d7751d17990b210ab5a64ffc36ea [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
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000366
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000367 @property
368 def annotated(self):
369 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000370
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371 def write(self, out):
372 index = getattr(threading.currentThread(), 'index', 0)
373 if not index and not self.__include_zero:
374 # Unindexed threads aren't buffered.
375 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000376
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000378 try:
379 # Use a dummy array to hold the string so the code can be lockless.
380 # Strings are immutable, requiring to keep a lock for the whole dictionary
381 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 if not index in self.__output_buffers:
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000383 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000386 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388
389 # Continue lockless.
390 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000391 while True:
392 # TODO(agable): find both of these with a single pass.
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000393 cr_loc = obj[0].find('\r')
394 lf_loc = obj[0].find('\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000395 if cr_loc == lf_loc == -1:
396 break
397 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000398 line, remaining = obj[0].split('\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000399 if line:
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000400 self._wrapped.write('%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000401 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000402 line, remaining = obj[0].split('\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000403 if line:
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000404 self._wrapped.write('%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 obj[0] = remaining
406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000408 """Flush buffered output."""
409 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000410 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000411 try:
412 # Detect threads no longer existing.
413 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000414 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000418 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000419 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000420 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422
423 # Don't keep the lock while writting. Will append \n when it shouldn't.
424 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000425 if orphan[1]:
Bob Haarmaneeafa0e2019-10-07 21:16:52 +0000426 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000427 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000428
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000429
430def MakeFileAutoFlush(fileobj, delay=10):
431 autoflush = getattr(fileobj, 'autoflush', None)
432 if autoflush:
433 autoflush.delay = delay
434 return fileobj
435 return AutoFlush(fileobj, delay)
436
437
438def MakeFileAnnotated(fileobj, include_zero=False):
439 if getattr(fileobj, 'annotated', None):
440 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000441 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000442
443
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000444GCLIENT_CHILDREN = []
445GCLIENT_CHILDREN_LOCK = threading.Lock()
446
447
448class GClientChildren(object):
449 @staticmethod
450 def add(popen_obj):
451 with GCLIENT_CHILDREN_LOCK:
452 GCLIENT_CHILDREN.append(popen_obj)
453
454 @staticmethod
455 def remove(popen_obj):
456 with GCLIENT_CHILDREN_LOCK:
457 GCLIENT_CHILDREN.remove(popen_obj)
458
459 @staticmethod
460 def _attemptToKillChildren():
461 global GCLIENT_CHILDREN
462 with GCLIENT_CHILDREN_LOCK:
463 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
464
465 for zombie in zombies:
466 try:
467 zombie.kill()
468 except OSError:
469 pass
470
471 with GCLIENT_CHILDREN_LOCK:
472 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
473
474 @staticmethod
475 def _areZombies():
476 with GCLIENT_CHILDREN_LOCK:
477 return bool(GCLIENT_CHILDREN)
478
479 @staticmethod
480 def KillAllRemainingChildren():
481 GClientChildren._attemptToKillChildren()
482
483 if GClientChildren._areZombies():
484 time.sleep(0.5)
485 GClientChildren._attemptToKillChildren()
486
487 with GCLIENT_CHILDREN_LOCK:
488 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000489 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000490 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000491 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000492
493
Edward Lemur24146be2019-08-01 21:44:52 +0000494def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
495 show_header=False, always_show_header=False, retry=False,
496 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000497 """Runs a command and calls back a filter function if needed.
498
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000499 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000500 print_stdout: If True, the command's stdout is forwarded to stdout.
501 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000502 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000503 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000504 show_header: Whether to display a header before the command output.
505 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000506 retry: If the process exits non-zero, sleep for a brief interval and try
507 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000508
509 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000510
511 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000512 """
Edward Lemur24146be2019-08-01 21:44:52 +0000513 def show_header_if_necessary(needs_header, attempt):
514 """Show the header at most once."""
515 if not needs_header[0]:
516 return
517
518 needs_header[0] = False
519 # Automatically generated header. We only prepend a newline if
520 # always_show_header is false, since it usually indicates there's an
521 # external progress display, and it's better not to clobber it in that case.
522 header = '' if always_show_header else '\n'
523 header += '________ running \'%s\' in \'%s\'' % (
524 ' '.join(args), kwargs.get('cwd', '.'))
525 if attempt:
526 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
527 header += '\n'
528
529 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000530 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
531 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000532 if filter_fn:
533 filter_fn(header)
534
535 def filter_line(command_output, line_start):
536 """Extract the last line from command output and filter it."""
537 if not filter_fn or line_start is None:
538 return
539 command_output.seek(line_start)
540 filter_fn(command_output.read().decode('utf-8'))
541
542 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
543 # byte inputs and sys.stdout.buffer must be used instead.
544 if print_stdout:
545 sys.stdout.flush()
546 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
547 else:
548 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000549
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000550 sleep_interval = RETRY_INITIAL_SLEEP
551 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000552 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000553 kid = subprocess2.Popen(
554 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
555 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000556
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000557 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000558
Edward Lemur24146be2019-08-01 21:44:52 +0000559 # Store the output of the command regardless of the value of print_stdout or
560 # filter_fn.
561 command_output = io.BytesIO()
562
563 # Passed as a list for "by ref" semantics.
564 needs_header = [show_header]
565 if always_show_header:
566 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567
568 # Also, we need to forward stdout to prevent weird re-ordering of output.
569 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700570 # normally buffering is done for each line, but if the process requests
571 # input, no end-of-line character is output after the prompt and it would
572 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000573 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000574 line_start = None
575 while True:
576 in_byte = kid.stdout.read(1)
577 is_newline = in_byte in (b'\n', b'\r')
578 if not in_byte:
579 break
580
581 show_header_if_necessary(needs_header, attempt)
582
583 if is_newline:
584 filter_line(command_output, line_start)
585 line_start = None
586 elif line_start is None:
587 line_start = command_output.tell()
588
589 stdout_write(in_byte)
590 command_output.write(in_byte)
591
592 # Flush the rest of buffered output.
593 sys.stdout.flush()
594 if line_start is not None:
595 filter_line(command_output, line_start)
596
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000597 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000598 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000599
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000600 # Don't put this in a 'finally,' since the child may still run if we get
601 # an exception.
602 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000603
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000604 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000605 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000606 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000607
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000608 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000609 return command_output.getvalue()
610
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000611 if not retry:
612 break
Edward Lemur24146be2019-08-01 21:44:52 +0000613
Raul Tambreb946b232019-03-26 14:48:46 +0000614 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
615 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000616 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000617 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000618
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000619 raise subprocess2.CalledProcessError(
620 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000621
622
agable@chromium.org5a306a22014-02-24 22:13:59 +0000623class GitFilter(object):
624 """A filter_fn implementation for quieting down git output messages.
625
626 Allows a custom function to skip certain lines (predicate), and will throttle
627 the output of percentage completed lines to only output every X seconds.
628 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000629 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000630
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000631 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000632 """
633 Args:
634 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
635 XX% complete messages) to only be printed at least |time_throttle|
636 seconds apart.
637 predicate (f(line)): An optional function which is invoked for every line.
638 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000639 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000640 """
Edward Lemur24146be2019-08-01 21:44:52 +0000641 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642 self.last_time = 0
643 self.time_throttle = time_throttle
644 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000645 self.out_fh = out_fh or sys.stdout
646 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647
648 def __call__(self, line):
649 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000650 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000651 if esc > -1:
652 line = line[:esc]
653 if self.predicate and not self.predicate(line):
654 return
655 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000656 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000657 if match:
658 if match.group(1) != self.progress_prefix:
659 self.progress_prefix = match.group(1)
660 elif now - self.last_time < self.time_throttle:
661 return
662 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000663 if not self.first_line:
664 self.out_fh.write('[%s] ' % Elapsed())
665 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000666 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667
668
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000669def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000670 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000671
rcui@google.com13595ff2011-10-13 01:25:07 +0000672 Returns nearest upper-level directory with the passed in file.
673 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000674 if not path:
675 path = os.getcwd()
676 path = os.path.realpath(path)
677 while True:
678 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000679 if os.path.exists(file_path):
680 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000681 (new_path, _) = os.path.split(path)
682 if new_path == path:
683 return None
684 path = new_path
685
686
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000687def GetMacWinOrLinux():
688 """Returns 'mac', 'win', or 'linux', matching the current platform."""
689 if sys.platform.startswith(('cygwin', 'win')):
690 return 'win'
691 elif sys.platform.startswith('linux'):
692 return 'linux'
693 elif sys.platform == 'darwin':
694 return 'mac'
695 raise Error('Unknown platform: ' + sys.platform)
696
697
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000698def GetGClientRootAndEntries(path=None):
699 """Returns the gclient root and the dict of entries."""
700 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000701 root = FindFileUpwards(config_file, path)
702 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000703 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000704 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000705 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000706 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000707 with open(config_path) as config:
708 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000709 config_dir = os.path.dirname(config_path)
710 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000711
712
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000713def lockedmethod(method):
714 """Method decorator that holds self.lock for the duration of the call."""
715 def inner(self, *args, **kwargs):
716 try:
717 try:
718 self.lock.acquire()
719 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000720 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000721 raise
722 return method(self, *args, **kwargs)
723 finally:
724 self.lock.release()
725 return inner
726
727
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000728class WorkItem(object):
729 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000730 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
731 # As a workaround, use a single lock. Yep you read it right. Single lock for
732 # all the 100 objects.
733 lock = threading.Lock()
734
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000735 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000736 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000737 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000738 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000739 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700740 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000741
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000742 def run(self, work_queue):
743 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000744 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000745 pass
746
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000747 @property
748 def name(self):
749 return self._name
750
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000751
752class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 """Runs a set of WorkItem that have interdependencies and were WorkItem are
754 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000755
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200756 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000757 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000758
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000759 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000760 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000761 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000762 """jobs specifies the number of concurrent tasks to allow. progress is a
763 Progress instance."""
764 # Set when a thread is done or a new item is enqueued.
765 self.ready_cond = threading.Condition()
766 # Maximum number of concurrent tasks.
767 self.jobs = jobs
768 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000769 self.queued = []
770 # List of strings representing each Dependency.name that was run.
771 self.ran = []
772 # List of items currently running.
773 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000774 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000775 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000776 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000777 self.progress = progress
778 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000779 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000781 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000782 self.verbose = verbose
783 self.last_join = None
784 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000785
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 def enqueue(self, d):
787 """Enqueue one Dependency to be executed later once its requirements are
788 satisfied.
789 """
790 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000791 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000792 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000793 self.queued.append(d)
794 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000795 if self.jobs == 1:
796 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 logging.debug('enqueued(%s)' % d.name)
798 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000799 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000800 self.progress.update(0)
801 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000803 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000804
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000805 def out_cb(self, _):
806 self.last_subproc_output = datetime.datetime.now()
807 return True
808
809 @staticmethod
810 def format_task_output(task, comment=''):
811 if comment:
812 comment = ' (%s)' % comment
813 if task.start and task.finish:
814 elapsed = ' (Elapsed: %s)' % (
815 str(task.finish - task.start).partition('.')[0])
816 else:
817 elapsed = ''
818 return """
819%s%s%s
820----------------------------------------
821%s
822----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000823 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000824
hinoka885e5b12016-06-08 14:40:09 -0700825 def _is_conflict(self, job):
826 """Checks to see if a job will conflict with another running job."""
827 for running_job in self.running:
828 for used_resource in running_job.item.resources:
829 logging.debug('Checking resource %s' % used_resource)
830 if used_resource in job.resources:
831 return True
832 return False
833
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000834 def flush(self, *args, **kwargs):
835 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000836 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000837 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000838 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000839 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000840 while True:
841 # Check for task to run first, then wait.
842 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000843 if not self.exceptions.empty():
844 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846 self._flush_terminated_threads()
847 if (not self.queued and not self.running or
848 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000849 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000851
852 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000853 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000854 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000855 if (self.ignore_requirements or
856 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700857 if not self._is_conflict(self.queued[i]):
858 # Start one work item: all its requirements are satisfied.
859 self._run_one_task(self.queued.pop(i), args, kwargs)
860 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000861 else:
862 # Couldn't find an item that could run. Break out the outher loop.
863 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000864
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000866 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000867 break
868 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000869 try:
870 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000871 # If we haven't printed to terminal for a while, but we have received
872 # spew from a suprocess, let the user know we're still progressing.
873 now = datetime.datetime.now()
874 if (now - self.last_join > datetime.timedelta(seconds=60) and
875 self.last_subproc_output > self.last_join):
876 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000877 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000878 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000879 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000880 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000881 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000882 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000883 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000884 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000885 except KeyboardInterrupt:
886 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000887 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000888 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000889 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
890 self.ran), len(self.running)),
891 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000892 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000893 print(
894 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
895 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000896 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000897 print(
898 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000899 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000901 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000903
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000904 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000905 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000907 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000908 # To get back the stack location correctly, the raise a, b, c form must be
909 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000910 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000911 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
912 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000913 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000914 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000915
maruel@chromium.org3742c842010-09-09 19:27:14 +0000916 def _flush_terminated_threads(self):
917 """Flush threads that have terminated."""
918 running = self.running
919 self.running = []
920 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000921 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000922 self.running.append(t)
923 else:
924 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000925 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000926 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000927 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000928 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000929 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000930 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000931 if t.item.name in self.ran:
932 raise Error(
933 'gclient is confused, "%s" is already in "%s"' % (
934 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000935 if not t.item.name in self.ran:
936 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000937
938 def _run_one_task(self, task_item, args, kwargs):
939 if self.jobs > 1:
940 # Start the thread.
941 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000942 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000943 self.running.append(new_thread)
944 new_thread.start()
945 else:
946 # Run the 'thread' inside the main thread. Don't try to catch any
947 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000948 try:
949 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000950 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 task_item.run(*args, **kwargs)
952 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000953 print(
954 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000955 self.ran.append(task_item.name)
956 if self.verbose:
957 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000958 print('')
959 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000960 if self.progress:
961 self.progress.update(1, ', '.join(t.item.name for t in self.running))
962 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000963 print(
964 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000965 raise
966 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000967 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000968 raise
969
maruel@chromium.org3742c842010-09-09 19:27:14 +0000970
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000971 class _Worker(threading.Thread):
972 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000973 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000974 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000975 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000976 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000977 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000978 self.args = args
979 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000980 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000981
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000982 def run(self):
983 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000984 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000985 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000986 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000987 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000988 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000989 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000991 print(
992 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000993 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000994 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000995 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000996 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000997 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000998 except Exception:
999 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001000 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001001 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001002 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001003 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001004 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001005 work_queue.ready_cond.acquire()
1006 try:
1007 work_queue.ready_cond.notifyAll()
1008 finally:
1009 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001010
1011
agable92bec4f2016-08-24 09:27:27 -07001012def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001013 """Returns the most plausible editor to use.
1014
1015 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001016 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001017 - core.editor git configuration variable (if supplied by git-cl)
1018 - VISUAL environment variable
1019 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001020 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001021
1022 In the case of git-cl, this matches git's behaviour, except that it does not
1023 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001024 """
agable92bec4f2016-08-24 09:27:27 -07001025 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001026 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001027 editor = os.environ.get('VISUAL')
1028 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001029 editor = os.environ.get('EDITOR')
1030 if not editor:
1031 if sys.platform.startswith('win'):
1032 editor = 'notepad'
1033 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001034 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001035 return editor
1036
1037
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001038def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001040 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001041 # Make sure CRLF is handled properly by requiring none.
1042 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001043 print(
1044 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001045 fileobj = os.fdopen(file_handle, 'w')
1046 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001047 content = re.sub('\r?\n', '\n', content)
1048 # Some editors complain when the file doesn't end in \n.
1049 if not content.endswith('\n'):
1050 content += '\n'
1051 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001052 fileobj.close()
1053
1054 try:
agable92bec4f2016-08-24 09:27:27 -07001055 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001056 if not editor:
1057 return None
1058 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001059 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1060 # Msysgit requires the usage of 'env' to be present.
1061 cmd = 'env ' + cmd
1062 try:
1063 # shell=True to allow the shell to handle all forms of quotes in
1064 # $EDITOR.
1065 subprocess2.check_call(cmd, shell=True)
1066 except subprocess2.CalledProcessError:
1067 return None
1068 return FileRead(filename)
1069 finally:
1070 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001071
1072
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001073def UpgradeToHttps(url):
1074 """Upgrades random urls to https://.
1075
1076 Do not touch unknown urls like ssh:// or git://.
1077 Do not touch http:// urls with a port number,
1078 Fixes invalid GAE url.
1079 """
1080 if not url:
1081 return url
1082 if not re.match(r'[a-z\-]+\://.*', url):
1083 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1084 # relative url and will use http:///foo. Note that it defaults to http://
1085 # for compatibility with naked url like "localhost:8080".
1086 url = 'http://%s' % url
1087 parsed = list(urlparse.urlparse(url))
1088 # Do not automatically upgrade http to https if a port number is provided.
1089 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1090 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001091 return urlparse.urlunparse(parsed)
1092
1093
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001094def ParseCodereviewSettingsContent(content):
1095 """Process a codereview.settings file properly."""
1096 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1097 try:
1098 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1099 except ValueError:
1100 raise Error(
1101 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001102 def fix_url(key):
1103 if keyvals.get(key):
1104 keyvals[key] = UpgradeToHttps(keyvals[key])
1105 fix_url('CODE_REVIEW_SERVER')
1106 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001107 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001108
1109
1110def NumLocalCpus():
1111 """Returns the number of processors.
1112
dnj@chromium.org530523b2015-01-07 19:54:57 +00001113 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1114 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1115 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001116 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001117 # Surround the entire thing in try/except; no failure here should stop gclient
1118 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001119 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001120 # Use multiprocessing to get CPU count. This may raise
1121 # NotImplementedError.
1122 try:
1123 import multiprocessing
1124 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001125 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001126 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001127 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001128 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1129 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1130
1131 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1132 if 'NUMBER_OF_PROCESSORS' in os.environ:
1133 return int(os.environ['NUMBER_OF_PROCESSORS'])
1134 except Exception as e:
1135 logging.exception("Exception raised while probing CPU count: %s", e)
1136
1137 logging.debug('Failed to get CPU count. Defaulting to 1.')
1138 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001139
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001140
szager@chromium.orgfc616382014-03-18 20:32:04 +00001141def DefaultDeltaBaseCacheLimit():
1142 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1143
1144 The primary constraint is the address space of virtual memory. The cache
1145 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1146 parameter is set too high.
1147 """
1148 if platform.architecture()[0].startswith('64'):
1149 return '2g'
1150 else:
1151 return '512m'
1152
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001153
szager@chromium.orgff113292014-03-25 06:02:08 +00001154def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001155 """Return reasonable default values for configuring git-index-pack.
1156
1157 Experiments suggest that higher values for pack.threads don't improve
1158 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001159 cache_limit = DefaultDeltaBaseCacheLimit()
1160 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1161 if url in THREADED_INDEX_PACK_BLACKLIST:
1162 result.extend(['-c', 'pack.threads=1'])
1163 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001164
1165
1166def FindExecutable(executable):
1167 """This mimics the "which" utility."""
1168 path_folders = os.environ.get('PATH').split(os.pathsep)
1169
1170 for path_folder in path_folders:
1171 target = os.path.join(path_folder, executable)
1172 # Just incase we have some ~/blah paths.
1173 target = os.path.abspath(os.path.expanduser(target))
1174 if os.path.isfile(target) and os.access(target, os.X_OK):
1175 return target
1176 if sys.platform.startswith('win'):
1177 for suffix in ('.bat', '.cmd', '.exe'):
1178 alt_target = target + suffix
1179 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1180 return alt_target
1181 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001182
1183
1184def freeze(obj):
1185 """Takes a generic object ``obj``, and returns an immutable version of it.
1186
1187 Supported types:
1188 * dict / OrderedDict -> FrozenDict
1189 * list -> tuple
1190 * set -> frozenset
1191 * any object with a working __hash__ implementation (assumes that hashable
1192 means immutable)
1193
1194 Will raise TypeError if you pass an object which is not hashable.
1195 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001196 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001197 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001198 elif isinstance(obj, (list, tuple)):
1199 return tuple(freeze(i) for i in obj)
1200 elif isinstance(obj, set):
1201 return frozenset(freeze(i) for i in obj)
1202 else:
1203 hash(obj)
1204 return obj
1205
1206
1207class FrozenDict(collections.Mapping):
1208 """An immutable OrderedDict.
1209
1210 Modified From: http://stackoverflow.com/a/2704866
1211 """
1212 def __init__(self, *args, **kwargs):
1213 self._d = collections.OrderedDict(*args, **kwargs)
1214
1215 # Calculate the hash immediately so that we know all the items are
1216 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001217 self._hash = functools.reduce(
1218 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001219
1220 def __eq__(self, other):
1221 if not isinstance(other, collections.Mapping):
1222 return NotImplemented
1223 if self is other:
1224 return True
1225 if len(self) != len(other):
1226 return False
1227 for k, v in self.iteritems():
1228 if k not in other or other[k] != v:
1229 return False
1230 return True
1231
1232 def __iter__(self):
1233 return iter(self._d)
1234
1235 def __len__(self):
1236 return len(self._d)
1237
1238 def __getitem__(self, key):
1239 return self._d[key]
1240
1241 def __hash__(self):
1242 return self._hash
1243
1244 def __repr__(self):
1245 return 'FrozenDict(%r)' % (self._d.items(),)