blob: 6939fff1f663efdba9a24f7dda72d73ad023858f [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
Edward Lemur419c92f2019-10-25 22:17:49 +0000167def FileRead(filename, mode='rbU'):
168 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000169 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000170 if mode == 'rbU' and sys.version_info.major == 3:
171 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000172 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000173 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000174 if isinstance(s, bytes):
175 return s.decode('utf-8', 'replace')
176 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):
Ben Pastene1906f402019-10-24 15:36:00 +0000218 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000219 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
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000315class Wrapper(object):
316 """Wraps an object, acting as a transparent proxy for all properties by
317 default.
318 """
319 def __init__(self, wrapped):
320 self._wrapped = wrapped
321
322 def __getattr__(self, name):
323 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000324
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000325
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000326class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000327 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000328 def __init__(self, wrapped, delay):
329 super(AutoFlush, self).__init__(wrapped)
330 if not hasattr(self, 'lock'):
331 self.lock = threading.Lock()
332 self.__last_flushed_at = time.time()
333 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000334
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000335 @property
336 def autoflush(self):
337 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000338
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000339 def write(self, out, *args, **kwargs):
340 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000343 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000345 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000346 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000347 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000348 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000349 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000350 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000351
352
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000354 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355 threads with a NN> prefix.
356 """
357 def __init__(self, wrapped, include_zero=False):
358 super(Annotated, self).__init__(wrapped)
359 if not hasattr(self, 'lock'):
360 self.lock = threading.Lock()
361 self.__output_buffers = {}
362 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000363 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000364
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000365 @property
366 def annotated(self):
367 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000368
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000369 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000370 # Store as bytes to ensure Unicode characters get output correctly.
371 if not isinstance(out, bytes):
372 out = out.encode('utf-8')
373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374 index = getattr(threading.currentThread(), 'index', 0)
375 if not index and not self.__include_zero:
376 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000377 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000378
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000380 try:
381 # Use a dummy array to hold the string so the code can be lockless.
382 # Strings are immutable, requiring to keep a lock for the whole dictionary
383 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000385 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000386 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000389 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000390
391 # Continue lockless.
392 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000393 while True:
394 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000395 cr_loc = obj[0].find(b'\r')
396 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000397 if cr_loc == lf_loc == -1:
398 break
399 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000400 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000401 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000402 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000403 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000404 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000405 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000406 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000407 obj[0] = remaining
408
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000410 """Flush buffered output."""
411 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 try:
414 # Detect threads no longer existing.
415 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000416 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000418 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000419 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000420 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000424
425 # Don't keep the lock while writting. Will append \n when it shouldn't.
426 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000427 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000428 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000429 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000430
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000431
432def MakeFileAutoFlush(fileobj, delay=10):
433 autoflush = getattr(fileobj, 'autoflush', None)
434 if autoflush:
435 autoflush.delay = delay
436 return fileobj
437 return AutoFlush(fileobj, delay)
438
439
440def MakeFileAnnotated(fileobj, include_zero=False):
441 if getattr(fileobj, 'annotated', None):
442 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000443 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000444
445
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000446GCLIENT_CHILDREN = []
447GCLIENT_CHILDREN_LOCK = threading.Lock()
448
449
450class GClientChildren(object):
451 @staticmethod
452 def add(popen_obj):
453 with GCLIENT_CHILDREN_LOCK:
454 GCLIENT_CHILDREN.append(popen_obj)
455
456 @staticmethod
457 def remove(popen_obj):
458 with GCLIENT_CHILDREN_LOCK:
459 GCLIENT_CHILDREN.remove(popen_obj)
460
461 @staticmethod
462 def _attemptToKillChildren():
463 global GCLIENT_CHILDREN
464 with GCLIENT_CHILDREN_LOCK:
465 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
466
467 for zombie in zombies:
468 try:
469 zombie.kill()
470 except OSError:
471 pass
472
473 with GCLIENT_CHILDREN_LOCK:
474 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
475
476 @staticmethod
477 def _areZombies():
478 with GCLIENT_CHILDREN_LOCK:
479 return bool(GCLIENT_CHILDREN)
480
481 @staticmethod
482 def KillAllRemainingChildren():
483 GClientChildren._attemptToKillChildren()
484
485 if GClientChildren._areZombies():
486 time.sleep(0.5)
487 GClientChildren._attemptToKillChildren()
488
489 with GCLIENT_CHILDREN_LOCK:
490 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000491 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000492 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000493 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000494
495
Edward Lemur24146be2019-08-01 21:44:52 +0000496def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
497 show_header=False, always_show_header=False, retry=False,
498 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000499 """Runs a command and calls back a filter function if needed.
500
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000501 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000502 print_stdout: If True, the command's stdout is forwarded to stdout.
503 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000504 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000505 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000506 show_header: Whether to display a header before the command output.
507 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000508 retry: If the process exits non-zero, sleep for a brief interval and try
509 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000510
511 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000512
513 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000514 """
Edward Lemur24146be2019-08-01 21:44:52 +0000515 def show_header_if_necessary(needs_header, attempt):
516 """Show the header at most once."""
517 if not needs_header[0]:
518 return
519
520 needs_header[0] = False
521 # Automatically generated header. We only prepend a newline if
522 # always_show_header is false, since it usually indicates there's an
523 # external progress display, and it's better not to clobber it in that case.
524 header = '' if always_show_header else '\n'
525 header += '________ running \'%s\' in \'%s\'' % (
526 ' '.join(args), kwargs.get('cwd', '.'))
527 if attempt:
528 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
529 header += '\n'
530
531 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000532 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
533 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000534 if filter_fn:
535 filter_fn(header)
536
537 def filter_line(command_output, line_start):
538 """Extract the last line from command output and filter it."""
539 if not filter_fn or line_start is None:
540 return
541 command_output.seek(line_start)
542 filter_fn(command_output.read().decode('utf-8'))
543
544 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
545 # byte inputs and sys.stdout.buffer must be used instead.
546 if print_stdout:
547 sys.stdout.flush()
548 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
549 else:
550 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000551
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000552 sleep_interval = RETRY_INITIAL_SLEEP
553 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000554 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000555 kid = subprocess2.Popen(
556 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
557 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000558
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000559 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000560
Edward Lemur24146be2019-08-01 21:44:52 +0000561 # Store the output of the command regardless of the value of print_stdout or
562 # filter_fn.
563 command_output = io.BytesIO()
564
565 # Passed as a list for "by ref" semantics.
566 needs_header = [show_header]
567 if always_show_header:
568 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000569
570 # Also, we need to forward stdout to prevent weird re-ordering of output.
571 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700572 # normally buffering is done for each line, but if the process requests
573 # input, no end-of-line character is output after the prompt and it would
574 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000575 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000576 line_start = None
577 while True:
578 in_byte = kid.stdout.read(1)
579 is_newline = in_byte in (b'\n', b'\r')
580 if not in_byte:
581 break
582
583 show_header_if_necessary(needs_header, attempt)
584
585 if is_newline:
586 filter_line(command_output, line_start)
587 line_start = None
588 elif line_start is None:
589 line_start = command_output.tell()
590
591 stdout_write(in_byte)
592 command_output.write(in_byte)
593
594 # Flush the rest of buffered output.
595 sys.stdout.flush()
596 if line_start is not None:
597 filter_line(command_output, line_start)
598
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000599 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000600 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000601
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 # Don't put this in a 'finally,' since the child may still run if we get
603 # an exception.
604 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000605
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000606 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000607 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000608 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000609
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000610 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000611 return command_output.getvalue()
612
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000613 if not retry:
614 break
Edward Lemur24146be2019-08-01 21:44:52 +0000615
Raul Tambreb946b232019-03-26 14:48:46 +0000616 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
617 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000618 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000619 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000620
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000621 raise subprocess2.CalledProcessError(
622 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000623
624
agable@chromium.org5a306a22014-02-24 22:13:59 +0000625class GitFilter(object):
626 """A filter_fn implementation for quieting down git output messages.
627
628 Allows a custom function to skip certain lines (predicate), and will throttle
629 the output of percentage completed lines to only output every X seconds.
630 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000631 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000632
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000633 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000634 """
635 Args:
636 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
637 XX% complete messages) to only be printed at least |time_throttle|
638 seconds apart.
639 predicate (f(line)): An optional function which is invoked for every line.
640 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000641 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642 """
Edward Lemur24146be2019-08-01 21:44:52 +0000643 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000644 self.last_time = 0
645 self.time_throttle = time_throttle
646 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000647 self.out_fh = out_fh or sys.stdout
648 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000649
650 def __call__(self, line):
651 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000652 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000653 if esc > -1:
654 line = line[:esc]
655 if self.predicate and not self.predicate(line):
656 return
657 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000658 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000659 if match:
660 if match.group(1) != self.progress_prefix:
661 self.progress_prefix = match.group(1)
662 elif now - self.last_time < self.time_throttle:
663 return
664 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000665 if not self.first_line:
666 self.out_fh.write('[%s] ' % Elapsed())
667 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000668 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000669
670
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000671def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000672 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000673
rcui@google.com13595ff2011-10-13 01:25:07 +0000674 Returns nearest upper-level directory with the passed in file.
675 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000676 if not path:
677 path = os.getcwd()
678 path = os.path.realpath(path)
679 while True:
680 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000681 if os.path.exists(file_path):
682 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000683 (new_path, _) = os.path.split(path)
684 if new_path == path:
685 return None
686 path = new_path
687
688
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000689def GetMacWinOrLinux():
690 """Returns 'mac', 'win', or 'linux', matching the current platform."""
691 if sys.platform.startswith(('cygwin', 'win')):
692 return 'win'
693 elif sys.platform.startswith('linux'):
694 return 'linux'
695 elif sys.platform == 'darwin':
696 return 'mac'
697 raise Error('Unknown platform: ' + sys.platform)
698
699
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000700def GetGClientRootAndEntries(path=None):
701 """Returns the gclient root and the dict of entries."""
702 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000703 root = FindFileUpwards(config_file, path)
704 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000705 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000706 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000707 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000708 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000709 with open(config_path) as config:
710 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000711 config_dir = os.path.dirname(config_path)
712 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000713
714
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000715def lockedmethod(method):
716 """Method decorator that holds self.lock for the duration of the call."""
717 def inner(self, *args, **kwargs):
718 try:
719 try:
720 self.lock.acquire()
721 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000722 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000723 raise
724 return method(self, *args, **kwargs)
725 finally:
726 self.lock.release()
727 return inner
728
729
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000730class WorkItem(object):
731 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000732 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
733 # As a workaround, use a single lock. Yep you read it right. Single lock for
734 # all the 100 objects.
735 lock = threading.Lock()
736
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000737 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000738 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000739 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000740 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000741 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700742 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000743
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000744 def run(self, work_queue):
745 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000746 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000747 pass
748
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000749 @property
750 def name(self):
751 return self._name
752
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000753
754class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000755 """Runs a set of WorkItem that have interdependencies and were WorkItem are
756 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000757
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200758 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000759 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000760
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000761 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000762 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000763 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000764 """jobs specifies the number of concurrent tasks to allow. progress is a
765 Progress instance."""
766 # Set when a thread is done or a new item is enqueued.
767 self.ready_cond = threading.Condition()
768 # Maximum number of concurrent tasks.
769 self.jobs = jobs
770 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000771 self.queued = []
772 # List of strings representing each Dependency.name that was run.
773 self.ran = []
774 # List of items currently running.
775 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000776 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000777 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000778 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000779 self.progress = progress
780 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000781 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000782
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000783 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000784 self.verbose = verbose
785 self.last_join = None
786 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000787
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000788 def enqueue(self, d):
789 """Enqueue one Dependency to be executed later once its requirements are
790 satisfied.
791 """
792 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000793 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000794 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000795 self.queued.append(d)
796 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000797 if self.jobs == 1:
798 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000799 logging.debug('enqueued(%s)' % d.name)
800 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000801 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000802 self.progress.update(0)
803 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000804 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000805 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000806
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000807 def out_cb(self, _):
808 self.last_subproc_output = datetime.datetime.now()
809 return True
810
811 @staticmethod
812 def format_task_output(task, comment=''):
813 if comment:
814 comment = ' (%s)' % comment
815 if task.start and task.finish:
816 elapsed = ' (Elapsed: %s)' % (
817 str(task.finish - task.start).partition('.')[0])
818 else:
819 elapsed = ''
820 return """
821%s%s%s
822----------------------------------------
823%s
824----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000825 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000826
hinoka885e5b12016-06-08 14:40:09 -0700827 def _is_conflict(self, job):
828 """Checks to see if a job will conflict with another running job."""
829 for running_job in self.running:
830 for used_resource in running_job.item.resources:
831 logging.debug('Checking resource %s' % used_resource)
832 if used_resource in job.resources:
833 return True
834 return False
835
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000836 def flush(self, *args, **kwargs):
837 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000838 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000839 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000840 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000841 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 while True:
843 # Check for task to run first, then wait.
844 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000845 if not self.exceptions.empty():
846 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000847 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000848 self._flush_terminated_threads()
849 if (not self.queued and not self.running or
850 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000851 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000852 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000853
854 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000855 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000856 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000857 if (self.ignore_requirements or
858 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700859 if not self._is_conflict(self.queued[i]):
860 # Start one work item: all its requirements are satisfied.
861 self._run_one_task(self.queued.pop(i), args, kwargs)
862 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 else:
864 # Couldn't find an item that could run. Break out the outher loop.
865 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000866
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000867 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000868 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000869 break
870 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000871 try:
872 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 # If we haven't printed to terminal for a while, but we have received
874 # spew from a suprocess, let the user know we're still progressing.
875 now = datetime.datetime.now()
876 if (now - self.last_join > datetime.timedelta(seconds=60) and
877 self.last_subproc_output > self.last_join):
878 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000879 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000880 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000881 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000882 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000883 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000884 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000885 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000886 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000887 except KeyboardInterrupt:
888 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000889 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000890 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000891 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
892 self.ran), len(self.running)),
893 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000894 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000895 print(
896 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
897 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000898 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000899 print(
900 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000901 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000903 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000904 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000905
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000906 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000907 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000908 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000909 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000910 # To get back the stack location correctly, the raise a, b, c form must be
911 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000912 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000913 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
914 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000915 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000916 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000917
maruel@chromium.org3742c842010-09-09 19:27:14 +0000918 def _flush_terminated_threads(self):
919 """Flush threads that have terminated."""
920 running = self.running
921 self.running = []
922 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000923 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000924 self.running.append(t)
925 else:
926 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000927 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000928 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000929 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000930 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000931 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000932 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000933 if t.item.name in self.ran:
934 raise Error(
935 'gclient is confused, "%s" is already in "%s"' % (
936 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000937 if not t.item.name in self.ran:
938 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000939
940 def _run_one_task(self, task_item, args, kwargs):
941 if self.jobs > 1:
942 # Start the thread.
943 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000944 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000945 self.running.append(new_thread)
946 new_thread.start()
947 else:
948 # Run the 'thread' inside the main thread. Don't try to catch any
949 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 try:
951 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000952 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000953 task_item.run(*args, **kwargs)
954 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000955 print(
956 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000957 self.ran.append(task_item.name)
958 if self.verbose:
959 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000960 print('')
961 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000962 if self.progress:
963 self.progress.update(1, ', '.join(t.item.name for t in self.running))
964 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000965 print(
966 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 raise
968 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000969 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 raise
971
maruel@chromium.org3742c842010-09-09 19:27:14 +0000972
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000973 class _Worker(threading.Thread):
974 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000975 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000976 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000977 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000978 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000979 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000980 self.args = args
981 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000982 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000983
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000984 def run(self):
985 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000986 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000987 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000988 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000989 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000990 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000991 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000992 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000993 print(
994 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000995 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000996 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000997 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000998 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000999 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001000 except Exception:
1001 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001002 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001003 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001004 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001005 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001006 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001007 work_queue.ready_cond.acquire()
1008 try:
1009 work_queue.ready_cond.notifyAll()
1010 finally:
1011 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001012
1013
agable92bec4f2016-08-24 09:27:27 -07001014def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001015 """Returns the most plausible editor to use.
1016
1017 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001018 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001019 - core.editor git configuration variable (if supplied by git-cl)
1020 - VISUAL environment variable
1021 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001022 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001023
1024 In the case of git-cl, this matches git's behaviour, except that it does not
1025 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001026 """
agable92bec4f2016-08-24 09:27:27 -07001027 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001028 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001029 editor = os.environ.get('VISUAL')
1030 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001031 editor = os.environ.get('EDITOR')
1032 if not editor:
1033 if sys.platform.startswith('win'):
1034 editor = 'notepad'
1035 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001036 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001037 return editor
1038
1039
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001040def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001041 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001042 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001043 # Make sure CRLF is handled properly by requiring none.
1044 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001045 print(
1046 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001047 fileobj = os.fdopen(file_handle, 'w')
1048 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001049 content = re.sub('\r?\n', '\n', content)
1050 # Some editors complain when the file doesn't end in \n.
1051 if not content.endswith('\n'):
1052 content += '\n'
1053 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001054 fileobj.close()
1055
1056 try:
agable92bec4f2016-08-24 09:27:27 -07001057 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001058 if not editor:
1059 return None
1060 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001061 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1062 # Msysgit requires the usage of 'env' to be present.
1063 cmd = 'env ' + cmd
1064 try:
1065 # shell=True to allow the shell to handle all forms of quotes in
1066 # $EDITOR.
1067 subprocess2.check_call(cmd, shell=True)
1068 except subprocess2.CalledProcessError:
1069 return None
1070 return FileRead(filename)
1071 finally:
1072 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001073
1074
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001075def UpgradeToHttps(url):
1076 """Upgrades random urls to https://.
1077
1078 Do not touch unknown urls like ssh:// or git://.
1079 Do not touch http:// urls with a port number,
1080 Fixes invalid GAE url.
1081 """
1082 if not url:
1083 return url
1084 if not re.match(r'[a-z\-]+\://.*', url):
1085 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1086 # relative url and will use http:///foo. Note that it defaults to http://
1087 # for compatibility with naked url like "localhost:8080".
1088 url = 'http://%s' % url
1089 parsed = list(urlparse.urlparse(url))
1090 # Do not automatically upgrade http to https if a port number is provided.
1091 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1092 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001093 return urlparse.urlunparse(parsed)
1094
1095
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001096def ParseCodereviewSettingsContent(content):
1097 """Process a codereview.settings file properly."""
1098 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1099 try:
1100 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1101 except ValueError:
1102 raise Error(
1103 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001104 def fix_url(key):
1105 if keyvals.get(key):
1106 keyvals[key] = UpgradeToHttps(keyvals[key])
1107 fix_url('CODE_REVIEW_SERVER')
1108 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001109 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001110
1111
1112def NumLocalCpus():
1113 """Returns the number of processors.
1114
dnj@chromium.org530523b2015-01-07 19:54:57 +00001115 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1116 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1117 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001118 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001119 # Surround the entire thing in try/except; no failure here should stop gclient
1120 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001121 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001122 # Use multiprocessing to get CPU count. This may raise
1123 # NotImplementedError.
1124 try:
1125 import multiprocessing
1126 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001127 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001128 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001129 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001130 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1131 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1132
1133 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1134 if 'NUMBER_OF_PROCESSORS' in os.environ:
1135 return int(os.environ['NUMBER_OF_PROCESSORS'])
1136 except Exception as e:
1137 logging.exception("Exception raised while probing CPU count: %s", e)
1138
1139 logging.debug('Failed to get CPU count. Defaulting to 1.')
1140 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001141
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001142
szager@chromium.orgfc616382014-03-18 20:32:04 +00001143def DefaultDeltaBaseCacheLimit():
1144 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1145
1146 The primary constraint is the address space of virtual memory. The cache
1147 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1148 parameter is set too high.
1149 """
1150 if platform.architecture()[0].startswith('64'):
1151 return '2g'
1152 else:
1153 return '512m'
1154
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001155
szager@chromium.orgff113292014-03-25 06:02:08 +00001156def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001157 """Return reasonable default values for configuring git-index-pack.
1158
1159 Experiments suggest that higher values for pack.threads don't improve
1160 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001161 cache_limit = DefaultDeltaBaseCacheLimit()
1162 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1163 if url in THREADED_INDEX_PACK_BLACKLIST:
1164 result.extend(['-c', 'pack.threads=1'])
1165 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001166
1167
1168def FindExecutable(executable):
1169 """This mimics the "which" utility."""
1170 path_folders = os.environ.get('PATH').split(os.pathsep)
1171
1172 for path_folder in path_folders:
1173 target = os.path.join(path_folder, executable)
1174 # Just incase we have some ~/blah paths.
1175 target = os.path.abspath(os.path.expanduser(target))
1176 if os.path.isfile(target) and os.access(target, os.X_OK):
1177 return target
1178 if sys.platform.startswith('win'):
1179 for suffix in ('.bat', '.cmd', '.exe'):
1180 alt_target = target + suffix
1181 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1182 return alt_target
1183 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001184
1185
1186def freeze(obj):
1187 """Takes a generic object ``obj``, and returns an immutable version of it.
1188
1189 Supported types:
1190 * dict / OrderedDict -> FrozenDict
1191 * list -> tuple
1192 * set -> frozenset
1193 * any object with a working __hash__ implementation (assumes that hashable
1194 means immutable)
1195
1196 Will raise TypeError if you pass an object which is not hashable.
1197 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001198 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001199 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001200 elif isinstance(obj, (list, tuple)):
1201 return tuple(freeze(i) for i in obj)
1202 elif isinstance(obj, set):
1203 return frozenset(freeze(i) for i in obj)
1204 else:
1205 hash(obj)
1206 return obj
1207
1208
1209class FrozenDict(collections.Mapping):
1210 """An immutable OrderedDict.
1211
1212 Modified From: http://stackoverflow.com/a/2704866
1213 """
1214 def __init__(self, *args, **kwargs):
1215 self._d = collections.OrderedDict(*args, **kwargs)
1216
1217 # Calculate the hash immediately so that we know all the items are
1218 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001219 self._hash = functools.reduce(
1220 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001221
1222 def __eq__(self, other):
1223 if not isinstance(other, collections.Mapping):
1224 return NotImplemented
1225 if self is other:
1226 return True
1227 if len(self) != len(other):
1228 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001229 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001230 if k not in other or other[k] != v:
1231 return False
1232 return True
1233
1234 def __iter__(self):
1235 return iter(self._d)
1236
1237 def __len__(self):
1238 return len(self._d)
1239
1240 def __getitem__(self, key):
1241 return self._d[key]
1242
1243 def __hash__(self):
1244 return self._hash
1245
1246 def __repr__(self):
1247 return 'FrozenDict(%r)' % (self._d.items(),)