blob: 78c08b1b6cd5754a4b47139af3829d5d209e5f68 [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'):
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000168 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000169 # codecs.open() has different behavior than open() on python 2.6 so use
170 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000171 s = f.read()
172 try:
173 return s.decode('utf-8')
Raul Tambreb946b232019-03-26 14:48:46 +0000174 # AttributeError is for Py3 compatibility
175 except (UnicodeDecodeError, AttributeError):
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000176 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177
178
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000179def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000180 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182
183
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000184@contextlib.contextmanager
185def temporary_directory(**kwargs):
186 tdir = tempfile.mkdtemp(**kwargs)
187 try:
188 yield tdir
189 finally:
190 if tdir:
191 rmtree(tdir)
192
193
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000194def safe_rename(old, new):
195 """Renames a file reliably.
196
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000197 Sometimes os.rename does not work because a dying git process keeps a handle
198 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000199 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000200 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000201 """
202 # roughly 10s
203 retries = 100
204 for i in range(retries):
205 try:
206 os.rename(old, new)
207 break
208 except OSError:
209 if i == (retries - 1):
210 # Give up.
211 raise
212 # retry
213 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
214 time.sleep(0.1)
215
216
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000217def rm_file_or_tree(path):
218 if os.path.isfile(path):
219 os.remove(path)
220 else:
221 rmtree(path)
222
223
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000224def rmtree(path):
225 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000226
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000227 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000228
229 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700230 are read-only. We need to be able to force the files to be writable (i.e.,
231 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000232
233 Even with all this, Windows still sometimes fails to delete a file, citing
234 a permission error (maybe something to do with antivirus scans or disk
235 indexing). The best suggestion any of the user forums had was to wait a
236 bit and try again, so we do that too. It's hand-waving, but sometimes it
237 works. :/
238
239 On POSIX systems, things are a little bit simpler. The modes of the files
240 to be deleted doesn't matter, only the modes of the directories containing
241 them are significant. As the directory tree is traversed, each directory
242 has its mode set appropriately before descending into it. This should
243 result in the entire tree being removed, with the possible exception of
244 *path itself, because nothing attempts to change the mode of its parent.
245 Doing so would be hazardous, as it's not a directory slated for removal.
246 In the ordinary case, this is not a problem: for our purposes, the user
247 will never lack write permission on *path's parent.
248 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000249 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000250 return
251
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000252 if os.path.islink(path) or not os.path.isdir(path):
253 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000256 # Give up and use cmd.exe's rd command.
257 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000258 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000259 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
260 if exitcode == 0:
261 return
262 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000263 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000264 time.sleep(3)
265 raise Exception('Failed to remove path %s' % path)
266
267 # On POSIX systems, we need the x-bit set on the directory to access it,
268 # the r-bit to see its contents, and the w-bit to remove files from it.
269 # The actual modes of the files within the directory is irrelevant.
270 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000271
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000272 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000273 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000274
275 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000276 # If fullpath is a symbolic link that points to a directory, isdir will
277 # be True, but we don't want to descend into that as a directory, we just
278 # want to remove the link. Check islink and treat links as ordinary files
279 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000280 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000282 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000284 # Recurse.
285 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 remove(os.rmdir, path)
288
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000290def safe_makedirs(tree):
291 """Creates the directory in a safe manner.
292
293 Because multiple threads can create these directories concurently, trap the
294 exception and pass on.
295 """
296 count = 0
297 while not os.path.exists(tree):
298 count += 1
299 try:
300 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000301 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000302 # 17 POSIX, 183 Windows
303 if e.errno not in (17, 183):
304 raise
305 if count > 40:
306 # Give up.
307 raise
308
309
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000310def CommandToStr(args):
311 """Converts an arg list into a shell escaped string."""
312 return ' '.join(pipes.quote(arg) for arg in args)
313
314
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000315def CheckCallAndFilterAndHeader(args, always=False, header=None, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000316 """Adds 'header' support to CheckCallAndFilter.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000317
maruel@chromium.org17d01792010-09-01 18:07:10 +0000318 If |always| is True, a message indicating what is being done
319 is printed to stdout all the time even if not output is generated. Otherwise
320 the message header is printed only if the call generated any ouput.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000321 """
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000322 stdout = kwargs.setdefault('stdout', sys.stdout)
323 if header is None:
Daniel Chenga0c5f082017-10-19 13:35:19 -0700324 # The automatically generated header only prepends newline if always is
325 # false: always is usually set to false if there's an external progress
326 # display, and it's better not to clobber it in that case.
327 header = "%s________ running '%s' in '%s'\n" % (
328 '' if always else '\n',
ilevy@chromium.org4aad1852013-07-12 21:32:51 +0000329 ' '.join(args), kwargs.get('cwd', '.'))
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000330
maruel@chromium.org17d01792010-09-01 18:07:10 +0000331 if always:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000332 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000333 else:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000334 filter_fn = kwargs.get('filter_fn')
maruel@chromium.org17d01792010-09-01 18:07:10 +0000335 def filter_msg(line):
336 if line is None:
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000337 stdout.write(header)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000338 elif filter_fn:
339 filter_fn(line)
340 kwargs['filter_fn'] = filter_msg
341 kwargs['call_filter_on_first_line'] = True
342 # Obviously.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000343 kwargs.setdefault('print_stdout', True)
maruel@chromium.org17d01792010-09-01 18:07:10 +0000344 return CheckCallAndFilter(args, **kwargs)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000345
maruel@chromium.org17d01792010-09-01 18:07:10 +0000346
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347class Wrapper(object):
348 """Wraps an object, acting as a transparent proxy for all properties by
349 default.
350 """
351 def __init__(self, wrapped):
352 self._wrapped = wrapped
353
354 def __getattr__(self, name):
355 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000356
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000357
Edward Lemur231f5ea2018-01-31 19:02:36 +0100358class WriteToStdout(Wrapper):
359 """Creates a file object clone to also print to sys.stdout."""
360 def __init__(self, wrapped):
361 super(WriteToStdout, self).__init__(wrapped)
362 if not hasattr(self, 'lock'):
363 self.lock = threading.Lock()
364
365 def write(self, out, *args, **kwargs):
366 self._wrapped.write(out, *args, **kwargs)
367 self.lock.acquire()
368 try:
369 sys.stdout.write(out, *args, **kwargs)
370 finally:
371 self.lock.release()
372
373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000375 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376 def __init__(self, wrapped, delay):
377 super(AutoFlush, self).__init__(wrapped)
378 if not hasattr(self, 'lock'):
379 self.lock = threading.Lock()
380 self.__last_flushed_at = time.time()
381 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000382
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 @property
384 def autoflush(self):
385 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000386
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 def write(self, out, *args, **kwargs):
388 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000389 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000391 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000393 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000394 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000395 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000396 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000397 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000398 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000399
400
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000402 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000403 threads with a NN> prefix.
404 """
405 def __init__(self, wrapped, include_zero=False):
406 super(Annotated, self).__init__(wrapped)
407 if not hasattr(self, 'lock'):
408 self.lock = threading.Lock()
409 self.__output_buffers = {}
410 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000411
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 @property
413 def annotated(self):
414 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000415
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 def write(self, out):
417 index = getattr(threading.currentThread(), 'index', 0)
418 if not index and not self.__include_zero:
419 # Unindexed threads aren't buffered.
420 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000421
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000423 try:
424 # Use a dummy array to hold the string so the code can be lockless.
425 # Strings are immutable, requiring to keep a lock for the whole dictionary
426 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000427 if not index in self.__output_buffers:
428 obj = self.__output_buffers[index] = ['']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000429 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000430 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000431 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000432 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000433
434 # Continue lockless.
435 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000436 while True:
437 # TODO(agable): find both of these with a single pass.
438 cr_loc = obj[0].find('\r')
439 lf_loc = obj[0].find('\n')
440 if cr_loc == lf_loc == -1:
441 break
442 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
443 line, remaining = obj[0].split('\n', 1)
444 if line:
445 self._wrapped.write('%d>%s\n' % (index, line))
446 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
447 line, remaining = obj[0].split('\r', 1)
448 if line:
449 self._wrapped.write('%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000450 obj[0] = remaining
451
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000452 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000453 """Flush buffered output."""
454 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000455 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000456 try:
457 # Detect threads no longer existing.
458 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000459 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000460 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000461 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000462 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000463 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000464 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000465 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000466 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000467
468 # Don't keep the lock while writting. Will append \n when it shouldn't.
469 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000470 if orphan[1]:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000471 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1]))
472 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000473
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000474
475def MakeFileAutoFlush(fileobj, delay=10):
476 autoflush = getattr(fileobj, 'autoflush', None)
477 if autoflush:
478 autoflush.delay = delay
479 return fileobj
480 return AutoFlush(fileobj, delay)
481
482
483def MakeFileAnnotated(fileobj, include_zero=False):
484 if getattr(fileobj, 'annotated', None):
485 return fileobj
486 return Annotated(fileobj)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000487
488
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000489GCLIENT_CHILDREN = []
490GCLIENT_CHILDREN_LOCK = threading.Lock()
491
492
493class GClientChildren(object):
494 @staticmethod
495 def add(popen_obj):
496 with GCLIENT_CHILDREN_LOCK:
497 GCLIENT_CHILDREN.append(popen_obj)
498
499 @staticmethod
500 def remove(popen_obj):
501 with GCLIENT_CHILDREN_LOCK:
502 GCLIENT_CHILDREN.remove(popen_obj)
503
504 @staticmethod
505 def _attemptToKillChildren():
506 global GCLIENT_CHILDREN
507 with GCLIENT_CHILDREN_LOCK:
508 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
509
510 for zombie in zombies:
511 try:
512 zombie.kill()
513 except OSError:
514 pass
515
516 with GCLIENT_CHILDREN_LOCK:
517 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
518
519 @staticmethod
520 def _areZombies():
521 with GCLIENT_CHILDREN_LOCK:
522 return bool(GCLIENT_CHILDREN)
523
524 @staticmethod
525 def KillAllRemainingChildren():
526 GClientChildren._attemptToKillChildren()
527
528 if GClientChildren._areZombies():
529 time.sleep(0.5)
530 GClientChildren._attemptToKillChildren()
531
532 with GCLIENT_CHILDREN_LOCK:
533 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000534 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000535 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000536 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000537
538
maruel@chromium.org17d01792010-09-01 18:07:10 +0000539def CheckCallAndFilter(args, stdout=None, filter_fn=None,
540 print_stdout=None, call_filter_on_first_line=False,
tandrii64103db2016-10-11 05:30:05 -0700541 retry=False, **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000542 """Runs a command and calls back a filter function if needed.
543
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000544 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000545 print_stdout: If True, the command's stdout is forwarded to stdout.
546 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000547 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000548 character trimmed.
549 stdout: Can be any bufferable output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000550 retry: If the process exits non-zero, sleep for a brief interval and try
551 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000552
553 stderr is always redirected to stdout.
554 """
555 assert print_stdout or filter_fn
556 stdout = stdout or sys.stdout
Raul Tambreb946b232019-03-26 14:48:46 +0000557 output = io.BytesIO()
maruel@chromium.org17d01792010-09-01 18:07:10 +0000558 filter_fn = filter_fn or (lambda x: None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000559
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000560 sleep_interval = RETRY_INITIAL_SLEEP
561 run_cwd = kwargs.get('cwd', os.getcwd())
Raul Tambreb946b232019-03-26 14:48:46 +0000562 for _ in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000563 kid = subprocess2.Popen(
564 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
565 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000566
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000568
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000569 # Do a flush of stdout before we begin reading from the subprocess2's stdout
570 stdout.flush()
571
572 # Also, we need to forward stdout to prevent weird re-ordering of output.
573 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700574 # normally buffering is done for each line, but if the process requests
575 # input, no end-of-line character is output after the prompt and it would
576 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000577 try:
578 in_byte = kid.stdout.read(1)
579 if in_byte:
580 if call_filter_on_first_line:
581 filter_fn(None)
Edward Lemur602076d2019-07-27 00:36:16 +0000582 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000583 while in_byte:
Edward Lemur602076d2019-07-27 00:36:16 +0000584 if isinstance(in_byte, bytes):
585 in_byte = in_byte.decode('utf-8')
586 output.write(in_byte.encode('utf-8'))
hinoka@google.com267f33e2014-02-28 22:02:32 +0000587 if print_stdout:
Raul Tambre1fb04632019-04-08 19:13:37 +0000588 stdout.write(in_byte)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000589 if in_byte not in ['\r', '\n']:
590 in_line += in_byte
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000591 else:
592 filter_fn(in_line)
Edward Lemur602076d2019-07-27 00:36:16 +0000593 in_line = ''
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000594 in_byte = kid.stdout.read(1)
595 # Flush the rest of buffered output. This is only an issue with
596 # stdout/stderr not ending with a \n.
597 if len(in_line):
szager@google.com85d3e3a2011-10-07 17:12:00 +0000598 filter_fn(in_line)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000599 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000600
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000601 # Don't put this in a 'finally,' since the child may still run if we get
602 # an exception.
603 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000604
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000605 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000606 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000607 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000608
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000609 if rv == 0:
hinoka@google.com267f33e2014-02-28 22:02:32 +0000610 return output.getvalue()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000611 if not retry:
612 break
Raul Tambreb946b232019-03-26 14:48:46 +0000613 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
614 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000615 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000616 sleep_interval *= 2
617 raise subprocess2.CalledProcessError(
618 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000619
620
agable@chromium.org5a306a22014-02-24 22:13:59 +0000621class GitFilter(object):
622 """A filter_fn implementation for quieting down git output messages.
623
624 Allows a custom function to skip certain lines (predicate), and will throttle
625 the output of percentage completed lines to only output every X seconds.
626 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000627 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000629 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000630 """
631 Args:
632 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
633 XX% complete messages) to only be printed at least |time_throttle|
634 seconds apart.
635 predicate (f(line)): An optional function which is invoked for every line.
636 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000637 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 """
639 self.last_time = 0
640 self.time_throttle = time_throttle
641 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000642 self.out_fh = out_fh or sys.stdout
643 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000644
645 def __call__(self, line):
646 # git uses an escape sequence to clear the line; elide it.
Raul Tambreb946b232019-03-26 14:48:46 +0000647 esc = line.find(chr(0o33).encode())
agable@chromium.org5a306a22014-02-24 22:13:59 +0000648 if esc > -1:
649 line = line[:esc]
650 if self.predicate and not self.predicate(line):
651 return
652 now = time.time()
Raul Tambreb946b232019-03-26 14:48:46 +0000653 match = self.PERCENT_RE.match(line.decode())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000654 if match:
655 if match.group(1) != self.progress_prefix:
656 self.progress_prefix = match.group(1)
657 elif now - self.last_time < self.time_throttle:
658 return
659 self.last_time = now
660 self.out_fh.write('[%s] ' % Elapsed())
Raul Tambreb946b232019-03-26 14:48:46 +0000661 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000662
663
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000664def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000665 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000666
rcui@google.com13595ff2011-10-13 01:25:07 +0000667 Returns nearest upper-level directory with the passed in file.
668 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000669 if not path:
670 path = os.getcwd()
671 path = os.path.realpath(path)
672 while True:
673 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000674 if os.path.exists(file_path):
675 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000676 (new_path, _) = os.path.split(path)
677 if new_path == path:
678 return None
679 path = new_path
680
681
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000682def GetMacWinOrLinux():
683 """Returns 'mac', 'win', or 'linux', matching the current platform."""
684 if sys.platform.startswith(('cygwin', 'win')):
685 return 'win'
686 elif sys.platform.startswith('linux'):
687 return 'linux'
688 elif sys.platform == 'darwin':
689 return 'mac'
690 raise Error('Unknown platform: ' + sys.platform)
691
692
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000693def GetGClientRootAndEntries(path=None):
694 """Returns the gclient root and the dict of entries."""
695 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000696 root = FindFileUpwards(config_file, path)
697 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000698 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000699 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000700 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000701 env = {}
702 execfile(config_path, env)
703 config_dir = os.path.dirname(config_path)
704 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000705
706
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000707def lockedmethod(method):
708 """Method decorator that holds self.lock for the duration of the call."""
709 def inner(self, *args, **kwargs):
710 try:
711 try:
712 self.lock.acquire()
713 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000714 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000715 raise
716 return method(self, *args, **kwargs)
717 finally:
718 self.lock.release()
719 return inner
720
721
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000722class WorkItem(object):
723 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000724 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
725 # As a workaround, use a single lock. Yep you read it right. Single lock for
726 # all the 100 objects.
727 lock = threading.Lock()
728
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000729 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000730 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000731 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000732 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000733 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700734 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000735
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000736 def run(self, work_queue):
737 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000738 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000739 pass
740
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000741 @property
742 def name(self):
743 return self._name
744
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000745
746class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000747 """Runs a set of WorkItem that have interdependencies and were WorkItem are
748 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000749
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200750 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000751 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000752
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000754 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000755 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000756 """jobs specifies the number of concurrent tasks to allow. progress is a
757 Progress instance."""
758 # Set when a thread is done or a new item is enqueued.
759 self.ready_cond = threading.Condition()
760 # Maximum number of concurrent tasks.
761 self.jobs = jobs
762 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000763 self.queued = []
764 # List of strings representing each Dependency.name that was run.
765 self.ran = []
766 # List of items currently running.
767 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000768 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000769 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000770 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000771 self.progress = progress
772 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000773 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000774
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000775 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000776 self.verbose = verbose
777 self.last_join = None
778 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000779
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780 def enqueue(self, d):
781 """Enqueue one Dependency to be executed later once its requirements are
782 satisfied.
783 """
784 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000785 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000787 self.queued.append(d)
788 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000789 if self.jobs == 1:
790 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000791 logging.debug('enqueued(%s)' % d.name)
792 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000793 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000794 self.progress.update(0)
795 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000798
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000799 def out_cb(self, _):
800 self.last_subproc_output = datetime.datetime.now()
801 return True
802
803 @staticmethod
804 def format_task_output(task, comment=''):
805 if comment:
806 comment = ' (%s)' % comment
807 if task.start and task.finish:
808 elapsed = ' (Elapsed: %s)' % (
809 str(task.finish - task.start).partition('.')[0])
810 else:
811 elapsed = ''
812 return """
813%s%s%s
814----------------------------------------
815%s
816----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000817 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000818
hinoka885e5b12016-06-08 14:40:09 -0700819 def _is_conflict(self, job):
820 """Checks to see if a job will conflict with another running job."""
821 for running_job in self.running:
822 for used_resource in running_job.item.resources:
823 logging.debug('Checking resource %s' % used_resource)
824 if used_resource in job.resources:
825 return True
826 return False
827
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 def flush(self, *args, **kwargs):
829 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000830 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000831 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000832 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000833 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 while True:
835 # Check for task to run first, then wait.
836 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000837 if not self.exceptions.empty():
838 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000839 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000840 self._flush_terminated_threads()
841 if (not self.queued and not self.running or
842 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000843 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000844 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000845
846 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000847 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000848 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000849 if (self.ignore_requirements or
850 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700851 if not self._is_conflict(self.queued[i]):
852 # Start one work item: all its requirements are satisfied.
853 self._run_one_task(self.queued.pop(i), args, kwargs)
854 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 else:
856 # Couldn't find an item that could run. Break out the outher loop.
857 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000858
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000860 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000861 break
862 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000863 try:
864 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000865 # If we haven't printed to terminal for a while, but we have received
866 # spew from a suprocess, let the user know we're still progressing.
867 now = datetime.datetime.now()
868 if (now - self.last_join > datetime.timedelta(seconds=60) and
869 self.last_subproc_output > self.last_join):
870 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000871 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000872 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000874 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000875 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000876 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000877 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000878 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000879 except KeyboardInterrupt:
880 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000881 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000882 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000883 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
884 self.ran), len(self.running)),
885 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000886 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000887 print(
888 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
889 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000890 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000891 print(
892 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000893 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000894 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000895 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000896 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000897
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000900 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000901 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000902 # To get back the stack location correctly, the raise a, b, c form must be
903 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000904 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000905 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
906 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000907 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000908 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000909
maruel@chromium.org3742c842010-09-09 19:27:14 +0000910 def _flush_terminated_threads(self):
911 """Flush threads that have terminated."""
912 running = self.running
913 self.running = []
914 for t in running:
915 if t.isAlive():
916 self.running.append(t)
917 else:
918 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000919 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000920 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000921 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000922 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000923 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000924 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000925 if t.item.name in self.ran:
926 raise Error(
927 'gclient is confused, "%s" is already in "%s"' % (
928 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000929 if not t.item.name in self.ran:
930 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000931
932 def _run_one_task(self, task_item, args, kwargs):
933 if self.jobs > 1:
934 # Start the thread.
935 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000936 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000937 self.running.append(new_thread)
938 new_thread.start()
939 else:
940 # Run the 'thread' inside the main thread. Don't try to catch any
941 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 try:
943 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000944 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 task_item.run(*args, **kwargs)
946 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000947 print(
948 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000949 self.ran.append(task_item.name)
950 if self.verbose:
951 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000952 print('')
953 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000954 if self.progress:
955 self.progress.update(1, ', '.join(t.item.name for t in self.running))
956 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000957 print(
958 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000959 raise
960 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000961 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000962 raise
963
maruel@chromium.org3742c842010-09-09 19:27:14 +0000964
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000965 class _Worker(threading.Thread):
966 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000967 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000968 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000969 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000970 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000971 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000972 self.args = args
973 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000974 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000975
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000976 def run(self):
977 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000978 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000979 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000980 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000981 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000982 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000983 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000985 print(
986 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000987 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000988 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000989 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000991 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000992 except Exception:
993 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000994 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000995 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000996 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000997 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000998 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000999 work_queue.ready_cond.acquire()
1000 try:
1001 work_queue.ready_cond.notifyAll()
1002 finally:
1003 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001004
1005
agable92bec4f2016-08-24 09:27:27 -07001006def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001007 """Returns the most plausible editor to use.
1008
1009 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001010 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001011 - core.editor git configuration variable (if supplied by git-cl)
1012 - VISUAL environment variable
1013 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001014 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001015
1016 In the case of git-cl, this matches git's behaviour, except that it does not
1017 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001018 """
agable92bec4f2016-08-24 09:27:27 -07001019 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001020 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001021 editor = os.environ.get('VISUAL')
1022 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001023 editor = os.environ.get('EDITOR')
1024 if not editor:
1025 if sys.platform.startswith('win'):
1026 editor = 'notepad'
1027 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001028 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001029 return editor
1030
1031
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001032def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001033 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001034 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001035 # Make sure CRLF is handled properly by requiring none.
1036 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001037 print(
1038 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039 fileobj = os.fdopen(file_handle, 'w')
1040 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001041 content = re.sub('\r?\n', '\n', content)
1042 # Some editors complain when the file doesn't end in \n.
1043 if not content.endswith('\n'):
1044 content += '\n'
1045 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001046 fileobj.close()
1047
1048 try:
agable92bec4f2016-08-24 09:27:27 -07001049 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001050 if not editor:
1051 return None
1052 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001053 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1054 # Msysgit requires the usage of 'env' to be present.
1055 cmd = 'env ' + cmd
1056 try:
1057 # shell=True to allow the shell to handle all forms of quotes in
1058 # $EDITOR.
1059 subprocess2.check_call(cmd, shell=True)
1060 except subprocess2.CalledProcessError:
1061 return None
1062 return FileRead(filename)
1063 finally:
1064 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001065
1066
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001067def UpgradeToHttps(url):
1068 """Upgrades random urls to https://.
1069
1070 Do not touch unknown urls like ssh:// or git://.
1071 Do not touch http:// urls with a port number,
1072 Fixes invalid GAE url.
1073 """
1074 if not url:
1075 return url
1076 if not re.match(r'[a-z\-]+\://.*', url):
1077 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1078 # relative url and will use http:///foo. Note that it defaults to http://
1079 # for compatibility with naked url like "localhost:8080".
1080 url = 'http://%s' % url
1081 parsed = list(urlparse.urlparse(url))
1082 # Do not automatically upgrade http to https if a port number is provided.
1083 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1084 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001085 return urlparse.urlunparse(parsed)
1086
1087
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001088def ParseCodereviewSettingsContent(content):
1089 """Process a codereview.settings file properly."""
1090 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1091 try:
1092 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1093 except ValueError:
1094 raise Error(
1095 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001096 def fix_url(key):
1097 if keyvals.get(key):
1098 keyvals[key] = UpgradeToHttps(keyvals[key])
1099 fix_url('CODE_REVIEW_SERVER')
1100 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001101 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001102
1103
1104def NumLocalCpus():
1105 """Returns the number of processors.
1106
dnj@chromium.org530523b2015-01-07 19:54:57 +00001107 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1108 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1109 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001110 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001111 # Surround the entire thing in try/except; no failure here should stop gclient
1112 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001113 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001114 # Use multiprocessing to get CPU count. This may raise
1115 # NotImplementedError.
1116 try:
1117 import multiprocessing
1118 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001119 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001120 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001121 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001122 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1123 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1124
1125 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1126 if 'NUMBER_OF_PROCESSORS' in os.environ:
1127 return int(os.environ['NUMBER_OF_PROCESSORS'])
1128 except Exception as e:
1129 logging.exception("Exception raised while probing CPU count: %s", e)
1130
1131 logging.debug('Failed to get CPU count. Defaulting to 1.')
1132 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001133
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001134
szager@chromium.orgfc616382014-03-18 20:32:04 +00001135def DefaultDeltaBaseCacheLimit():
1136 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1137
1138 The primary constraint is the address space of virtual memory. The cache
1139 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1140 parameter is set too high.
1141 """
1142 if platform.architecture()[0].startswith('64'):
1143 return '2g'
1144 else:
1145 return '512m'
1146
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001147
szager@chromium.orgff113292014-03-25 06:02:08 +00001148def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001149 """Return reasonable default values for configuring git-index-pack.
1150
1151 Experiments suggest that higher values for pack.threads don't improve
1152 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001153 cache_limit = DefaultDeltaBaseCacheLimit()
1154 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1155 if url in THREADED_INDEX_PACK_BLACKLIST:
1156 result.extend(['-c', 'pack.threads=1'])
1157 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001158
1159
1160def FindExecutable(executable):
1161 """This mimics the "which" utility."""
1162 path_folders = os.environ.get('PATH').split(os.pathsep)
1163
1164 for path_folder in path_folders:
1165 target = os.path.join(path_folder, executable)
1166 # Just incase we have some ~/blah paths.
1167 target = os.path.abspath(os.path.expanduser(target))
1168 if os.path.isfile(target) and os.access(target, os.X_OK):
1169 return target
1170 if sys.platform.startswith('win'):
1171 for suffix in ('.bat', '.cmd', '.exe'):
1172 alt_target = target + suffix
1173 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1174 return alt_target
1175 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001176
1177
1178def freeze(obj):
1179 """Takes a generic object ``obj``, and returns an immutable version of it.
1180
1181 Supported types:
1182 * dict / OrderedDict -> FrozenDict
1183 * list -> tuple
1184 * set -> frozenset
1185 * any object with a working __hash__ implementation (assumes that hashable
1186 means immutable)
1187
1188 Will raise TypeError if you pass an object which is not hashable.
1189 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001190 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001191 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001192 elif isinstance(obj, (list, tuple)):
1193 return tuple(freeze(i) for i in obj)
1194 elif isinstance(obj, set):
1195 return frozenset(freeze(i) for i in obj)
1196 else:
1197 hash(obj)
1198 return obj
1199
1200
1201class FrozenDict(collections.Mapping):
1202 """An immutable OrderedDict.
1203
1204 Modified From: http://stackoverflow.com/a/2704866
1205 """
1206 def __init__(self, *args, **kwargs):
1207 self._d = collections.OrderedDict(*args, **kwargs)
1208
1209 # Calculate the hash immediately so that we know all the items are
1210 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001211 self._hash = functools.reduce(
1212 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001213
1214 def __eq__(self, other):
1215 if not isinstance(other, collections.Mapping):
1216 return NotImplemented
1217 if self is other:
1218 return True
1219 if len(self) != len(other):
1220 return False
1221 for k, v in self.iteritems():
1222 if k not in other or other[k] != v:
1223 return False
1224 return True
1225
1226 def __iter__(self):
1227 return iter(self._d)
1228
1229 def __len__(self):
1230 return len(self._d)
1231
1232 def __getitem__(self, key):
1233 return self._d[key]
1234
1235 def __hash__(self):
1236 return self._hash
1237
1238 def __repr__(self):
1239 return 'FrozenDict(%r)' % (self._d.items(),)