blob: 498b27a2034dd92079ef66d0225f67e3dec94b74 [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 Lesmesae3586b2020-03-23 21:21:14 +0000161def AskForData(message):
162 # Use this so that it can be mocked in tests on Python 2 and 3.
163 try:
164 if sys.version_info.major == 2:
165 return raw_input(message)
166 return input(message)
167 except KeyboardInterrupt:
168 # Hide the exception.
169 sys.exit(1)
170
171
Edward Lemur419c92f2019-10-25 22:17:49 +0000172def FileRead(filename, mode='rbU'):
173 # Always decodes output to a Unicode string.
Edward Lemur26a8b9f2019-08-15 20:46:44 +0000174 # On Python 3 newlines are converted to '\n' by default and 'U' is deprecated.
Edward Lemur419c92f2019-10-25 22:17:49 +0000175 if mode == 'rbU' and sys.version_info.major == 3:
176 mode = 'rb'
maruel@chromium.org51e84fb2012-07-03 23:06:21 +0000177 with open(filename, mode=mode) as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000178 s = f.read()
Edward Lemur419c92f2019-10-25 22:17:49 +0000179 if isinstance(s, bytes):
180 return s.decode('utf-8', 'replace')
181 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182
183
Edward Lemur1773f372020-02-22 00:27:14 +0000184def FileWrite(filename, content, mode='w', encoding='utf-8'):
185 with codecs.open(filename, mode=mode, encoding=encoding) 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
Edward Lemur1773f372020-02-22 00:27:14 +0000199@contextlib.contextmanager
200def temporary_file():
201 """Creates a temporary file.
202
203 On Windows, a file must be closed before it can be opened again. This function
204 allows to write something like:
205
206 with gclient_utils.temporary_file() as tmp:
207 gclient_utils.FileWrite(tmp, foo)
208 useful_stuff(tmp)
209
210 Instead of something like:
211
212 with tempfile.NamedTemporaryFile(delete=False) as tmp:
213 tmp.write(foo)
214 tmp.close()
215 try:
216 useful_stuff(tmp)
217 finally:
218 os.remove(tmp.name)
219 """
220 handle, name = tempfile.mkstemp()
221 os.close(handle)
222 try:
223 yield name
224 finally:
225 os.remove(name)
226
227
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000228def safe_rename(old, new):
229 """Renames a file reliably.
230
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000231 Sometimes os.rename does not work because a dying git process keeps a handle
232 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000233 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000234 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000235 """
236 # roughly 10s
237 retries = 100
238 for i in range(retries):
239 try:
240 os.rename(old, new)
241 break
242 except OSError:
243 if i == (retries - 1):
244 # Give up.
245 raise
246 # retry
247 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
248 time.sleep(0.1)
249
250
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000251def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000252 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000253 os.remove(path)
254 else:
255 rmtree(path)
256
257
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000258def rmtree(path):
259 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000260
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000261 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000262
263 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700264 are read-only. We need to be able to force the files to be writable (i.e.,
265 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000266
267 Even with all this, Windows still sometimes fails to delete a file, citing
268 a permission error (maybe something to do with antivirus scans or disk
269 indexing). The best suggestion any of the user forums had was to wait a
270 bit and try again, so we do that too. It's hand-waving, but sometimes it
271 works. :/
272
273 On POSIX systems, things are a little bit simpler. The modes of the files
274 to be deleted doesn't matter, only the modes of the directories containing
275 them are significant. As the directory tree is traversed, each directory
276 has its mode set appropriately before descending into it. This should
277 result in the entire tree being removed, with the possible exception of
278 *path itself, because nothing attempts to change the mode of its parent.
279 Doing so would be hazardous, as it's not a directory slated for removal.
280 In the ordinary case, this is not a problem: for our purposes, the user
281 will never lack write permission on *path's parent.
282 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000283 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284 return
285
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000286 if os.path.islink(path) or not os.path.isdir(path):
287 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000290 # Give up and use cmd.exe's rd command.
291 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000292 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000293 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
294 if exitcode == 0:
295 return
296 else:
Raul Tambreb946b232019-03-26 14:48:46 +0000297 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000298 time.sleep(3)
299 raise Exception('Failed to remove path %s' % path)
300
301 # On POSIX systems, we need the x-bit set on the directory to access it,
302 # the r-bit to see its contents, and the w-bit to remove files from it.
303 # The actual modes of the files within the directory is irrelevant.
304 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000305
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000306 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000307 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000308
309 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000310 # If fullpath is a symbolic link that points to a directory, isdir will
311 # be True, but we don't want to descend into that as a directory, we just
312 # want to remove the link. Check islink and treat links as ordinary files
313 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000314 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000315 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000316 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000317 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000318 # Recurse.
319 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000320
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000321 remove(os.rmdir, path)
322
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000323
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000324def safe_makedirs(tree):
325 """Creates the directory in a safe manner.
326
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000327 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000328 exception and pass on.
329 """
330 count = 0
331 while not os.path.exists(tree):
332 count += 1
333 try:
334 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000335 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000336 # 17 POSIX, 183 Windows
337 if e.errno not in (17, 183):
338 raise
339 if count > 40:
340 # Give up.
341 raise
342
343
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000344def CommandToStr(args):
345 """Converts an arg list into a shell escaped string."""
346 return ' '.join(pipes.quote(arg) for arg in args)
347
348
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000349class Wrapper(object):
350 """Wraps an object, acting as a transparent proxy for all properties by
351 default.
352 """
353 def __init__(self, wrapped):
354 self._wrapped = wrapped
355
356 def __getattr__(self, name):
357 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000358
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000359
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000360class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000361 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000362 def __init__(self, wrapped, delay):
363 super(AutoFlush, self).__init__(wrapped)
364 if not hasattr(self, 'lock'):
365 self.lock = threading.Lock()
366 self.__last_flushed_at = time.time()
367 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000368
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000369 @property
370 def autoflush(self):
371 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000372
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000373 def write(self, out, *args, **kwargs):
374 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000375 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000377 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000378 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000379 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000380 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000381 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000382 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000383 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000385
386
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000388 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000389 threads with a NN> prefix.
390 """
391 def __init__(self, wrapped, include_zero=False):
392 super(Annotated, self).__init__(wrapped)
393 if not hasattr(self, 'lock'):
394 self.lock = threading.Lock()
395 self.__output_buffers = {}
396 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000397 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000398
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000399 @property
400 def annotated(self):
401 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000402
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000403 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000404 # Store as bytes to ensure Unicode characters get output correctly.
405 if not isinstance(out, bytes):
406 out = out.encode('utf-8')
407
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000408 index = getattr(threading.currentThread(), 'index', 0)
409 if not index and not self.__include_zero:
410 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000411 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000412
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000413 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000414 try:
415 # Use a dummy array to hold the string so the code can be lockless.
416 # Strings are immutable, requiring to keep a lock for the whole dictionary
417 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000418 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000419 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000420 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000424
425 # Continue lockless.
426 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000427 while True:
428 # TODO(agable): find both of these with a single pass.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000429 cr_loc = obj[0].find(b'\r')
430 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000431 if cr_loc == lf_loc == -1:
432 break
433 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000434 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000435 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000437 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000438 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000439 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000440 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000441 obj[0] = remaining
442
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000443 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000444 """Flush buffered output."""
445 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000447 try:
448 # Detect threads no longer existing.
449 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000450 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000453 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000454 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000455 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000456 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000457 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000459 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000460 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000461 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000462 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000463 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000464
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000465
466def MakeFileAutoFlush(fileobj, delay=10):
467 autoflush = getattr(fileobj, 'autoflush', None)
468 if autoflush:
469 autoflush.delay = delay
470 return fileobj
471 return AutoFlush(fileobj, delay)
472
473
474def MakeFileAnnotated(fileobj, include_zero=False):
475 if getattr(fileobj, 'annotated', None):
476 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000477 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000478
479
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000480GCLIENT_CHILDREN = []
481GCLIENT_CHILDREN_LOCK = threading.Lock()
482
483
484class GClientChildren(object):
485 @staticmethod
486 def add(popen_obj):
487 with GCLIENT_CHILDREN_LOCK:
488 GCLIENT_CHILDREN.append(popen_obj)
489
490 @staticmethod
491 def remove(popen_obj):
492 with GCLIENT_CHILDREN_LOCK:
493 GCLIENT_CHILDREN.remove(popen_obj)
494
495 @staticmethod
496 def _attemptToKillChildren():
497 global GCLIENT_CHILDREN
498 with GCLIENT_CHILDREN_LOCK:
499 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
500
501 for zombie in zombies:
502 try:
503 zombie.kill()
504 except OSError:
505 pass
506
507 with GCLIENT_CHILDREN_LOCK:
508 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
509
510 @staticmethod
511 def _areZombies():
512 with GCLIENT_CHILDREN_LOCK:
513 return bool(GCLIENT_CHILDREN)
514
515 @staticmethod
516 def KillAllRemainingChildren():
517 GClientChildren._attemptToKillChildren()
518
519 if GClientChildren._areZombies():
520 time.sleep(0.5)
521 GClientChildren._attemptToKillChildren()
522
523 with GCLIENT_CHILDREN_LOCK:
524 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000525 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000526 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000527 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000528
529
Edward Lemur24146be2019-08-01 21:44:52 +0000530def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
531 show_header=False, always_show_header=False, retry=False,
532 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000533 """Runs a command and calls back a filter function if needed.
534
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000535 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000536 print_stdout: If True, the command's stdout is forwarded to stdout.
537 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000538 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000539 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000540 show_header: Whether to display a header before the command output.
541 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000542 retry: If the process exits non-zero, sleep for a brief interval and try
543 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000544
545 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000546
547 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000548 """
Edward Lemur24146be2019-08-01 21:44:52 +0000549 def show_header_if_necessary(needs_header, attempt):
550 """Show the header at most once."""
551 if not needs_header[0]:
552 return
553
554 needs_header[0] = False
555 # Automatically generated header. We only prepend a newline if
556 # always_show_header is false, since it usually indicates there's an
557 # external progress display, and it's better not to clobber it in that case.
558 header = '' if always_show_header else '\n'
559 header += '________ running \'%s\' in \'%s\'' % (
560 ' '.join(args), kwargs.get('cwd', '.'))
561 if attempt:
562 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
563 header += '\n'
564
565 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000566 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
567 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000568 if filter_fn:
569 filter_fn(header)
570
571 def filter_line(command_output, line_start):
572 """Extract the last line from command output and filter it."""
573 if not filter_fn or line_start is None:
574 return
575 command_output.seek(line_start)
576 filter_fn(command_output.read().decode('utf-8'))
577
578 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
579 # byte inputs and sys.stdout.buffer must be used instead.
580 if print_stdout:
581 sys.stdout.flush()
582 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
583 else:
584 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000585
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000586 sleep_interval = RETRY_INITIAL_SLEEP
587 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000588 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000589 kid = subprocess2.Popen(
590 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
591 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000592
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000593 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000594
Edward Lemur24146be2019-08-01 21:44:52 +0000595 # Store the output of the command regardless of the value of print_stdout or
596 # filter_fn.
597 command_output = io.BytesIO()
598
599 # Passed as a list for "by ref" semantics.
600 needs_header = [show_header]
601 if always_show_header:
602 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000603
604 # Also, we need to forward stdout to prevent weird re-ordering of output.
605 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700606 # normally buffering is done for each line, but if the process requests
607 # input, no end-of-line character is output after the prompt and it would
608 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000609 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000610 line_start = None
611 while True:
612 in_byte = kid.stdout.read(1)
613 is_newline = in_byte in (b'\n', b'\r')
614 if not in_byte:
615 break
616
617 show_header_if_necessary(needs_header, attempt)
618
619 if is_newline:
620 filter_line(command_output, line_start)
621 line_start = None
622 elif line_start is None:
623 line_start = command_output.tell()
624
625 stdout_write(in_byte)
626 command_output.write(in_byte)
627
628 # Flush the rest of buffered output.
629 sys.stdout.flush()
630 if line_start is not None:
631 filter_line(command_output, line_start)
632
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000633 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000634 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000635
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000636 # Don't put this in a 'finally,' since the child may still run if we get
637 # an exception.
638 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000639
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000640 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000641 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000642 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000643
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000644 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000645 return command_output.getvalue()
646
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000647 if not retry:
648 break
Edward Lemur24146be2019-08-01 21:44:52 +0000649
Raul Tambreb946b232019-03-26 14:48:46 +0000650 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
651 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000652 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000653 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000654
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000655 raise subprocess2.CalledProcessError(
656 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000657
658
agable@chromium.org5a306a22014-02-24 22:13:59 +0000659class GitFilter(object):
660 """A filter_fn implementation for quieting down git output messages.
661
662 Allows a custom function to skip certain lines (predicate), and will throttle
663 the output of percentage completed lines to only output every X seconds.
664 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000665 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000666
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000667 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000668 """
669 Args:
670 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
671 XX% complete messages) to only be printed at least |time_throttle|
672 seconds apart.
673 predicate (f(line)): An optional function which is invoked for every line.
674 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000675 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000676 """
Edward Lemur24146be2019-08-01 21:44:52 +0000677 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000678 self.last_time = 0
679 self.time_throttle = time_throttle
680 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000681 self.out_fh = out_fh or sys.stdout
682 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000683
684 def __call__(self, line):
685 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000686 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000687 if esc > -1:
688 line = line[:esc]
689 if self.predicate and not self.predicate(line):
690 return
691 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000692 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000693 if match:
694 if match.group(1) != self.progress_prefix:
695 self.progress_prefix = match.group(1)
696 elif now - self.last_time < self.time_throttle:
697 return
698 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000699 if not self.first_line:
700 self.out_fh.write('[%s] ' % Elapsed())
701 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000702 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000703
704
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000705def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000706 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000707
rcui@google.com13595ff2011-10-13 01:25:07 +0000708 Returns nearest upper-level directory with the passed in file.
709 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000710 if not path:
711 path = os.getcwd()
712 path = os.path.realpath(path)
713 while True:
714 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000715 if os.path.exists(file_path):
716 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000717 (new_path, _) = os.path.split(path)
718 if new_path == path:
719 return None
720 path = new_path
721
722
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000723def GetMacWinOrLinux():
724 """Returns 'mac', 'win', or 'linux', matching the current platform."""
725 if sys.platform.startswith(('cygwin', 'win')):
726 return 'win'
727 elif sys.platform.startswith('linux'):
728 return 'linux'
729 elif sys.platform == 'darwin':
730 return 'mac'
731 raise Error('Unknown platform: ' + sys.platform)
732
733
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000734def GetGClientRootAndEntries(path=None):
735 """Returns the gclient root and the dict of entries."""
736 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000737 root = FindFileUpwards(config_file, path)
738 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000739 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000740 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000741 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000742 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000743 with open(config_path) as config:
744 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000745 config_dir = os.path.dirname(config_path)
746 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000747
748
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000749def lockedmethod(method):
750 """Method decorator that holds self.lock for the duration of the call."""
751 def inner(self, *args, **kwargs):
752 try:
753 try:
754 self.lock.acquire()
755 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000756 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000757 raise
758 return method(self, *args, **kwargs)
759 finally:
760 self.lock.release()
761 return inner
762
763
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000764class WorkItem(object):
765 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000766 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
767 # As a workaround, use a single lock. Yep you read it right. Single lock for
768 # all the 100 objects.
769 lock = threading.Lock()
770
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000771 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000772 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000773 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000774 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000775 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700776 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000777
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000778 def run(self, work_queue):
779 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000780 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000781 pass
782
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000783 @property
784 def name(self):
785 return self._name
786
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000787
788class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000789 """Runs a set of WorkItem that have interdependencies and were WorkItem are
790 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000791
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200792 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000793 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000794
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000795 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000796 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000797 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000798 """jobs specifies the number of concurrent tasks to allow. progress is a
799 Progress instance."""
800 # Set when a thread is done or a new item is enqueued.
801 self.ready_cond = threading.Condition()
802 # Maximum number of concurrent tasks.
803 self.jobs = jobs
804 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000805 self.queued = []
806 # List of strings representing each Dependency.name that was run.
807 self.ran = []
808 # List of items currently running.
809 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000810 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000811 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000812 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813 self.progress = progress
814 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000815 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000816
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000817 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000818 self.verbose = verbose
819 self.last_join = None
820 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000821
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000822 def enqueue(self, d):
823 """Enqueue one Dependency to be executed later once its requirements are
824 satisfied.
825 """
826 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000827 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000829 self.queued.append(d)
830 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000831 if self.jobs == 1:
832 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000833 logging.debug('enqueued(%s)' % d.name)
834 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000835 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000836 self.progress.update(0)
837 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000838 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000839 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000840
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000841 def out_cb(self, _):
842 self.last_subproc_output = datetime.datetime.now()
843 return True
844
845 @staticmethod
846 def format_task_output(task, comment=''):
847 if comment:
848 comment = ' (%s)' % comment
849 if task.start and task.finish:
850 elapsed = ' (Elapsed: %s)' % (
851 str(task.finish - task.start).partition('.')[0])
852 else:
853 elapsed = ''
854 return """
855%s%s%s
856----------------------------------------
857%s
858----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000859 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000860
hinoka885e5b12016-06-08 14:40:09 -0700861 def _is_conflict(self, job):
862 """Checks to see if a job will conflict with another running job."""
863 for running_job in self.running:
864 for used_resource in running_job.item.resources:
865 logging.debug('Checking resource %s' % used_resource)
866 if used_resource in job.resources:
867 return True
868 return False
869
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 def flush(self, *args, **kwargs):
871 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000872 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000874 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000875 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000876 while True:
877 # Check for task to run first, then wait.
878 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000879 if not self.exceptions.empty():
880 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000881 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000882 self._flush_terminated_threads()
883 if (not self.queued and not self.running or
884 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000885 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000886 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000887
888 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000889 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000890 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000891 if (self.ignore_requirements or
892 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700893 if not self._is_conflict(self.queued[i]):
894 # Start one work item: all its requirements are satisfied.
895 self._run_one_task(self.queued.pop(i), args, kwargs)
896 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000897 else:
898 # Couldn't find an item that could run. Break out the outher loop.
899 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000900
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000901 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000902 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000903 break
904 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000905 try:
906 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000907 # If we haven't printed to terminal for a while, but we have received
908 # spew from a suprocess, let the user know we're still progressing.
909 now = datetime.datetime.now()
910 if (now - self.last_join > datetime.timedelta(seconds=60) and
911 self.last_subproc_output > self.last_join):
912 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000913 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000914 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000915 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000916 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000917 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000918 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000919 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000920 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000921 except KeyboardInterrupt:
922 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000923 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000924 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000925 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
926 self.ran), len(self.running)),
927 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000928 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000929 print(
930 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
931 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000932 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000933 print(
934 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000935 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000936 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000937 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000938 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000939
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000940 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000942 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000943 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000944 # To get back the stack location correctly, the raise a, b, c form must be
945 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000946 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000947 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
948 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000949 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000950 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000951
maruel@chromium.org3742c842010-09-09 19:27:14 +0000952 def _flush_terminated_threads(self):
953 """Flush threads that have terminated."""
954 running = self.running
955 self.running = []
956 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000957 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000958 self.running.append(t)
959 else:
960 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000961 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000962 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000963 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000964 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000965 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000966 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000967 if t.item.name in self.ran:
968 raise Error(
969 'gclient is confused, "%s" is already in "%s"' % (
970 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000971 if not t.item.name in self.ran:
972 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000973
974 def _run_one_task(self, task_item, args, kwargs):
975 if self.jobs > 1:
976 # Start the thread.
977 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000978 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000979 self.running.append(new_thread)
980 new_thread.start()
981 else:
982 # Run the 'thread' inside the main thread. Don't try to catch any
983 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 try:
985 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000986 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000987 task_item.run(*args, **kwargs)
988 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000989 print(
990 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000991 self.ran.append(task_item.name)
992 if self.verbose:
993 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000994 print('')
995 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000996 if self.progress:
997 self.progress.update(1, ', '.join(t.item.name for t in self.running))
998 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000999 print(
1000 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001001 raise
1002 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001003 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001004 raise
1005
maruel@chromium.org3742c842010-09-09 19:27:14 +00001006
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001007 class _Worker(threading.Thread):
1008 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001009 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001010 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001011 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001012 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001013 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001014 self.args = args
1015 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001016 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001017
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001018 def run(self):
1019 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001020 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001021 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001022 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001023 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001024 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001025 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001026 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001027 print(
1028 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001029 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001030 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001031 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001032 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001033 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001034 except Exception:
1035 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001036 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001037 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001038 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001039 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001040 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001041 work_queue.ready_cond.acquire()
1042 try:
1043 work_queue.ready_cond.notifyAll()
1044 finally:
1045 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001046
1047
agable92bec4f2016-08-24 09:27:27 -07001048def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001049 """Returns the most plausible editor to use.
1050
1051 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001052 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001053 - core.editor git configuration variable (if supplied by git-cl)
1054 - VISUAL environment variable
1055 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001056 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001057
1058 In the case of git-cl, this matches git's behaviour, except that it does not
1059 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001060 """
agable92bec4f2016-08-24 09:27:27 -07001061 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001062 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001063 editor = os.environ.get('VISUAL')
1064 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001065 editor = os.environ.get('EDITOR')
1066 if not editor:
1067 if sys.platform.startswith('win'):
1068 editor = 'notepad'
1069 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001070 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001071 return editor
1072
1073
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001074def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001075 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001076 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001077 # Make sure CRLF is handled properly by requiring none.
1078 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001079 print(
1080 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001081 fileobj = os.fdopen(file_handle, 'w')
1082 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001083 content = re.sub('\r?\n', '\n', content)
1084 # Some editors complain when the file doesn't end in \n.
1085 if not content.endswith('\n'):
1086 content += '\n'
1087 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001088 fileobj.close()
1089
1090 try:
agable92bec4f2016-08-24 09:27:27 -07001091 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001092 if not editor:
1093 return None
1094 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001095 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1096 # Msysgit requires the usage of 'env' to be present.
1097 cmd = 'env ' + cmd
1098 try:
1099 # shell=True to allow the shell to handle all forms of quotes in
1100 # $EDITOR.
1101 subprocess2.check_call(cmd, shell=True)
1102 except subprocess2.CalledProcessError:
1103 return None
1104 return FileRead(filename)
1105 finally:
1106 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001107
1108
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001109def UpgradeToHttps(url):
1110 """Upgrades random urls to https://.
1111
1112 Do not touch unknown urls like ssh:// or git://.
1113 Do not touch http:// urls with a port number,
1114 Fixes invalid GAE url.
1115 """
1116 if not url:
1117 return url
1118 if not re.match(r'[a-z\-]+\://.*', url):
1119 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1120 # relative url and will use http:///foo. Note that it defaults to http://
1121 # for compatibility with naked url like "localhost:8080".
1122 url = 'http://%s' % url
1123 parsed = list(urlparse.urlparse(url))
1124 # Do not automatically upgrade http to https if a port number is provided.
1125 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1126 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001127 return urlparse.urlunparse(parsed)
1128
1129
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001130def ParseCodereviewSettingsContent(content):
1131 """Process a codereview.settings file properly."""
1132 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1133 try:
1134 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1135 except ValueError:
1136 raise Error(
1137 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001138 def fix_url(key):
1139 if keyvals.get(key):
1140 keyvals[key] = UpgradeToHttps(keyvals[key])
1141 fix_url('CODE_REVIEW_SERVER')
1142 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001143 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001144
1145
1146def NumLocalCpus():
1147 """Returns the number of processors.
1148
dnj@chromium.org530523b2015-01-07 19:54:57 +00001149 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1150 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1151 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001152 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001153 # Surround the entire thing in try/except; no failure here should stop gclient
1154 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001155 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001156 # Use multiprocessing to get CPU count. This may raise
1157 # NotImplementedError.
1158 try:
1159 import multiprocessing
1160 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001161 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001162 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001163 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001164 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1165 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1166
1167 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1168 if 'NUMBER_OF_PROCESSORS' in os.environ:
1169 return int(os.environ['NUMBER_OF_PROCESSORS'])
1170 except Exception as e:
1171 logging.exception("Exception raised while probing CPU count: %s", e)
1172
1173 logging.debug('Failed to get CPU count. Defaulting to 1.')
1174 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001175
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001176
szager@chromium.orgfc616382014-03-18 20:32:04 +00001177def DefaultDeltaBaseCacheLimit():
1178 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1179
1180 The primary constraint is the address space of virtual memory. The cache
1181 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1182 parameter is set too high.
1183 """
1184 if platform.architecture()[0].startswith('64'):
1185 return '2g'
1186 else:
1187 return '512m'
1188
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001189
szager@chromium.orgff113292014-03-25 06:02:08 +00001190def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001191 """Return reasonable default values for configuring git-index-pack.
1192
1193 Experiments suggest that higher values for pack.threads don't improve
1194 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001195 cache_limit = DefaultDeltaBaseCacheLimit()
1196 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1197 if url in THREADED_INDEX_PACK_BLACKLIST:
1198 result.extend(['-c', 'pack.threads=1'])
1199 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001200
1201
1202def FindExecutable(executable):
1203 """This mimics the "which" utility."""
1204 path_folders = os.environ.get('PATH').split(os.pathsep)
1205
1206 for path_folder in path_folders:
1207 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001208 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001209 target = os.path.abspath(os.path.expanduser(target))
1210 if os.path.isfile(target) and os.access(target, os.X_OK):
1211 return target
1212 if sys.platform.startswith('win'):
1213 for suffix in ('.bat', '.cmd', '.exe'):
1214 alt_target = target + suffix
1215 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1216 return alt_target
1217 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001218
1219
1220def freeze(obj):
1221 """Takes a generic object ``obj``, and returns an immutable version of it.
1222
1223 Supported types:
1224 * dict / OrderedDict -> FrozenDict
1225 * list -> tuple
1226 * set -> frozenset
1227 * any object with a working __hash__ implementation (assumes that hashable
1228 means immutable)
1229
1230 Will raise TypeError if you pass an object which is not hashable.
1231 """
Raul Tambre6693d092020-02-19 20:36:45 +00001232 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001233 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001234 elif isinstance(obj, (list, tuple)):
1235 return tuple(freeze(i) for i in obj)
1236 elif isinstance(obj, set):
1237 return frozenset(freeze(i) for i in obj)
1238 else:
1239 hash(obj)
1240 return obj
1241
1242
Raul Tambre6693d092020-02-19 20:36:45 +00001243class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001244 """An immutable OrderedDict.
1245
1246 Modified From: http://stackoverflow.com/a/2704866
1247 """
1248 def __init__(self, *args, **kwargs):
1249 self._d = collections.OrderedDict(*args, **kwargs)
1250
1251 # Calculate the hash immediately so that we know all the items are
1252 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001253 self._hash = functools.reduce(
1254 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001255
1256 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001257 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001258 return NotImplemented
1259 if self is other:
1260 return True
1261 if len(self) != len(other):
1262 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001263 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001264 if k not in other or other[k] != v:
1265 return False
1266 return True
1267
1268 def __iter__(self):
1269 return iter(self._d)
1270
1271 def __len__(self):
1272 return len(self._d)
1273
1274 def __getitem__(self, key):
1275 return self._d[key]
1276
1277 def __hash__(self):
1278 return self._hash
1279
1280 def __repr__(self):
1281 return 'FrozenDict(%r)' % (self._d.items(),)