blob: 9f4de36ad1626ce4fda3ae42b7008b31af5361d0 [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
Raul Tambre6693d092020-02-19 20:36:45 +000031 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000032 import Queue as queue
33 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000034else:
Raul Tambre6693d092020-02-19 20:36:45 +000035 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000036 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000037 import queue
38 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000039
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000040
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000041RETRY_MAX = 3
42RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000043START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000044
45
borenet@google.com6a9b1682014-03-24 18:35:23 +000046_WARNINGS = []
47
48
szager@chromium.orgff113292014-03-25 06:02:08 +000049# These repos are known to cause OOM errors on 32-bit platforms, due the the
50# very large objects they contain. It is not safe to use threaded index-pack
51# when cloning/fetching them.
52THREADED_INDEX_PACK_BLACKLIST = [
53 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
54]
55
Raul Tambreb946b232019-03-26 14:48:46 +000056"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
57if sys.version_info.major == 2:
58 # We have to use exec to avoid a SyntaxError in Python 3.
59 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
60else:
61 def reraise(typ, value, tb=None):
62 if value is None:
63 value = typ()
64 if value.__traceback__ is not tb:
65 raise value.with_traceback(tb)
66 raise value
67
szager@chromium.orgff113292014-03-25 06:02:08 +000068
maruel@chromium.org66c83e62010-09-07 14:18:45 +000069class Error(Exception):
70 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000071 def __init__(self, msg, *args, **kwargs):
72 index = getattr(threading.currentThread(), 'index', 0)
73 if index:
74 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
75 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000076
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000077
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000078def Elapsed(until=None):
79 if until is None:
80 until = datetime.datetime.now()
81 return str(until - START).partition('.')[0]
82
83
borenet@google.com6a9b1682014-03-24 18:35:23 +000084def PrintWarnings():
85 """Prints any accumulated warnings."""
86 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000087 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000088 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000089 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000090
91
92def AddWarning(msg):
93 """Adds the given warning message to the list of accumulated warnings."""
94 _WARNINGS.append(msg)
95
96
msb@chromium.orgac915bb2009-11-13 17:03:01 +000097def SplitUrlRevision(url):
98 """Splits url and returns a two-tuple: url, rev"""
99 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000100 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000101 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000102 components = re.search(regex, url).groups()
103 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000104 components = url.rsplit('@', 1)
105 if re.match(r'^\w+\@', url) and '@' not in components[0]:
106 components = [url]
107
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000108 if len(components) == 1:
109 components += [None]
110 return tuple(components)
111
112
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000113def IsGitSha(revision):
114 """Returns true if the given string is a valid hex-encoded sha"""
115 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
116
117
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200118def IsFullGitSha(revision):
119 """Returns true if the given string is a valid hex-encoded full sha"""
120 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
121
122
floitsch@google.comeaab7842011-04-28 09:07:58 +0000123def IsDateRevision(revision):
124 """Returns true if the given revision is of the form "{ ... }"."""
125 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
126
127
128def MakeDateRevision(date):
129 """Returns a revision representing the latest revision before the given
130 date."""
131 return "{" + date + "}"
132
133
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000134def SyntaxErrorToError(filename, e):
135 """Raises a gclient_utils.Error exception with the human readable message"""
136 try:
137 # Try to construct a human readable error message
138 if filename:
139 error_message = 'There is a syntax error in %s\n' % filename
140 else:
141 error_message = 'There is a syntax error\n'
142 error_message += 'Line #%s, character %s: "%s"' % (
143 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
144 except:
145 # Something went wrong, re-raise the original exception
146 raise e
147 else:
148 raise Error(error_message)
149
150
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000151class PrintableObject(object):
152 def __str__(self):
153 output = ''
154 for i in dir(self):
155 if i.startswith('__'):
156 continue
157 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
158 return output
159
160
Edward Lemur419c92f2019-10-25 22:17:49 +0000161def FileRead(filename, mode='rbU'):
162 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000163 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000164 if mode == 'rbU' and sys.version_info.major == 3:
165 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000166 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000167 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000168 if isinstance(s, bytes):
169 return s.decode('utf-8', 'replace')
170 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171
172
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000173def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000174 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176
177
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000178@contextlib.contextmanager
179def temporary_directory(**kwargs):
180 tdir = tempfile.mkdtemp(**kwargs)
181 try:
182 yield tdir
183 finally:
184 if tdir:
185 rmtree(tdir)
186
187
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000188def safe_rename(old, new):
189 """Renames a file reliably.
190
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000191 Sometimes os.rename does not work because a dying git process keeps a handle
192 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000193 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000194 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000195 """
196 # roughly 10s
197 retries = 100
198 for i in range(retries):
199 try:
200 os.rename(old, new)
201 break
202 except OSError:
203 if i == (retries - 1):
204 # Give up.
205 raise
206 # retry
207 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
208 time.sleep(0.1)
209
210
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000211def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000212 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000213 os.remove(path)
214 else:
215 rmtree(path)
216
217
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000218def rmtree(path):
219 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000221 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000222
223 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700224 are read-only. We need to be able to force the files to be writable (i.e.,
225 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000226
227 Even with all this, Windows still sometimes fails to delete a file, citing
228 a permission error (maybe something to do with antivirus scans or disk
229 indexing). The best suggestion any of the user forums had was to wait a
230 bit and try again, so we do that too. It's hand-waving, but sometimes it
231 works. :/
232
233 On POSIX systems, things are a little bit simpler. The modes of the files
234 to be deleted doesn't matter, only the modes of the directories containing
235 them are significant. As the directory tree is traversed, each directory
236 has its mode set appropriately before descending into it. This should
237 result in the entire tree being removed, with the possible exception of
238 *path itself, because nothing attempts to change the mode of its parent.
239 Doing so would be hazardous, as it's not a directory slated for removal.
240 In the ordinary case, this is not a problem: for our purposes, the user
241 will never lack write permission on *path's parent.
242 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000243 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000244 return
245
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000246 if os.path.islink(path) or not os.path.isdir(path):
247 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000248
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000249 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000250 # Give up and use cmd.exe's rd command.
251 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000252 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000253 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
254 if exitcode == 0:
255 return
256 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000257 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000258 time.sleep(3)
259 raise Exception('Failed to remove path %s' % path)
260
261 # On POSIX systems, we need the x-bit set on the directory to access it,
262 # the r-bit to see its contents, and the w-bit to remove files from it.
263 # The actual modes of the files within the directory is irrelevant.
264 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000265
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000266 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000267 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000268
269 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270 # If fullpath is a symbolic link that points to a directory, isdir will
271 # be True, but we don't want to descend into that as a directory, we just
272 # want to remove the link. Check islink and treat links as ordinary files
273 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000274 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000275 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000276 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000278 # Recurse.
279 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000280
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000281 remove(os.rmdir, path)
282
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000283
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000284def safe_makedirs(tree):
285 """Creates the directory in a safe manner.
286
287 Because multiple threads can create these directories concurently, trap the
288 exception and pass on.
289 """
290 count = 0
291 while not os.path.exists(tree):
292 count += 1
293 try:
294 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000295 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000296 # 17 POSIX, 183 Windows
297 if e.errno not in (17, 183):
298 raise
299 if count > 40:
300 # Give up.
301 raise
302
303
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000304def CommandToStr(args):
305 """Converts an arg list into a shell escaped string."""
306 return ' '.join(pipes.quote(arg) for arg in args)
307
308
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000309class Wrapper(object):
310 """Wraps an object, acting as a transparent proxy for all properties by
311 default.
312 """
313 def __init__(self, wrapped):
314 self._wrapped = wrapped
315
316 def __getattr__(self, name):
317 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000318
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000319
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000321 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000322 def __init__(self, wrapped, delay):
323 super(AutoFlush, self).__init__(wrapped)
324 if not hasattr(self, 'lock'):
325 self.lock = threading.Lock()
326 self.__last_flushed_at = time.time()
327 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000328
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000329 @property
330 def autoflush(self):
331 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000332
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 def write(self, out, *args, **kwargs):
334 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000335 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000336 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000337 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000338 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000339 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000341 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000342 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000343 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000345
346
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000348 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349 threads with a NN> prefix.
350 """
351 def __init__(self, wrapped, include_zero=False):
352 super(Annotated, self).__init__(wrapped)
353 if not hasattr(self, 'lock'):
354 self.lock = threading.Lock()
355 self.__output_buffers = {}
356 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000357 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000358
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000359 @property
360 def annotated(self):
361 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000362
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000363 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000364 # Store as bytes to ensure Unicode characters get output correctly.
365 if not isinstance(out, bytes):
366 out = out.encode('utf-8')
367
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368 index = getattr(threading.currentThread(), 'index', 0)
369 if not index and not self.__include_zero:
370 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000371 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000372
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000374 try:
375 # Use a dummy array to hold the string so the code can be lockless.
376 # Strings are immutable, requiring to keep a lock for the whole dictionary
377 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000378 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000379 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000380 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000382 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384
385 # Continue lockless.
386 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000387 while True:
388 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000389 cr_loc = obj[0].find(b'\r')
390 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000391 if cr_loc == lf_loc == -1:
392 break
393 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000394 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000395 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000396 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000397 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000398 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000399 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000400 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000401 obj[0] = remaining
402
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000403 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000404 """Flush buffered output."""
405 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000406 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000407 try:
408 # Detect threads no longer existing.
409 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000410 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000412 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000413 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000414 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000415 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000416 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000418
419 # Don't keep the lock while writting. Will append \n when it shouldn't.
420 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000421 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000422 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000424
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425
426def MakeFileAutoFlush(fileobj, delay=10):
427 autoflush = getattr(fileobj, 'autoflush', None)
428 if autoflush:
429 autoflush.delay = delay
430 return fileobj
431 return AutoFlush(fileobj, delay)
432
433
434def MakeFileAnnotated(fileobj, include_zero=False):
435 if getattr(fileobj, 'annotated', None):
436 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000437 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000438
439
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000440GCLIENT_CHILDREN = []
441GCLIENT_CHILDREN_LOCK = threading.Lock()
442
443
444class GClientChildren(object):
445 @staticmethod
446 def add(popen_obj):
447 with GCLIENT_CHILDREN_LOCK:
448 GCLIENT_CHILDREN.append(popen_obj)
449
450 @staticmethod
451 def remove(popen_obj):
452 with GCLIENT_CHILDREN_LOCK:
453 GCLIENT_CHILDREN.remove(popen_obj)
454
455 @staticmethod
456 def _attemptToKillChildren():
457 global GCLIENT_CHILDREN
458 with GCLIENT_CHILDREN_LOCK:
459 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
460
461 for zombie in zombies:
462 try:
463 zombie.kill()
464 except OSError:
465 pass
466
467 with GCLIENT_CHILDREN_LOCK:
468 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
469
470 @staticmethod
471 def _areZombies():
472 with GCLIENT_CHILDREN_LOCK:
473 return bool(GCLIENT_CHILDREN)
474
475 @staticmethod
476 def KillAllRemainingChildren():
477 GClientChildren._attemptToKillChildren()
478
479 if GClientChildren._areZombies():
480 time.sleep(0.5)
481 GClientChildren._attemptToKillChildren()
482
483 with GCLIENT_CHILDREN_LOCK:
484 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000485 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000486 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000487 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000488
489
Edward Lemur24146be2019-08-01 21:44:52 +0000490def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
491 show_header=False, always_show_header=False, retry=False,
492 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000493 """Runs a command and calls back a filter function if needed.
494
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000495 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000496 print_stdout: If True, the command's stdout is forwarded to stdout.
497 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000498 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000499 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000500 show_header: Whether to display a header before the command output.
501 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000502 retry: If the process exits non-zero, sleep for a brief interval and try
503 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000504
505 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000506
507 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000508 """
Edward Lemur24146be2019-08-01 21:44:52 +0000509 def show_header_if_necessary(needs_header, attempt):
510 """Show the header at most once."""
511 if not needs_header[0]:
512 return
513
514 needs_header[0] = False
515 # Automatically generated header. We only prepend a newline if
516 # always_show_header is false, since it usually indicates there's an
517 # external progress display, and it's better not to clobber it in that case.
518 header = '' if always_show_header else '\n'
519 header += '________ running \'%s\' in \'%s\'' % (
520 ' '.join(args), kwargs.get('cwd', '.'))
521 if attempt:
522 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
523 header += '\n'
524
525 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000526 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
527 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000528 if filter_fn:
529 filter_fn(header)
530
531 def filter_line(command_output, line_start):
532 """Extract the last line from command output and filter it."""
533 if not filter_fn or line_start is None:
534 return
535 command_output.seek(line_start)
536 filter_fn(command_output.read().decode('utf-8'))
537
538 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
539 # byte inputs and sys.stdout.buffer must be used instead.
540 if print_stdout:
541 sys.stdout.flush()
542 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
543 else:
544 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000545
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000546 sleep_interval = RETRY_INITIAL_SLEEP
547 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000548 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000549 kid = subprocess2.Popen(
550 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
551 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000552
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000553 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000554
Edward Lemur24146be2019-08-01 21:44:52 +0000555 # Store the output of the command regardless of the value of print_stdout or
556 # filter_fn.
557 command_output = io.BytesIO()
558
559 # Passed as a list for "by ref" semantics.
560 needs_header = [show_header]
561 if always_show_header:
562 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000563
564 # Also, we need to forward stdout to prevent weird re-ordering of output.
565 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700566 # normally buffering is done for each line, but if the process requests
567 # input, no end-of-line character is output after the prompt and it would
568 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000569 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000570 line_start = None
571 while True:
572 in_byte = kid.stdout.read(1)
573 is_newline = in_byte in (b'\n', b'\r')
574 if not in_byte:
575 break
576
577 show_header_if_necessary(needs_header, attempt)
578
579 if is_newline:
580 filter_line(command_output, line_start)
581 line_start = None
582 elif line_start is None:
583 line_start = command_output.tell()
584
585 stdout_write(in_byte)
586 command_output.write(in_byte)
587
588 # Flush the rest of buffered output.
589 sys.stdout.flush()
590 if line_start is not None:
591 filter_line(command_output, line_start)
592
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000593 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000594 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000595
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000596 # Don't put this in a 'finally,' since the child may still run if we get
597 # an exception.
598 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000599
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000600 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000601 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000603
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000604 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000605 return command_output.getvalue()
606
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000607 if not retry:
608 break
Edward Lemur24146be2019-08-01 21:44:52 +0000609
Raul Tambreb946b232019-03-26 14:48:46 +0000610 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
611 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000612 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000613 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000614
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000615 raise subprocess2.CalledProcessError(
616 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000617
618
agable@chromium.org5a306a22014-02-24 22:13:59 +0000619class GitFilter(object):
620 """A filter_fn implementation for quieting down git output messages.
621
622 Allows a custom function to skip certain lines (predicate), and will throttle
623 the output of percentage completed lines to only output every X seconds.
624 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000625 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000626
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000627 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000628 """
629 Args:
630 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
631 XX% complete messages) to only be printed at least |time_throttle|
632 seconds apart.
633 predicate (f(line)): An optional function which is invoked for every line.
634 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000635 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000636 """
Edward Lemur24146be2019-08-01 21:44:52 +0000637 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000638 self.last_time = 0
639 self.time_throttle = time_throttle
640 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000641 self.out_fh = out_fh or sys.stdout
642 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000643
644 def __call__(self, line):
645 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000646 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000647 if esc > -1:
648 line = line[:esc]
649 if self.predicate and not self.predicate(line):
650 return
651 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000652 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000653 if match:
654 if match.group(1) != self.progress_prefix:
655 self.progress_prefix = match.group(1)
656 elif now - self.last_time < self.time_throttle:
657 return
658 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000659 if not self.first_line:
660 self.out_fh.write('[%s] ' % Elapsed())
661 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000662 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000663
664
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000665def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000666 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000667
rcui@google.com13595ff2011-10-13 01:25:07 +0000668 Returns nearest upper-level directory with the passed in file.
669 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000670 if not path:
671 path = os.getcwd()
672 path = os.path.realpath(path)
673 while True:
674 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000675 if os.path.exists(file_path):
676 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000677 (new_path, _) = os.path.split(path)
678 if new_path == path:
679 return None
680 path = new_path
681
682
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000683def GetMacWinOrLinux():
684 """Returns 'mac', 'win', or 'linux', matching the current platform."""
685 if sys.platform.startswith(('cygwin', 'win')):
686 return 'win'
687 elif sys.platform.startswith('linux'):
688 return 'linux'
689 elif sys.platform == 'darwin':
690 return 'mac'
691 raise Error('Unknown platform: ' + sys.platform)
692
693
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000694def GetGClientRootAndEntries(path=None):
695 """Returns the gclient root and the dict of entries."""
696 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000697 root = FindFileUpwards(config_file, path)
698 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000699 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000700 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000701 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000702 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000703 with open(config_path) as config:
704 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000705 config_dir = os.path.dirname(config_path)
706 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000707
708
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000709def lockedmethod(method):
710 """Method decorator that holds self.lock for the duration of the call."""
711 def inner(self, *args, **kwargs):
712 try:
713 try:
714 self.lock.acquire()
715 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000716 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000717 raise
718 return method(self, *args, **kwargs)
719 finally:
720 self.lock.release()
721 return inner
722
723
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000724class WorkItem(object):
725 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000726 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
727 # As a workaround, use a single lock. Yep you read it right. Single lock for
728 # all the 100 objects.
729 lock = threading.Lock()
730
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000731 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000732 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000733 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000734 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000735 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700736 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000737
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000738 def run(self, work_queue):
739 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000740 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000741 pass
742
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000743 @property
744 def name(self):
745 return self._name
746
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000747
748class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000749 """Runs a set of WorkItem that have interdependencies and were WorkItem are
750 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000751
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200752 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000753 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000754
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000755 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000756 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000757 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000758 """jobs specifies the number of concurrent tasks to allow. progress is a
759 Progress instance."""
760 # Set when a thread is done or a new item is enqueued.
761 self.ready_cond = threading.Condition()
762 # Maximum number of concurrent tasks.
763 self.jobs = jobs
764 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000765 self.queued = []
766 # List of strings representing each Dependency.name that was run.
767 self.ran = []
768 # List of items currently running.
769 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000770 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000771 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000772 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000773 self.progress = progress
774 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000775 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000776
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000777 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000778 self.verbose = verbose
779 self.last_join = None
780 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000781
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000782 def enqueue(self, d):
783 """Enqueue one Dependency to be executed later once its requirements are
784 satisfied.
785 """
786 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000787 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000788 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000789 self.queued.append(d)
790 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000791 if self.jobs == 1:
792 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000793 logging.debug('enqueued(%s)' % d.name)
794 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000795 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000796 self.progress.update(0)
797 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000798 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000799 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000801 def out_cb(self, _):
802 self.last_subproc_output = datetime.datetime.now()
803 return True
804
805 @staticmethod
806 def format_task_output(task, comment=''):
807 if comment:
808 comment = ' (%s)' % comment
809 if task.start and task.finish:
810 elapsed = ' (Elapsed: %s)' % (
811 str(task.finish - task.start).partition('.')[0])
812 else:
813 elapsed = ''
814 return """
815%s%s%s
816----------------------------------------
817%s
818----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000819 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000820
hinoka885e5b12016-06-08 14:40:09 -0700821 def _is_conflict(self, job):
822 """Checks to see if a job will conflict with another running job."""
823 for running_job in self.running:
824 for used_resource in running_job.item.resources:
825 logging.debug('Checking resource %s' % used_resource)
826 if used_resource in job.resources:
827 return True
828 return False
829
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830 def flush(self, *args, **kwargs):
831 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000832 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000833 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000834 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000835 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 while True:
837 # Check for task to run first, then wait.
838 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000839 if not self.exceptions.empty():
840 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000841 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000842 self._flush_terminated_threads()
843 if (not self.queued and not self.running or
844 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000845 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000846 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847
848 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000849 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000851 if (self.ignore_requirements or
852 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700853 if not self._is_conflict(self.queued[i]):
854 # Start one work item: all its requirements are satisfied.
855 self._run_one_task(self.queued.pop(i), args, kwargs)
856 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000857 else:
858 # Couldn't find an item that could run. Break out the outher loop.
859 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000860
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000861 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000862 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 break
864 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000865 try:
866 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000867 # If we haven't printed to terminal for a while, but we have received
868 # spew from a suprocess, let the user know we're still progressing.
869 now = datetime.datetime.now()
870 if (now - self.last_join > datetime.timedelta(seconds=60) and
871 self.last_subproc_output > self.last_join):
872 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000873 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000874 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000875 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000876 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000877 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000878 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000879 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000880 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000881 except KeyboardInterrupt:
882 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000883 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000884 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000885 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
886 self.ran), len(self.running)),
887 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000888 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000889 print(
890 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
891 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000893 print(
894 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000895 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000896 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000897 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000901 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000902 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000903 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000904 # To get back the stack location correctly, the raise a, b, c form must be
905 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000907 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
908 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000909 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000910 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000911
maruel@chromium.org3742c842010-09-09 19:27:14 +0000912 def _flush_terminated_threads(self):
913 """Flush threads that have terminated."""
914 running = self.running
915 self.running = []
916 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000917 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000918 self.running.append(t)
919 else:
920 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000921 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000922 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000923 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000924 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000925 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000926 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000927 if t.item.name in self.ran:
928 raise Error(
929 'gclient is confused, "%s" is already in "%s"' % (
930 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000931 if not t.item.name in self.ran:
932 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000933
934 def _run_one_task(self, task_item, args, kwargs):
935 if self.jobs > 1:
936 # Start the thread.
937 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000938 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000939 self.running.append(new_thread)
940 new_thread.start()
941 else:
942 # Run the 'thread' inside the main thread. Don't try to catch any
943 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000944 try:
945 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000946 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000947 task_item.run(*args, **kwargs)
948 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000949 print(
950 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000951 self.ran.append(task_item.name)
952 if self.verbose:
953 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000954 print('')
955 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000956 if self.progress:
957 self.progress.update(1, ', '.join(t.item.name for t in self.running))
958 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000959 print(
960 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000961 raise
962 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000963 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000964 raise
965
maruel@chromium.org3742c842010-09-09 19:27:14 +0000966
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000967 class _Worker(threading.Thread):
968 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000969 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000970 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000971 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000972 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000973 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000974 self.args = args
975 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000976 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000977
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000978 def run(self):
979 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000980 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000981 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000982 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000984 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000985 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000986 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000987 print(
988 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000989 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000990 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000991 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000992 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000993 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000994 except Exception:
995 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +0000996 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000997 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000998 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000999 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001000 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001001 work_queue.ready_cond.acquire()
1002 try:
1003 work_queue.ready_cond.notifyAll()
1004 finally:
1005 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001006
1007
agable92bec4f2016-08-24 09:27:27 -07001008def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001009 """Returns the most plausible editor to use.
1010
1011 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001012 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001013 - core.editor git configuration variable (if supplied by git-cl)
1014 - VISUAL environment variable
1015 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001016 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001017
1018 In the case of git-cl, this matches git's behaviour, except that it does not
1019 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001020 """
agable92bec4f2016-08-24 09:27:27 -07001021 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001022 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001023 editor = os.environ.get('VISUAL')
1024 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001025 editor = os.environ.get('EDITOR')
1026 if not editor:
1027 if sys.platform.startswith('win'):
1028 editor = 'notepad'
1029 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001030 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001031 return editor
1032
1033
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001034def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001035 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001036 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001037 # Make sure CRLF is handled properly by requiring none.
1038 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001039 print(
1040 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001041 fileobj = os.fdopen(file_handle, 'w')
1042 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001043 content = re.sub('\r?\n', '\n', content)
1044 # Some editors complain when the file doesn't end in \n.
1045 if not content.endswith('\n'):
1046 content += '\n'
1047 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001048 fileobj.close()
1049
1050 try:
agable92bec4f2016-08-24 09:27:27 -07001051 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001052 if not editor:
1053 return None
1054 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001055 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1056 # Msysgit requires the usage of 'env' to be present.
1057 cmd = 'env ' + cmd
1058 try:
1059 # shell=True to allow the shell to handle all forms of quotes in
1060 # $EDITOR.
1061 subprocess2.check_call(cmd, shell=True)
1062 except subprocess2.CalledProcessError:
1063 return None
1064 return FileRead(filename)
1065 finally:
1066 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001067
1068
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001069def UpgradeToHttps(url):
1070 """Upgrades random urls to https://.
1071
1072 Do not touch unknown urls like ssh:// or git://.
1073 Do not touch http:// urls with a port number,
1074 Fixes invalid GAE url.
1075 """
1076 if not url:
1077 return url
1078 if not re.match(r'[a-z\-]+\://.*', url):
1079 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1080 # relative url and will use http:///foo. Note that it defaults to http://
1081 # for compatibility with naked url like "localhost:8080".
1082 url = 'http://%s' % url
1083 parsed = list(urlparse.urlparse(url))
1084 # Do not automatically upgrade http to https if a port number is provided.
1085 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1086 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001087 return urlparse.urlunparse(parsed)
1088
1089
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001090def ParseCodereviewSettingsContent(content):
1091 """Process a codereview.settings file properly."""
1092 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1093 try:
1094 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1095 except ValueError:
1096 raise Error(
1097 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001098 def fix_url(key):
1099 if keyvals.get(key):
1100 keyvals[key] = UpgradeToHttps(keyvals[key])
1101 fix_url('CODE_REVIEW_SERVER')
1102 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001103 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001104
1105
1106def NumLocalCpus():
1107 """Returns the number of processors.
1108
dnj@chromium.org530523b2015-01-07 19:54:57 +00001109 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1110 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1111 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001112 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001113 # Surround the entire thing in try/except; no failure here should stop gclient
1114 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001115 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001116 # Use multiprocessing to get CPU count. This may raise
1117 # NotImplementedError.
1118 try:
1119 import multiprocessing
1120 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001121 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001122 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001123 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001124 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1125 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1126
1127 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1128 if 'NUMBER_OF_PROCESSORS' in os.environ:
1129 return int(os.environ['NUMBER_OF_PROCESSORS'])
1130 except Exception as e:
1131 logging.exception("Exception raised while probing CPU count: %s", e)
1132
1133 logging.debug('Failed to get CPU count. Defaulting to 1.')
1134 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001135
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001136
szager@chromium.orgfc616382014-03-18 20:32:04 +00001137def DefaultDeltaBaseCacheLimit():
1138 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1139
1140 The primary constraint is the address space of virtual memory. The cache
1141 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1142 parameter is set too high.
1143 """
1144 if platform.architecture()[0].startswith('64'):
1145 return '2g'
1146 else:
1147 return '512m'
1148
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001149
szager@chromium.orgff113292014-03-25 06:02:08 +00001150def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001151 """Return reasonable default values for configuring git-index-pack.
1152
1153 Experiments suggest that higher values for pack.threads don't improve
1154 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001155 cache_limit = DefaultDeltaBaseCacheLimit()
1156 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1157 if url in THREADED_INDEX_PACK_BLACKLIST:
1158 result.extend(['-c', 'pack.threads=1'])
1159 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001160
1161
1162def FindExecutable(executable):
1163 """This mimics the "which" utility."""
1164 path_folders = os.environ.get('PATH').split(os.pathsep)
1165
1166 for path_folder in path_folders:
1167 target = os.path.join(path_folder, executable)
1168 # Just incase we have some ~/blah paths.
1169 target = os.path.abspath(os.path.expanduser(target))
1170 if os.path.isfile(target) and os.access(target, os.X_OK):
1171 return target
1172 if sys.platform.startswith('win'):
1173 for suffix in ('.bat', '.cmd', '.exe'):
1174 alt_target = target + suffix
1175 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1176 return alt_target
1177 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001178
1179
1180def freeze(obj):
1181 """Takes a generic object ``obj``, and returns an immutable version of it.
1182
1183 Supported types:
1184 * dict / OrderedDict -> FrozenDict
1185 * list -> tuple
1186 * set -> frozenset
1187 * any object with a working __hash__ implementation (assumes that hashable
1188 means immutable)
1189
1190 Will raise TypeError if you pass an object which is not hashable.
1191 """
Raul Tambre6693d092020-02-19 20:36:45 +00001192 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001193 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001194 elif isinstance(obj, (list, tuple)):
1195 return tuple(freeze(i) for i in obj)
1196 elif isinstance(obj, set):
1197 return frozenset(freeze(i) for i in obj)
1198 else:
1199 hash(obj)
1200 return obj
1201
1202
Raul Tambre6693d092020-02-19 20:36:45 +00001203class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001204 """An immutable OrderedDict.
1205
1206 Modified From: http://stackoverflow.com/a/2704866
1207 """
1208 def __init__(self, *args, **kwargs):
1209 self._d = collections.OrderedDict(*args, **kwargs)
1210
1211 # Calculate the hash immediately so that we know all the items are
1212 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001213 self._hash = functools.reduce(
1214 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001215
1216 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001217 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001218 return NotImplemented
1219 if self is other:
1220 return True
1221 if len(self) != len(other):
1222 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001223 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001224 if k not in other or other[k] != v:
1225 return False
1226 return True
1227
1228 def __iter__(self):
1229 return iter(self._d)
1230
1231 def __len__(self):
1232 return len(self._d)
1233
1234 def __getitem__(self, key):
1235 return self._d[key]
1236
1237 def __hash__(self):
1238 return self._hash
1239
1240 def __repr__(self):
1241 return 'FrozenDict(%r)' % (self._d.items(),)