blob: 90cee87e7d1e17ea2e6de0b45eb0349a5eba0ad8 [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
msb@chromium.orgac915bb2009-11-13 17:03:01 +000020import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000021import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000022import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000024import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000025import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000026import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000027import subprocess2
28
Raul Tambreb946b232019-03-26 14:48:46 +000029if sys.version_info.major == 2:
30 from cStringIO import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000031 import Queue as queue
32 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000033else:
34 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000035 import queue
36 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000037
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000038
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000039RETRY_MAX = 3
40RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000041START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000042
43
borenet@google.com6a9b1682014-03-24 18:35:23 +000044_WARNINGS = []
45
46
szager@chromium.orgff113292014-03-25 06:02:08 +000047# These repos are known to cause OOM errors on 32-bit platforms, due the the
48# very large objects they contain. It is not safe to use threaded index-pack
49# when cloning/fetching them.
50THREADED_INDEX_PACK_BLACKLIST = [
51 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
52]
53
Raul Tambreb946b232019-03-26 14:48:46 +000054"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
55if sys.version_info.major == 2:
56 # We have to use exec to avoid a SyntaxError in Python 3.
57 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
58else:
59 def reraise(typ, value, tb=None):
60 if value is None:
61 value = typ()
62 if value.__traceback__ is not tb:
63 raise value.with_traceback(tb)
64 raise value
65
szager@chromium.orgff113292014-03-25 06:02:08 +000066
maruel@chromium.org66c83e62010-09-07 14:18:45 +000067class Error(Exception):
68 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000069 def __init__(self, msg, *args, **kwargs):
70 index = getattr(threading.currentThread(), 'index', 0)
71 if index:
72 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
73 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000074
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000075
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000076def Elapsed(until=None):
77 if until is None:
78 until = datetime.datetime.now()
79 return str(until - START).partition('.')[0]
80
81
borenet@google.com6a9b1682014-03-24 18:35:23 +000082def PrintWarnings():
83 """Prints any accumulated warnings."""
84 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000085 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000086 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000087 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000088
89
90def AddWarning(msg):
91 """Adds the given warning message to the list of accumulated warnings."""
92 _WARNINGS.append(msg)
93
94
msb@chromium.orgac915bb2009-11-13 17:03:01 +000095def SplitUrlRevision(url):
96 """Splits url and returns a two-tuple: url, rev"""
97 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +000098 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +000099 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000100 components = re.search(regex, url).groups()
101 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000102 components = url.rsplit('@', 1)
103 if re.match(r'^\w+\@', url) and '@' not in components[0]:
104 components = [url]
105
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000106 if len(components) == 1:
107 components += [None]
108 return tuple(components)
109
110
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000111def IsGitSha(revision):
112 """Returns true if the given string is a valid hex-encoded sha"""
113 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
114
115
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200116def IsFullGitSha(revision):
117 """Returns true if the given string is a valid hex-encoded full sha"""
118 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
119
120
floitsch@google.comeaab7842011-04-28 09:07:58 +0000121def IsDateRevision(revision):
122 """Returns true if the given revision is of the form "{ ... }"."""
123 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
124
125
126def MakeDateRevision(date):
127 """Returns a revision representing the latest revision before the given
128 date."""
129 return "{" + date + "}"
130
131
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000132def SyntaxErrorToError(filename, e):
133 """Raises a gclient_utils.Error exception with the human readable message"""
134 try:
135 # Try to construct a human readable error message
136 if filename:
137 error_message = 'There is a syntax error in %s\n' % filename
138 else:
139 error_message = 'There is a syntax error\n'
140 error_message += 'Line #%s, character %s: "%s"' % (
141 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
142 except:
143 # Something went wrong, re-raise the original exception
144 raise e
145 else:
146 raise Error(error_message)
147
148
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000149class PrintableObject(object):
150 def __str__(self):
151 output = ''
152 for i in dir(self):
153 if i.startswith('__'):
154 continue
155 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
156 return output
157
158
Edward Lemur419c92f2019-10-25 22:17:49 +0000159def FileRead(filename, mode='rbU'):
160 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000161 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000162 if mode == 'rbU' and sys.version_info.major == 3:
163 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000164 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000165 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000166 if isinstance(s, bytes):
167 return s.decode('utf-8', 'replace')
168 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000169
170
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000172 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000173 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000174
175
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000176@contextlib.contextmanager
177def temporary_directory(**kwargs):
178 tdir = tempfile.mkdtemp(**kwargs)
179 try:
180 yield tdir
181 finally:
182 if tdir:
183 rmtree(tdir)
184
185
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000186def safe_rename(old, new):
187 """Renames a file reliably.
188
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000189 Sometimes os.rename does not work because a dying git process keeps a handle
190 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000191 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000192 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000193 """
194 # roughly 10s
195 retries = 100
196 for i in range(retries):
197 try:
198 os.rename(old, new)
199 break
200 except OSError:
201 if i == (retries - 1):
202 # Give up.
203 raise
204 # retry
205 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
206 time.sleep(0.1)
207
208
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000209def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000210 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000211 os.remove(path)
212 else:
213 rmtree(path)
214
215
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000216def rmtree(path):
217 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000218
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000219 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
221 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700222 are read-only. We need to be able to force the files to be writable (i.e.,
223 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
225 Even with all this, Windows still sometimes fails to delete a file, citing
226 a permission error (maybe something to do with antivirus scans or disk
227 indexing). The best suggestion any of the user forums had was to wait a
228 bit and try again, so we do that too. It's hand-waving, but sometimes it
229 works. :/
230
231 On POSIX systems, things are a little bit simpler. The modes of the files
232 to be deleted doesn't matter, only the modes of the directories containing
233 them are significant. As the directory tree is traversed, each directory
234 has its mode set appropriately before descending into it. This should
235 result in the entire tree being removed, with the possible exception of
236 *path itself, because nothing attempts to change the mode of its parent.
237 Doing so would be hazardous, as it's not a directory slated for removal.
238 In the ordinary case, this is not a problem: for our purposes, the user
239 will never lack write permission on *path's parent.
240 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000241 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000242 return
243
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000244 if os.path.islink(path) or not os.path.isdir(path):
245 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000246
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000247 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000248 # Give up and use cmd.exe's rd command.
249 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000250 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000251 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
252 if exitcode == 0:
253 return
254 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000255 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000256 time.sleep(3)
257 raise Exception('Failed to remove path %s' % path)
258
259 # On POSIX systems, we need the x-bit set on the directory to access it,
260 # the r-bit to see its contents, and the w-bit to remove files from it.
261 # The actual modes of the files within the directory is irrelevant.
262 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000264 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000265 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000266
267 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000268 # If fullpath is a symbolic link that points to a directory, isdir will
269 # be True, but we don't want to descend into that as a directory, we just
270 # want to remove the link. Check islink and treat links as ordinary files
271 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000272 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000273 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000274 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000275 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000276 # Recurse.
277 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000279 remove(os.rmdir, path)
280
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000282def safe_makedirs(tree):
283 """Creates the directory in a safe manner.
284
285 Because multiple threads can create these directories concurently, trap the
286 exception and pass on.
287 """
288 count = 0
289 while not os.path.exists(tree):
290 count += 1
291 try:
292 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000293 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000294 # 17 POSIX, 183 Windows
295 if e.errno not in (17, 183):
296 raise
297 if count > 40:
298 # Give up.
299 raise
300
301
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000302def CommandToStr(args):
303 """Converts an arg list into a shell escaped string."""
304 return ' '.join(pipes.quote(arg) for arg in args)
305
306
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000307class Wrapper(object):
308 """Wraps an object, acting as a transparent proxy for all properties by
309 default.
310 """
311 def __init__(self, wrapped):
312 self._wrapped = wrapped
313
314 def __getattr__(self, name):
315 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000316
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000317
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000318class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000319 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320 def __init__(self, wrapped, delay):
321 super(AutoFlush, self).__init__(wrapped)
322 if not hasattr(self, 'lock'):
323 self.lock = threading.Lock()
324 self.__last_flushed_at = time.time()
325 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000326
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000327 @property
328 def autoflush(self):
329 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000330
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331 def write(self, out, *args, **kwargs):
332 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000333 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000334 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000335 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000337 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000339 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000341 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000343
344
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000345class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000346 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 threads with a NN> prefix.
348 """
349 def __init__(self, wrapped, include_zero=False):
350 super(Annotated, self).__init__(wrapped)
351 if not hasattr(self, 'lock'):
352 self.lock = threading.Lock()
353 self.__output_buffers = {}
354 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000355 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000356
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357 @property
358 def annotated(self):
359 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000360
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000361 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000362 # Store as bytes to ensure Unicode characters get output correctly.
363 if not isinstance(out, bytes):
364 out = out.encode('utf-8')
365
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000366 index = getattr(threading.currentThread(), 'index', 0)
367 if not index and not self.__include_zero:
368 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000369 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000370
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000371 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000372 try:
373 # Use a dummy array to hold the string so the code can be lockless.
374 # Strings are immutable, requiring to keep a lock for the whole dictionary
375 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000377 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000378 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000379 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000380 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000382
383 # Continue lockless.
384 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000385 while True:
386 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000387 cr_loc = obj[0].find(b'\r')
388 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000389 if cr_loc == lf_loc == -1:
390 break
391 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000392 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000393 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000394 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000395 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000396 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000397 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000398 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000399 obj[0] = remaining
400
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000402 """Flush buffered output."""
403 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000404 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000405 try:
406 # Detect threads no longer existing.
407 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000408 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000410 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000412 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000413 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000414 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416
417 # Don't keep the lock while writting. Will append \n when it shouldn't.
418 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000419 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000420 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423
424def MakeFileAutoFlush(fileobj, delay=10):
425 autoflush = getattr(fileobj, 'autoflush', None)
426 if autoflush:
427 autoflush.delay = delay
428 return fileobj
429 return AutoFlush(fileobj, delay)
430
431
432def MakeFileAnnotated(fileobj, include_zero=False):
433 if getattr(fileobj, 'annotated', None):
434 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000435 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000436
437
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000438GCLIENT_CHILDREN = []
439GCLIENT_CHILDREN_LOCK = threading.Lock()
440
441
442class GClientChildren(object):
443 @staticmethod
444 def add(popen_obj):
445 with GCLIENT_CHILDREN_LOCK:
446 GCLIENT_CHILDREN.append(popen_obj)
447
448 @staticmethod
449 def remove(popen_obj):
450 with GCLIENT_CHILDREN_LOCK:
451 GCLIENT_CHILDREN.remove(popen_obj)
452
453 @staticmethod
454 def _attemptToKillChildren():
455 global GCLIENT_CHILDREN
456 with GCLIENT_CHILDREN_LOCK:
457 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
458
459 for zombie in zombies:
460 try:
461 zombie.kill()
462 except OSError:
463 pass
464
465 with GCLIENT_CHILDREN_LOCK:
466 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
467
468 @staticmethod
469 def _areZombies():
470 with GCLIENT_CHILDREN_LOCK:
471 return bool(GCLIENT_CHILDREN)
472
473 @staticmethod
474 def KillAllRemainingChildren():
475 GClientChildren._attemptToKillChildren()
476
477 if GClientChildren._areZombies():
478 time.sleep(0.5)
479 GClientChildren._attemptToKillChildren()
480
481 with GCLIENT_CHILDREN_LOCK:
482 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000483 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000484 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000485 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000486
487
Edward Lemur24146be2019-08-01 21:44:52 +0000488def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
489 show_header=False, always_show_header=False, retry=False,
490 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000491 """Runs a command and calls back a filter function if needed.
492
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000493 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000494 print_stdout: If True, the command's stdout is forwarded to stdout.
495 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000496 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000497 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000498 show_header: Whether to display a header before the command output.
499 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000500 retry: If the process exits non-zero, sleep for a brief interval and try
501 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000502
503 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000504
505 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000506 """
Edward Lemur24146be2019-08-01 21:44:52 +0000507 def show_header_if_necessary(needs_header, attempt):
508 """Show the header at most once."""
509 if not needs_header[0]:
510 return
511
512 needs_header[0] = False
513 # Automatically generated header. We only prepend a newline if
514 # always_show_header is false, since it usually indicates there's an
515 # external progress display, and it's better not to clobber it in that case.
516 header = '' if always_show_header else '\n'
517 header += '________ running \'%s\' in \'%s\'' % (
518 ' '.join(args), kwargs.get('cwd', '.'))
519 if attempt:
520 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
521 header += '\n'
522
523 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000524 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
525 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000526 if filter_fn:
527 filter_fn(header)
528
529 def filter_line(command_output, line_start):
530 """Extract the last line from command output and filter it."""
531 if not filter_fn or line_start is None:
532 return
533 command_output.seek(line_start)
534 filter_fn(command_output.read().decode('utf-8'))
535
536 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
537 # byte inputs and sys.stdout.buffer must be used instead.
538 if print_stdout:
539 sys.stdout.flush()
540 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
541 else:
542 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000543
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000544 sleep_interval = RETRY_INITIAL_SLEEP
545 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000546 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000547 kid = subprocess2.Popen(
548 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
549 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000550
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000551 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000552
Edward Lemur24146be2019-08-01 21:44:52 +0000553 # Store the output of the command regardless of the value of print_stdout or
554 # filter_fn.
555 command_output = io.BytesIO()
556
557 # Passed as a list for "by ref" semantics.
558 needs_header = [show_header]
559 if always_show_header:
560 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000561
562 # Also, we need to forward stdout to prevent weird re-ordering of output.
563 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700564 # normally buffering is done for each line, but if the process requests
565 # input, no end-of-line character is output after the prompt and it would
566 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000568 line_start = None
569 while True:
570 in_byte = kid.stdout.read(1)
571 is_newline = in_byte in (b'\n', b'\r')
572 if not in_byte:
573 break
574
575 show_header_if_necessary(needs_header, attempt)
576
577 if is_newline:
578 filter_line(command_output, line_start)
579 line_start = None
580 elif line_start is None:
581 line_start = command_output.tell()
582
583 stdout_write(in_byte)
584 command_output.write(in_byte)
585
586 # Flush the rest of buffered output.
587 sys.stdout.flush()
588 if line_start is not None:
589 filter_line(command_output, line_start)
590
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000591 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000592 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000593
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000594 # Don't put this in a 'finally,' since the child may still run if we get
595 # an exception.
596 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000597
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000598 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000599 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000600 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000601
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000603 return command_output.getvalue()
604
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000605 if not retry:
606 break
Edward Lemur24146be2019-08-01 21:44:52 +0000607
Raul Tambreb946b232019-03-26 14:48:46 +0000608 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
609 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000610 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000611 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000612
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000613 raise subprocess2.CalledProcessError(
614 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000615
616
agable@chromium.org5a306a22014-02-24 22:13:59 +0000617class GitFilter(object):
618 """A filter_fn implementation for quieting down git output messages.
619
620 Allows a custom function to skip certain lines (predicate), and will throttle
621 the output of percentage completed lines to only output every X seconds.
622 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000623 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000624
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000625 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626 """
627 Args:
628 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
629 XX% complete messages) to only be printed at least |time_throttle|
630 seconds apart.
631 predicate (f(line)): An optional function which is invoked for every line.
632 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000633 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000634 """
Edward Lemur24146be2019-08-01 21:44:52 +0000635 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636 self.last_time = 0
637 self.time_throttle = time_throttle
638 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000639 self.out_fh = out_fh or sys.stdout
640 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000641
642 def __call__(self, line):
643 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000644 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000645 if esc > -1:
646 line = line[:esc]
647 if self.predicate and not self.predicate(line):
648 return
649 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000650 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000651 if match:
652 if match.group(1) != self.progress_prefix:
653 self.progress_prefix = match.group(1)
654 elif now - self.last_time < self.time_throttle:
655 return
656 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000657 if not self.first_line:
658 self.out_fh.write('[%s] ' % Elapsed())
659 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000660 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000661
662
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000663def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000664 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000665
rcui@google.com13595ff2011-10-13 01:25:07 +0000666 Returns nearest upper-level directory with the passed in file.
667 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000668 if not path:
669 path = os.getcwd()
670 path = os.path.realpath(path)
671 while True:
672 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000673 if os.path.exists(file_path):
674 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000675 (new_path, _) = os.path.split(path)
676 if new_path == path:
677 return None
678 path = new_path
679
680
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000681def GetMacWinOrLinux():
682 """Returns 'mac', 'win', or 'linux', matching the current platform."""
683 if sys.platform.startswith(('cygwin', 'win')):
684 return 'win'
685 elif sys.platform.startswith('linux'):
686 return 'linux'
687 elif sys.platform == 'darwin':
688 return 'mac'
689 raise Error('Unknown platform: ' + sys.platform)
690
691
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000692def GetGClientRootAndEntries(path=None):
693 """Returns the gclient root and the dict of entries."""
694 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000695 root = FindFileUpwards(config_file, path)
696 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000697 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000698 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000699 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000700 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000701 with open(config_path) as config:
702 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000703 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:
Edward Lemura877ee62019-09-03 20:23:17 +0000915 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000916 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
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001221 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001222 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(),)