blob: 2c38a3a3d34d4c15536aadfbb6e1dd7a783110d1 [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Raul Tambreb946b232019-03-26 14:48:46 +000013import functools
14import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000015import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020016import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000018import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000019import platform
Raul Tambreb946b232019-03-26 14:48:46 +000020
21try:
22 import Queue as queue
23except ImportError: # For Py3 compatibility
24 import queue
25
msb@chromium.orgac915bb2009-11-13 17:03:01 +000026import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000027import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000028import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000031import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000032import time
Raul Tambreb946b232019-03-26 14:48:46 +000033
34try:
35 import urlparse
36except ImportError: # For Py3 compatibility
37 import urllib.parse as urlparse
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000038
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000039import subprocess2
40
Raul Tambreb946b232019-03-26 14:48:46 +000041if sys.version_info.major == 2:
42 from cStringIO import StringIO
Raul Tambre5d284fd2019-10-07 18:11:26 +000043 string_type = basestring
Raul Tambreb946b232019-03-26 14:48:46 +000044else:
45 from io import StringIO
Raul Tambre5d284fd2019-10-07 18:11:26 +000046 string_type = str
Raul Tambreb946b232019-03-26 14:48:46 +000047
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000048
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000049RETRY_MAX = 3
50RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000051START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000052
53
borenet@google.com6a9b1682014-03-24 18:35:23 +000054_WARNINGS = []
55
56
szager@chromium.orgff113292014-03-25 06:02:08 +000057# These repos are known to cause OOM errors on 32-bit platforms, due the the
58# very large objects they contain. It is not safe to use threaded index-pack
59# when cloning/fetching them.
60THREADED_INDEX_PACK_BLACKLIST = [
61 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
62]
63
Raul Tambreb946b232019-03-26 14:48:46 +000064"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
65if sys.version_info.major == 2:
66 # We have to use exec to avoid a SyntaxError in Python 3.
67 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
68else:
69 def reraise(typ, value, tb=None):
70 if value is None:
71 value = typ()
72 if value.__traceback__ is not tb:
73 raise value.with_traceback(tb)
74 raise value
75
szager@chromium.orgff113292014-03-25 06:02:08 +000076
maruel@chromium.org66c83e62010-09-07 14:18:45 +000077class Error(Exception):
78 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000079 def __init__(self, msg, *args, **kwargs):
80 index = getattr(threading.currentThread(), 'index', 0)
81 if index:
82 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
83 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000084
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000085
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000086def Elapsed(until=None):
87 if until is None:
88 until = datetime.datetime.now()
89 return str(until - START).partition('.')[0]
90
91
borenet@google.com6a9b1682014-03-24 18:35:23 +000092def PrintWarnings():
93 """Prints any accumulated warnings."""
94 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000095 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000096 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000097 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000098
99
100def AddWarning(msg):
101 """Adds the given warning message to the list of accumulated warnings."""
102 _WARNINGS.append(msg)
103
104
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000105def SplitUrlRevision(url):
106 """Splits url and returns a two-tuple: url, rev"""
107 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000108 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000109 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000110 components = re.search(regex, url).groups()
111 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000112 components = url.rsplit('@', 1)
113 if re.match(r'^\w+\@', url) and '@' not in components[0]:
114 components = [url]
115
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000116 if len(components) == 1:
117 components += [None]
118 return tuple(components)
119
120
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000121def IsGitSha(revision):
122 """Returns true if the given string is a valid hex-encoded sha"""
123 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
124
125
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200126def IsFullGitSha(revision):
127 """Returns true if the given string is a valid hex-encoded full sha"""
128 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
129
130
floitsch@google.comeaab7842011-04-28 09:07:58 +0000131def IsDateRevision(revision):
132 """Returns true if the given revision is of the form "{ ... }"."""
133 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
134
135
136def MakeDateRevision(date):
137 """Returns a revision representing the latest revision before the given
138 date."""
139 return "{" + date + "}"
140
141
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000142def SyntaxErrorToError(filename, e):
143 """Raises a gclient_utils.Error exception with the human readable message"""
144 try:
145 # Try to construct a human readable error message
146 if filename:
147 error_message = 'There is a syntax error in %s\n' % filename
148 else:
149 error_message = 'There is a syntax error\n'
150 error_message += 'Line #%s, character %s: "%s"' % (
151 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
152 except:
153 # Something went wrong, re-raise the original exception
154 raise e
155 else:
156 raise Error(error_message)
157
158
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000159class PrintableObject(object):
160 def __str__(self):
161 output = ''
162 for i in dir(self):
163 if i.startswith('__'):
164 continue
165 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
166 return output
167
168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169def FileRead(filename, mode='rU'):
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000170 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
171 if mode == 'rU' and sys.version_info.major == 3:
172 mode = 'r'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000173 with open(filename, mode=mode) as f:
maruel@chromium.orgc3cd5372012-07-11 17:39:24 +0000174 # codecs.open() has different behavior than open() on python 2.6 so use
175 # open() and decode manually.
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000176 s = f.read()
177 try:
178 return s.decode('utf-8')
Raul Tambreb946b232019-03-26 14:48:46 +0000179 # AttributeError is for Py3 compatibility
180 except (UnicodeDecodeError, AttributeError):
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000181 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182
183
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000184def FileWrite(filename, content, mode='w'):
maruel@chromium.orgdae209f2012-07-03 16:08:15 +0000185 with codecs.open(filename, mode=mode, encoding='utf-8') as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000186 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187
188
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000189@contextlib.contextmanager
190def temporary_directory(**kwargs):
191 tdir = tempfile.mkdtemp(**kwargs)
192 try:
193 yield tdir
194 finally:
195 if tdir:
196 rmtree(tdir)
197
198
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000199def safe_rename(old, new):
200 """Renames a file reliably.
201
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000202 Sometimes os.rename does not work because a dying git process keeps a handle
203 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000204 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000205 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000206 """
207 # roughly 10s
208 retries = 100
209 for i in range(retries):
210 try:
211 os.rename(old, new)
212 break
213 except OSError:
214 if i == (retries - 1):
215 # Give up.
216 raise
217 # retry
218 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
219 time.sleep(0.1)
220
221
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000222def rm_file_or_tree(path):
223 if os.path.isfile(path):
224 os.remove(path)
225 else:
226 rmtree(path)
227
228
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000229def rmtree(path):
230 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000231
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000232 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000233
234 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700235 are read-only. We need to be able to force the files to be writable (i.e.,
236 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000237
238 Even with all this, Windows still sometimes fails to delete a file, citing
239 a permission error (maybe something to do with antivirus scans or disk
240 indexing). The best suggestion any of the user forums had was to wait a
241 bit and try again, so we do that too. It's hand-waving, but sometimes it
242 works. :/
243
244 On POSIX systems, things are a little bit simpler. The modes of the files
245 to be deleted doesn't matter, only the modes of the directories containing
246 them are significant. As the directory tree is traversed, each directory
247 has its mode set appropriately before descending into it. This should
248 result in the entire tree being removed, with the possible exception of
249 *path itself, because nothing attempts to change the mode of its parent.
250 Doing so would be hazardous, as it's not a directory slated for removal.
251 In the ordinary case, this is not a problem: for our purposes, the user
252 will never lack write permission on *path's parent.
253 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000254 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255 return
256
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000257 if os.path.islink(path) or not os.path.isdir(path):
258 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000260 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000261 # Give up and use cmd.exe's rd command.
262 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000263 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000264 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
265 if exitcode == 0:
266 return
267 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000268 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000269 time.sleep(3)
270 raise Exception('Failed to remove path %s' % path)
271
272 # On POSIX systems, we need the x-bit set on the directory to access it,
273 # the r-bit to see its contents, and the w-bit to remove files from it.
274 # The actual modes of the files within the directory is irrelevant.
275 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000276
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000277 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000278 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000279
280 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281 # If fullpath is a symbolic link that points to a directory, isdir will
282 # be True, but we don't want to descend into that as a directory, we just
283 # want to remove the link. Check islink and treat links as ordinary files
284 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000285 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000287 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000289 # Recurse.
290 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000291
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000292 remove(os.rmdir, path)
293
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000294
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000295def safe_makedirs(tree):
296 """Creates the directory in a safe manner.
297
298 Because multiple threads can create these directories concurently, trap the
299 exception and pass on.
300 """
301 count = 0
302 while not os.path.exists(tree):
303 count += 1
304 try:
305 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000306 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000307 # 17 POSIX, 183 Windows
308 if e.errno not in (17, 183):
309 raise
310 if count > 40:
311 # Give up.
312 raise
313
314
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000315def CommandToStr(args):
316 """Converts an arg list into a shell escaped string."""
317 return ' '.join(pipes.quote(arg) for arg in args)
318
319
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000320class Wrapper(object):
321 """Wraps an object, acting as a transparent proxy for all properties by
322 default.
323 """
324 def __init__(self, wrapped):
325 self._wrapped = wrapped
326
327 def __getattr__(self, name):
328 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000329
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000330
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000331class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000332 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000333 def __init__(self, wrapped, delay):
334 super(AutoFlush, self).__init__(wrapped)
335 if not hasattr(self, 'lock'):
336 self.lock = threading.Lock()
337 self.__last_flushed_at = time.time()
338 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000339
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000340 @property
341 def autoflush(self):
342 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000343
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000344 def write(self, out, *args, **kwargs):
345 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000346 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000347 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000348 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000350 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000351 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000352 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000353 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000354 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000355 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000356
357
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000358class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000359 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360 threads with a NN> prefix.
361 """
362 def __init__(self, wrapped, include_zero=False):
363 super(Annotated, self).__init__(wrapped)
364 if not hasattr(self, 'lock'):
365 self.lock = threading.Lock()
366 self.__output_buffers = {}
367 self.__include_zero = include_zero
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000368
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000369 @property
370 def annotated(self):
371 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000372
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 def write(self, out):
374 index = getattr(threading.currentThread(), 'index', 0)
375 if not index and not self.__include_zero:
Raul Tambre5d284fd2019-10-07 18:11:26 +0000376 # Store as bytes to ensure Unicode characters get output correctly.
377 if isinstance(out, bytes):
378 out = out.decode('utf-8')
379
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000380 # Unindexed threads aren't buffered.
381 return self._wrapped.write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000382
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000383 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000384 try:
385 # Use a dummy array to hold the string so the code can be lockless.
386 # Strings are immutable, requiring to keep a lock for the whole dictionary
387 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388 if not index in self.__output_buffers:
Raul Tambre5d284fd2019-10-07 18:11:26 +0000389 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000390 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000391 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000392 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000393 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000394
Raul Tambre5d284fd2019-10-07 18:11:26 +0000395 # Store as bytes to ensure Unicode characters get output correctly.
396 if isinstance(out, string_type):
397 out = out.encode('utf-8')
398
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000399 # Continue lockless.
400 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000401 while True:
402 # TODO(agable): find both of these with a single pass.
Raul Tambre5d284fd2019-10-07 18:11:26 +0000403 cr_loc = obj[0].find(b'\r')
404 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000405 if cr_loc == lf_loc == -1:
406 break
407 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Raul Tambre5d284fd2019-10-07 18:11:26 +0000408 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000409 if line:
Raul Tambre5d284fd2019-10-07 18:11:26 +0000410 self._wrapped.write('%d>%s\n' % (index, line.decode('utf-8')))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000411 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Raul Tambre5d284fd2019-10-07 18:11:26 +0000412 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000413 if line:
Raul Tambre5d284fd2019-10-07 18:11:26 +0000414 self._wrapped.write('%d>%s\r' % (index, line.decode('utf-8')))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000415 obj[0] = remaining
416
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000418 """Flush buffered output."""
419 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000421 try:
422 # Detect threads no longer existing.
423 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000424 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000426 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000427 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000428 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000429 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000430 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000431 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000432
433 # Don't keep the lock while writting. Will append \n when it shouldn't.
434 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000435 if orphan[1]:
Raul Tambre5d284fd2019-10-07 18:11:26 +0000436 self._wrapped.write('%d>%s\n' % (orphan[0], orphan[1].decode('utf-8')))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000437 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000438
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000439
440def MakeFileAutoFlush(fileobj, delay=10):
441 autoflush = getattr(fileobj, 'autoflush', None)
442 if autoflush:
443 autoflush.delay = delay
444 return fileobj
445 return AutoFlush(fileobj, delay)
446
447
448def MakeFileAnnotated(fileobj, include_zero=False):
449 if getattr(fileobj, 'annotated', None):
450 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000451 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000452
453
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000454GCLIENT_CHILDREN = []
455GCLIENT_CHILDREN_LOCK = threading.Lock()
456
457
458class GClientChildren(object):
459 @staticmethod
460 def add(popen_obj):
461 with GCLIENT_CHILDREN_LOCK:
462 GCLIENT_CHILDREN.append(popen_obj)
463
464 @staticmethod
465 def remove(popen_obj):
466 with GCLIENT_CHILDREN_LOCK:
467 GCLIENT_CHILDREN.remove(popen_obj)
468
469 @staticmethod
470 def _attemptToKillChildren():
471 global GCLIENT_CHILDREN
472 with GCLIENT_CHILDREN_LOCK:
473 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
474
475 for zombie in zombies:
476 try:
477 zombie.kill()
478 except OSError:
479 pass
480
481 with GCLIENT_CHILDREN_LOCK:
482 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
483
484 @staticmethod
485 def _areZombies():
486 with GCLIENT_CHILDREN_LOCK:
487 return bool(GCLIENT_CHILDREN)
488
489 @staticmethod
490 def KillAllRemainingChildren():
491 GClientChildren._attemptToKillChildren()
492
493 if GClientChildren._areZombies():
494 time.sleep(0.5)
495 GClientChildren._attemptToKillChildren()
496
497 with GCLIENT_CHILDREN_LOCK:
498 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000499 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000500 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000501 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000502
503
Edward Lemur24146be2019-08-01 21:44:52 +0000504def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
505 show_header=False, always_show_header=False, retry=False,
506 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000507 """Runs a command and calls back a filter function if needed.
508
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000509 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000510 print_stdout: If True, the command's stdout is forwarded to stdout.
511 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000512 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000513 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000514 show_header: Whether to display a header before the command output.
515 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000516 retry: If the process exits non-zero, sleep for a brief interval and try
517 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000518
519 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000520
521 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000522 """
Edward Lemur24146be2019-08-01 21:44:52 +0000523 def show_header_if_necessary(needs_header, attempt):
524 """Show the header at most once."""
525 if not needs_header[0]:
526 return
527
528 needs_header[0] = False
529 # Automatically generated header. We only prepend a newline if
530 # always_show_header is false, since it usually indicates there's an
531 # external progress display, and it's better not to clobber it in that case.
532 header = '' if always_show_header else '\n'
533 header += '________ running \'%s\' in \'%s\'' % (
534 ' '.join(args), kwargs.get('cwd', '.'))
535 if attempt:
536 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
537 header += '\n'
538
539 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000540 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
541 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000542 if filter_fn:
543 filter_fn(header)
544
545 def filter_line(command_output, line_start):
546 """Extract the last line from command output and filter it."""
547 if not filter_fn or line_start is None:
548 return
549 command_output.seek(line_start)
550 filter_fn(command_output.read().decode('utf-8'))
551
552 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
553 # byte inputs and sys.stdout.buffer must be used instead.
554 if print_stdout:
555 sys.stdout.flush()
556 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
557 else:
558 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000559
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000560 sleep_interval = RETRY_INITIAL_SLEEP
561 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000562 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000563 kid = subprocess2.Popen(
564 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
565 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000566
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000568
Edward Lemur24146be2019-08-01 21:44:52 +0000569 # Store the output of the command regardless of the value of print_stdout or
570 # filter_fn.
571 command_output = io.BytesIO()
572
573 # Passed as a list for "by ref" semantics.
574 needs_header = [show_header]
575 if always_show_header:
576 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000577
578 # Also, we need to forward stdout to prevent weird re-ordering of output.
579 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700580 # normally buffering is done for each line, but if the process requests
581 # input, no end-of-line character is output after the prompt and it would
582 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000583 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000584 line_start = None
585 while True:
586 in_byte = kid.stdout.read(1)
587 is_newline = in_byte in (b'\n', b'\r')
588 if not in_byte:
589 break
590
591 show_header_if_necessary(needs_header, attempt)
592
593 if is_newline:
594 filter_line(command_output, line_start)
595 line_start = None
596 elif line_start is None:
597 line_start = command_output.tell()
598
599 stdout_write(in_byte)
600 command_output.write(in_byte)
601
602 # Flush the rest of buffered output.
603 sys.stdout.flush()
604 if line_start is not None:
605 filter_line(command_output, line_start)
606
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000607 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000608 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000609
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000610 # Don't put this in a 'finally,' since the child may still run if we get
611 # an exception.
612 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000613
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000614 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000615 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000616 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000617
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000618 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000619 return command_output.getvalue()
620
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000621 if not retry:
622 break
Edward Lemur24146be2019-08-01 21:44:52 +0000623
Raul Tambreb946b232019-03-26 14:48:46 +0000624 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
625 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000626 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000627 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000628
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000629 raise subprocess2.CalledProcessError(
630 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000631
632
agable@chromium.org5a306a22014-02-24 22:13:59 +0000633class GitFilter(object):
634 """A filter_fn implementation for quieting down git output messages.
635
636 Allows a custom function to skip certain lines (predicate), and will throttle
637 the output of percentage completed lines to only output every X seconds.
638 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000639 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000640
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000641 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000642 """
643 Args:
644 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
645 XX% complete messages) to only be printed at least |time_throttle|
646 seconds apart.
647 predicate (f(line)): An optional function which is invoked for every line.
648 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000649 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000650 """
Edward Lemur24146be2019-08-01 21:44:52 +0000651 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000652 self.last_time = 0
653 self.time_throttle = time_throttle
654 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000655 self.out_fh = out_fh or sys.stdout
656 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000657
658 def __call__(self, line):
659 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000660 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000661 if esc > -1:
662 line = line[:esc]
663 if self.predicate and not self.predicate(line):
664 return
665 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000666 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000667 if match:
668 if match.group(1) != self.progress_prefix:
669 self.progress_prefix = match.group(1)
670 elif now - self.last_time < self.time_throttle:
671 return
672 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000673 if not self.first_line:
674 self.out_fh.write('[%s] ' % Elapsed())
675 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000676 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000677
678
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000679def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000680 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000681
rcui@google.com13595ff2011-10-13 01:25:07 +0000682 Returns nearest upper-level directory with the passed in file.
683 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000684 if not path:
685 path = os.getcwd()
686 path = os.path.realpath(path)
687 while True:
688 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000689 if os.path.exists(file_path):
690 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000691 (new_path, _) = os.path.split(path)
692 if new_path == path:
693 return None
694 path = new_path
695
696
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000697def GetMacWinOrLinux():
698 """Returns 'mac', 'win', or 'linux', matching the current platform."""
699 if sys.platform.startswith(('cygwin', 'win')):
700 return 'win'
701 elif sys.platform.startswith('linux'):
702 return 'linux'
703 elif sys.platform == 'darwin':
704 return 'mac'
705 raise Error('Unknown platform: ' + sys.platform)
706
707
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000708def GetGClientRootAndEntries(path=None):
709 """Returns the gclient root and the dict of entries."""
710 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000711 root = FindFileUpwards(config_file, path)
712 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000713 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000714 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000715 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000716 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000717 with open(config_path) as config:
718 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000719 config_dir = os.path.dirname(config_path)
720 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000721
722
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000723def lockedmethod(method):
724 """Method decorator that holds self.lock for the duration of the call."""
725 def inner(self, *args, **kwargs):
726 try:
727 try:
728 self.lock.acquire()
729 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000730 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000731 raise
732 return method(self, *args, **kwargs)
733 finally:
734 self.lock.release()
735 return inner
736
737
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000738class WorkItem(object):
739 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000740 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
741 # As a workaround, use a single lock. Yep you read it right. Single lock for
742 # all the 100 objects.
743 lock = threading.Lock()
744
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000745 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000746 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000747 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000748 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000749 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700750 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000751
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000752 def run(self, work_queue):
753 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000754 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000755 pass
756
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000757 @property
758 def name(self):
759 return self._name
760
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000761
762class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000763 """Runs a set of WorkItem that have interdependencies and were WorkItem are
764 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000765
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200766 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000767 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000768
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000769 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000770 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000771 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000772 """jobs specifies the number of concurrent tasks to allow. progress is a
773 Progress instance."""
774 # Set when a thread is done or a new item is enqueued.
775 self.ready_cond = threading.Condition()
776 # Maximum number of concurrent tasks.
777 self.jobs = jobs
778 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000779 self.queued = []
780 # List of strings representing each Dependency.name that was run.
781 self.ran = []
782 # List of items currently running.
783 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000784 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000785 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000786 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000787 self.progress = progress
788 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000789 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000790
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000791 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000792 self.verbose = verbose
793 self.last_join = None
794 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000795
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796 def enqueue(self, d):
797 """Enqueue one Dependency to be executed later once its requirements are
798 satisfied.
799 """
800 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000801 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000802 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000803 self.queued.append(d)
804 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000805 if self.jobs == 1:
806 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000807 logging.debug('enqueued(%s)' % d.name)
808 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000809 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000810 self.progress.update(0)
811 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000812 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000813 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000814
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000815 def out_cb(self, _):
816 self.last_subproc_output = datetime.datetime.now()
817 return True
818
819 @staticmethod
820 def format_task_output(task, comment=''):
821 if comment:
822 comment = ' (%s)' % comment
823 if task.start and task.finish:
824 elapsed = ' (Elapsed: %s)' % (
825 str(task.finish - task.start).partition('.')[0])
826 else:
827 elapsed = ''
828 return """
829%s%s%s
830----------------------------------------
831%s
832----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000833 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000834
hinoka885e5b12016-06-08 14:40:09 -0700835 def _is_conflict(self, job):
836 """Checks to see if a job will conflict with another running job."""
837 for running_job in self.running:
838 for used_resource in running_job.item.resources:
839 logging.debug('Checking resource %s' % used_resource)
840 if used_resource in job.resources:
841 return True
842 return False
843
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844 def flush(self, *args, **kwargs):
845 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000847 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000848 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 while True:
851 # Check for task to run first, then wait.
852 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000853 if not self.exceptions.empty():
854 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000856 self._flush_terminated_threads()
857 if (not self.queued and not self.running or
858 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000859 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000861
862 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000863 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000864 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000865 if (self.ignore_requirements or
866 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700867 if not self._is_conflict(self.queued[i]):
868 # Start one work item: all its requirements are satisfied.
869 self._run_one_task(self.queued.pop(i), args, kwargs)
870 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000871 else:
872 # Couldn't find an item that could run. Break out the outher loop.
873 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000874
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000875 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000876 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000877 break
878 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000879 try:
880 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000881 # If we haven't printed to terminal for a while, but we have received
882 # spew from a suprocess, let the user know we're still progressing.
883 now = datetime.datetime.now()
884 if (now - self.last_join > datetime.timedelta(seconds=60) and
885 self.last_subproc_output > self.last_join):
886 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000887 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000888 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000889 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000890 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000891 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000893 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000894 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000895 except KeyboardInterrupt:
896 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000897 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000898 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000899 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
900 self.ran), len(self.running)),
901 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000902 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000903 print(
904 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
905 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000907 print(
908 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000909 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000910 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000911 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000912 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000913
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000914 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000915 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000916 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000917 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000918 # To get back the stack location correctly, the raise a, b, c form must be
919 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000920 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000921 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
922 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000923 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000924 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000925
maruel@chromium.org3742c842010-09-09 19:27:14 +0000926 def _flush_terminated_threads(self):
927 """Flush threads that have terminated."""
928 running = self.running
929 self.running = []
930 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000931 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000932 self.running.append(t)
933 else:
934 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000935 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000936 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000937 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000938 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000939 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000940 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000941 if t.item.name in self.ran:
942 raise Error(
943 'gclient is confused, "%s" is already in "%s"' % (
944 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000945 if not t.item.name in self.ran:
946 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000947
948 def _run_one_task(self, task_item, args, kwargs):
949 if self.jobs > 1:
950 # Start the thread.
951 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000952 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000953 self.running.append(new_thread)
954 new_thread.start()
955 else:
956 # Run the 'thread' inside the main thread. Don't try to catch any
957 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000958 try:
959 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000960 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000961 task_item.run(*args, **kwargs)
962 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000963 print(
964 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000965 self.ran.append(task_item.name)
966 if self.verbose:
967 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000968 print('')
969 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 if self.progress:
971 self.progress.update(1, ', '.join(t.item.name for t in self.running))
972 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000973 print(
974 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000975 raise
976 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +0000977 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000978 raise
979
maruel@chromium.org3742c842010-09-09 19:27:14 +0000980
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000981 class _Worker(threading.Thread):
982 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000983 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000984 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000985 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000986 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000987 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +0000988 self.args = args
989 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000990 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000991
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000992 def run(self):
993 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000994 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000995 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000996 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000997 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000998 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000999 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001001 print(
1002 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001003 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001004 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001005 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001006 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001007 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001008 except Exception:
1009 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001010 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001011 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001012 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001013 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001014 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001015 work_queue.ready_cond.acquire()
1016 try:
1017 work_queue.ready_cond.notifyAll()
1018 finally:
1019 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001020
1021
agable92bec4f2016-08-24 09:27:27 -07001022def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001023 """Returns the most plausible editor to use.
1024
1025 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001026 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001027 - core.editor git configuration variable (if supplied by git-cl)
1028 - VISUAL environment variable
1029 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001030 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001031
1032 In the case of git-cl, this matches git's behaviour, except that it does not
1033 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001034 """
agable92bec4f2016-08-24 09:27:27 -07001035 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001036 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001037 editor = os.environ.get('VISUAL')
1038 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001039 editor = os.environ.get('EDITOR')
1040 if not editor:
1041 if sys.platform.startswith('win'):
1042 editor = 'notepad'
1043 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001044 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001045 return editor
1046
1047
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001048def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001049 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001050 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001051 # Make sure CRLF is handled properly by requiring none.
1052 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001053 print(
1054 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001055 fileobj = os.fdopen(file_handle, 'w')
1056 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001057 content = re.sub('\r?\n', '\n', content)
1058 # Some editors complain when the file doesn't end in \n.
1059 if not content.endswith('\n'):
1060 content += '\n'
1061 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001062 fileobj.close()
1063
1064 try:
agable92bec4f2016-08-24 09:27:27 -07001065 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001066 if not editor:
1067 return None
1068 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001069 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1070 # Msysgit requires the usage of 'env' to be present.
1071 cmd = 'env ' + cmd
1072 try:
1073 # shell=True to allow the shell to handle all forms of quotes in
1074 # $EDITOR.
1075 subprocess2.check_call(cmd, shell=True)
1076 except subprocess2.CalledProcessError:
1077 return None
1078 return FileRead(filename)
1079 finally:
1080 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001081
1082
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001083def UpgradeToHttps(url):
1084 """Upgrades random urls to https://.
1085
1086 Do not touch unknown urls like ssh:// or git://.
1087 Do not touch http:// urls with a port number,
1088 Fixes invalid GAE url.
1089 """
1090 if not url:
1091 return url
1092 if not re.match(r'[a-z\-]+\://.*', url):
1093 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1094 # relative url and will use http:///foo. Note that it defaults to http://
1095 # for compatibility with naked url like "localhost:8080".
1096 url = 'http://%s' % url
1097 parsed = list(urlparse.urlparse(url))
1098 # Do not automatically upgrade http to https if a port number is provided.
1099 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1100 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001101 return urlparse.urlunparse(parsed)
1102
1103
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001104def ParseCodereviewSettingsContent(content):
1105 """Process a codereview.settings file properly."""
1106 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1107 try:
1108 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1109 except ValueError:
1110 raise Error(
1111 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001112 def fix_url(key):
1113 if keyvals.get(key):
1114 keyvals[key] = UpgradeToHttps(keyvals[key])
1115 fix_url('CODE_REVIEW_SERVER')
1116 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001117 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001118
1119
1120def NumLocalCpus():
1121 """Returns the number of processors.
1122
dnj@chromium.org530523b2015-01-07 19:54:57 +00001123 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1124 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1125 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001126 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001127 # Surround the entire thing in try/except; no failure here should stop gclient
1128 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001129 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001130 # Use multiprocessing to get CPU count. This may raise
1131 # NotImplementedError.
1132 try:
1133 import multiprocessing
1134 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001135 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001136 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001137 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001138 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1139 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1140
1141 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1142 if 'NUMBER_OF_PROCESSORS' in os.environ:
1143 return int(os.environ['NUMBER_OF_PROCESSORS'])
1144 except Exception as e:
1145 logging.exception("Exception raised while probing CPU count: %s", e)
1146
1147 logging.debug('Failed to get CPU count. Defaulting to 1.')
1148 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001149
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001150
szager@chromium.orgfc616382014-03-18 20:32:04 +00001151def DefaultDeltaBaseCacheLimit():
1152 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1153
1154 The primary constraint is the address space of virtual memory. The cache
1155 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1156 parameter is set too high.
1157 """
1158 if platform.architecture()[0].startswith('64'):
1159 return '2g'
1160 else:
1161 return '512m'
1162
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001163
szager@chromium.orgff113292014-03-25 06:02:08 +00001164def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001165 """Return reasonable default values for configuring git-index-pack.
1166
1167 Experiments suggest that higher values for pack.threads don't improve
1168 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001169 cache_limit = DefaultDeltaBaseCacheLimit()
1170 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1171 if url in THREADED_INDEX_PACK_BLACKLIST:
1172 result.extend(['-c', 'pack.threads=1'])
1173 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001174
1175
1176def FindExecutable(executable):
1177 """This mimics the "which" utility."""
1178 path_folders = os.environ.get('PATH').split(os.pathsep)
1179
1180 for path_folder in path_folders:
1181 target = os.path.join(path_folder, executable)
1182 # Just incase we have some ~/blah paths.
1183 target = os.path.abspath(os.path.expanduser(target))
1184 if os.path.isfile(target) and os.access(target, os.X_OK):
1185 return target
1186 if sys.platform.startswith('win'):
1187 for suffix in ('.bat', '.cmd', '.exe'):
1188 alt_target = target + suffix
1189 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1190 return alt_target
1191 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001192
1193
1194def freeze(obj):
1195 """Takes a generic object ``obj``, and returns an immutable version of it.
1196
1197 Supported types:
1198 * dict / OrderedDict -> FrozenDict
1199 * list -> tuple
1200 * set -> frozenset
1201 * any object with a working __hash__ implementation (assumes that hashable
1202 means immutable)
1203
1204 Will raise TypeError if you pass an object which is not hashable.
1205 """
Edward Lesmes6f64a052018-03-20 17:35:49 -04001206 if isinstance(obj, collections.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001207 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001208 elif isinstance(obj, (list, tuple)):
1209 return tuple(freeze(i) for i in obj)
1210 elif isinstance(obj, set):
1211 return frozenset(freeze(i) for i in obj)
1212 else:
1213 hash(obj)
1214 return obj
1215
1216
1217class FrozenDict(collections.Mapping):
1218 """An immutable OrderedDict.
1219
1220 Modified From: http://stackoverflow.com/a/2704866
1221 """
1222 def __init__(self, *args, **kwargs):
1223 self._d = collections.OrderedDict(*args, **kwargs)
1224
1225 # Calculate the hash immediately so that we know all the items are
1226 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001227 self._hash = functools.reduce(
1228 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001229
1230 def __eq__(self, other):
1231 if not isinstance(other, collections.Mapping):
1232 return NotImplemented
1233 if self is other:
1234 return True
1235 if len(self) != len(other):
1236 return False
1237 for k, v in self.iteritems():
1238 if k not in other or other[k] != v:
1239 return False
1240 return True
1241
1242 def __iter__(self):
1243 return iter(self._d)
1244
1245 def __len__(self):
1246 return len(self._d)
1247
1248 def __getitem__(self, key):
1249 return self._d[key]
1250
1251 def __hash__(self):
1252 return self._hash
1253
1254 def __repr__(self):
1255 return 'FrozenDict(%r)' % (self._d.items(),)