blob: 8267b9662b2a58ec8e43536b636219dc8f3ef20d [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:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000428 cr_loc = obj[0].find(b'\r')
429 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000430 if cr_loc == lf_loc == -1:
431 break
432 elif cr_loc == -1 or (lf_loc >= 0 and lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000433 line, remaining = obj[0].split(b'\n', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000434 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000435 self._wrapped_write(b'%d>%s\n' % (index, line))
Raul Tambre25eb8e42019-05-14 16:39:45 +0000436 elif lf_loc == -1 or (cr_loc >= 0 and cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000437 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000438 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000439 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000440 obj[0] = remaining
441
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000442 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000443 """Flush buffered output."""
444 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000445 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000446 try:
447 # Detect threads no longer existing.
448 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000449 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000450 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000451 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000452 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000453 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000454 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000455 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000456 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000457
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000458 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000459 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000460 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000461 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000462 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000463
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000464
465def MakeFileAutoFlush(fileobj, delay=10):
466 autoflush = getattr(fileobj, 'autoflush', None)
467 if autoflush:
468 autoflush.delay = delay
469 return fileobj
470 return AutoFlush(fileobj, delay)
471
472
473def MakeFileAnnotated(fileobj, include_zero=False):
474 if getattr(fileobj, 'annotated', None):
475 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000476 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000477
478
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000479GCLIENT_CHILDREN = []
480GCLIENT_CHILDREN_LOCK = threading.Lock()
481
482
483class GClientChildren(object):
484 @staticmethod
485 def add(popen_obj):
486 with GCLIENT_CHILDREN_LOCK:
487 GCLIENT_CHILDREN.append(popen_obj)
488
489 @staticmethod
490 def remove(popen_obj):
491 with GCLIENT_CHILDREN_LOCK:
492 GCLIENT_CHILDREN.remove(popen_obj)
493
494 @staticmethod
495 def _attemptToKillChildren():
496 global GCLIENT_CHILDREN
497 with GCLIENT_CHILDREN_LOCK:
498 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
499
500 for zombie in zombies:
501 try:
502 zombie.kill()
503 except OSError:
504 pass
505
506 with GCLIENT_CHILDREN_LOCK:
507 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
508
509 @staticmethod
510 def _areZombies():
511 with GCLIENT_CHILDREN_LOCK:
512 return bool(GCLIENT_CHILDREN)
513
514 @staticmethod
515 def KillAllRemainingChildren():
516 GClientChildren._attemptToKillChildren()
517
518 if GClientChildren._areZombies():
519 time.sleep(0.5)
520 GClientChildren._attemptToKillChildren()
521
522 with GCLIENT_CHILDREN_LOCK:
523 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000524 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000525 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000526 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000527
528
Edward Lemur24146be2019-08-01 21:44:52 +0000529def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
530 show_header=False, always_show_header=False, retry=False,
531 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000532 """Runs a command and calls back a filter function if needed.
533
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000534 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000535 print_stdout: If True, the command's stdout is forwarded to stdout.
536 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000537 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000538 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000539 show_header: Whether to display a header before the command output.
540 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000541 retry: If the process exits non-zero, sleep for a brief interval and try
542 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000543
544 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000545
546 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000547 """
Edward Lemur24146be2019-08-01 21:44:52 +0000548 def show_header_if_necessary(needs_header, attempt):
549 """Show the header at most once."""
550 if not needs_header[0]:
551 return
552
553 needs_header[0] = False
554 # Automatically generated header. We only prepend a newline if
555 # always_show_header is false, since it usually indicates there's an
556 # external progress display, and it's better not to clobber it in that case.
557 header = '' if always_show_header else '\n'
558 header += '________ running \'%s\' in \'%s\'' % (
559 ' '.join(args), kwargs.get('cwd', '.'))
560 if attempt:
561 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
562 header += '\n'
563
564 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000565 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
566 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000567 if filter_fn:
568 filter_fn(header)
569
570 def filter_line(command_output, line_start):
571 """Extract the last line from command output and filter it."""
572 if not filter_fn or line_start is None:
573 return
574 command_output.seek(line_start)
575 filter_fn(command_output.read().decode('utf-8'))
576
577 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
578 # byte inputs and sys.stdout.buffer must be used instead.
579 if print_stdout:
580 sys.stdout.flush()
581 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
582 else:
583 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000584
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000585 sleep_interval = RETRY_INITIAL_SLEEP
586 run_cwd = kwargs.get('cwd', os.getcwd())
Edward Lemur24146be2019-08-01 21:44:52 +0000587 for attempt in range(RETRY_MAX + 1):
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000588 kid = subprocess2.Popen(
589 args, bufsize=0, stdout=subprocess2.PIPE, stderr=subprocess2.STDOUT,
590 **kwargs)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000591
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000592 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000593
Edward Lemur24146be2019-08-01 21:44:52 +0000594 # Store the output of the command regardless of the value of print_stdout or
595 # filter_fn.
596 command_output = io.BytesIO()
597
598 # Passed as a list for "by ref" semantics.
599 needs_header = [show_header]
600 if always_show_header:
601 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000602
603 # Also, we need to forward stdout to prevent weird re-ordering of output.
604 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700605 # normally buffering is done for each line, but if the process requests
606 # input, no end-of-line character is output after the prompt and it would
607 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000608 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000609 line_start = None
610 while True:
611 in_byte = kid.stdout.read(1)
612 is_newline = in_byte in (b'\n', b'\r')
613 if not in_byte:
614 break
615
616 show_header_if_necessary(needs_header, attempt)
617
618 if is_newline:
619 filter_line(command_output, line_start)
620 line_start = None
621 elif line_start is None:
622 line_start = command_output.tell()
623
624 stdout_write(in_byte)
625 command_output.write(in_byte)
626
627 # Flush the rest of buffered output.
628 sys.stdout.flush()
629 if line_start is not None:
630 filter_line(command_output, line_start)
631
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000632 rv = kid.wait()
Edward Lemurdf746d02019-07-27 00:42:46 +0000633 kid.stdout.close()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000634
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000635 # Don't put this in a 'finally,' since the child may still run if we get
636 # an exception.
637 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000638
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000639 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000640 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000641 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000642
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000643 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000644 return command_output.getvalue()
645
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000646 if not retry:
647 break
Edward Lemur24146be2019-08-01 21:44:52 +0000648
Raul Tambreb946b232019-03-26 14:48:46 +0000649 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
650 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000651 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000652 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000653
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000654 raise subprocess2.CalledProcessError(
655 rv, args, kwargs.get('cwd', None), None, None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000656
657
agable@chromium.org5a306a22014-02-24 22:13:59 +0000658class GitFilter(object):
659 """A filter_fn implementation for quieting down git output messages.
660
661 Allows a custom function to skip certain lines (predicate), and will throttle
662 the output of percentage completed lines to only output every X seconds.
663 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000664 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000665
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000666 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000667 """
668 Args:
669 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
670 XX% complete messages) to only be printed at least |time_throttle|
671 seconds apart.
672 predicate (f(line)): An optional function which is invoked for every line.
673 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000674 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000675 """
Edward Lemur24146be2019-08-01 21:44:52 +0000676 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000677 self.last_time = 0
678 self.time_throttle = time_throttle
679 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000680 self.out_fh = out_fh or sys.stdout
681 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000682
683 def __call__(self, line):
684 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000685 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000686 if esc > -1:
687 line = line[:esc]
688 if self.predicate and not self.predicate(line):
689 return
690 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000691 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000692 if match:
693 if match.group(1) != self.progress_prefix:
694 self.progress_prefix = match.group(1)
695 elif now - self.last_time < self.time_throttle:
696 return
697 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000698 if not self.first_line:
699 self.out_fh.write('[%s] ' % Elapsed())
700 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000701 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000702
703
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000704def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000705 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000706
rcui@google.com13595ff2011-10-13 01:25:07 +0000707 Returns nearest upper-level directory with the passed in file.
708 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000709 if not path:
710 path = os.getcwd()
711 path = os.path.realpath(path)
712 while True:
713 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000714 if os.path.exists(file_path):
715 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000716 (new_path, _) = os.path.split(path)
717 if new_path == path:
718 return None
719 path = new_path
720
721
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000722def GetMacWinOrLinux():
723 """Returns 'mac', 'win', or 'linux', matching the current platform."""
724 if sys.platform.startswith(('cygwin', 'win')):
725 return 'win'
726 elif sys.platform.startswith('linux'):
727 return 'linux'
728 elif sys.platform == 'darwin':
729 return 'mac'
730 raise Error('Unknown platform: ' + sys.platform)
731
732
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000733def GetGClientRootAndEntries(path=None):
734 """Returns the gclient root and the dict of entries."""
735 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000736 root = FindFileUpwards(config_file, path)
737 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000738 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000739 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000740 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000741 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000742 with open(config_path) as config:
743 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000744 config_dir = os.path.dirname(config_path)
745 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000746
747
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000748def lockedmethod(method):
749 """Method decorator that holds self.lock for the duration of the call."""
750 def inner(self, *args, **kwargs):
751 try:
752 try:
753 self.lock.acquire()
754 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000755 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000756 raise
757 return method(self, *args, **kwargs)
758 finally:
759 self.lock.release()
760 return inner
761
762
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000763class WorkItem(object):
764 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000765 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
766 # As a workaround, use a single lock. Yep you read it right. Single lock for
767 # all the 100 objects.
768 lock = threading.Lock()
769
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000770 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000771 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000772 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000773 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000774 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700775 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000776
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000777 def run(self, work_queue):
778 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000779 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780 pass
781
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000782 @property
783 def name(self):
784 return self._name
785
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000786
787class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000788 """Runs a set of WorkItem that have interdependencies and were WorkItem are
789 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000790
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200791 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000792 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000793
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000794 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000795 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000796 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000797 """jobs specifies the number of concurrent tasks to allow. progress is a
798 Progress instance."""
799 # Set when a thread is done or a new item is enqueued.
800 self.ready_cond = threading.Condition()
801 # Maximum number of concurrent tasks.
802 self.jobs = jobs
803 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000804 self.queued = []
805 # List of strings representing each Dependency.name that was run.
806 self.ran = []
807 # List of items currently running.
808 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000809 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000810 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000811 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000812 self.progress = progress
813 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000814 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000815
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000816 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000817 self.verbose = verbose
818 self.last_join = None
819 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000820
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000821 def enqueue(self, d):
822 """Enqueue one Dependency to be executed later once its requirements are
823 satisfied.
824 """
825 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000826 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000827 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 self.queued.append(d)
829 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000830 if self.jobs == 1:
831 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000832 logging.debug('enqueued(%s)' % d.name)
833 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000834 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000835 self.progress.update(0)
836 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000838 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000839
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000840 def out_cb(self, _):
841 self.last_subproc_output = datetime.datetime.now()
842 return True
843
844 @staticmethod
845 def format_task_output(task, comment=''):
846 if comment:
847 comment = ' (%s)' % comment
848 if task.start and task.finish:
849 elapsed = ' (Elapsed: %s)' % (
850 str(task.finish - task.start).partition('.')[0])
851 else:
852 elapsed = ''
853 return """
854%s%s%s
855----------------------------------------
856%s
857----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000858 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000859
hinoka885e5b12016-06-08 14:40:09 -0700860 def _is_conflict(self, job):
861 """Checks to see if a job will conflict with another running job."""
862 for running_job in self.running:
863 for used_resource in running_job.item.resources:
864 logging.debug('Checking resource %s' % used_resource)
865 if used_resource in job.resources:
866 return True
867 return False
868
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000869 def flush(self, *args, **kwargs):
870 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000871 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000872 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000873 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000874 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000875 while True:
876 # Check for task to run first, then wait.
877 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000878 if not self.exceptions.empty():
879 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000880 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000881 self._flush_terminated_threads()
882 if (not self.queued and not self.running or
883 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000884 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000885 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000886
887 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000888 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000889 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000890 if (self.ignore_requirements or
891 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700892 if not self._is_conflict(self.queued[i]):
893 # Start one work item: all its requirements are satisfied.
894 self._run_one_task(self.queued.pop(i), args, kwargs)
895 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000896 else:
897 # Couldn't find an item that could run. Break out the outher loop.
898 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000899
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000900 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000901 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000902 break
903 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000904 try:
905 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 # If we haven't printed to terminal for a while, but we have received
907 # spew from a suprocess, let the user know we're still progressing.
908 now = datetime.datetime.now()
909 if (now - self.last_join > datetime.timedelta(seconds=60) and
910 self.last_subproc_output > self.last_join):
911 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000912 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000913 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000914 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000915 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000916 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000917 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000918 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000919 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000920 except KeyboardInterrupt:
921 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000922 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000923 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000924 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
925 self.ran), len(self.running)),
926 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000927 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000928 print(
929 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
930 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000931 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000932 print(
933 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000934 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000935 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000936 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000937 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000938
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000939 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000940 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000941 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000942 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000943 # To get back the stack location correctly, the raise a, b, c form must be
944 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000945 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000946 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
947 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000948 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000949 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000950
maruel@chromium.org3742c842010-09-09 19:27:14 +0000951 def _flush_terminated_threads(self):
952 """Flush threads that have terminated."""
953 running = self.running
954 self.running = []
955 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000956 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000957 self.running.append(t)
958 else:
959 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000960 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000961 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000962 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000963 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000964 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000965 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000966 if t.item.name in self.ran:
967 raise Error(
968 'gclient is confused, "%s" is already in "%s"' % (
969 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +0000970 if not t.item.name in self.ran:
971 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000972
973 def _run_one_task(self, task_item, args, kwargs):
974 if self.jobs > 1:
975 # Start the thread.
976 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000977 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +0000978 self.running.append(new_thread)
979 new_thread.start()
980 else:
981 # Run the 'thread' inside the main thread. Don't try to catch any
982 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 try:
984 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000985 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000986 task_item.run(*args, **kwargs)
987 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +0000988 print(
989 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000990 self.ran.append(task_item.name)
991 if self.verbose:
992 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000993 print('')
994 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000995 if self.progress:
996 self.progress.update(1, ', '.join(t.item.name for t in self.running))
997 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000998 print(
999 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 raise
1001 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001002 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001003 raise
1004
maruel@chromium.org3742c842010-09-09 19:27:14 +00001005
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001006 class _Worker(threading.Thread):
1007 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001008 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001009 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001010 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001011 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001012 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001013 self.args = args
1014 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001015 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001016
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001017 def run(self):
1018 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001019 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001020 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001021 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001022 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001023 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001024 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001026 print(
1027 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001028 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001029 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001030 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001031 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001032 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001033 except Exception:
1034 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001035 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001036 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001037 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001038 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001039 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001040 work_queue.ready_cond.acquire()
1041 try:
1042 work_queue.ready_cond.notifyAll()
1043 finally:
1044 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001045
1046
agable92bec4f2016-08-24 09:27:27 -07001047def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001048 """Returns the most plausible editor to use.
1049
1050 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001051 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001052 - core.editor git configuration variable (if supplied by git-cl)
1053 - VISUAL environment variable
1054 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001055 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001056
1057 In the case of git-cl, this matches git's behaviour, except that it does not
1058 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001059 """
agable92bec4f2016-08-24 09:27:27 -07001060 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001061 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001062 editor = os.environ.get('VISUAL')
1063 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001064 editor = os.environ.get('EDITOR')
1065 if not editor:
1066 if sys.platform.startswith('win'):
1067 editor = 'notepad'
1068 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001069 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001070 return editor
1071
1072
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001073def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001074 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001075 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001076 # Make sure CRLF is handled properly by requiring none.
1077 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001078 print(
1079 '!! Please remove \\r from your change description !!', file=sys.stderr)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001080 fileobj = os.fdopen(file_handle, 'w')
1081 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001082 content = re.sub('\r?\n', '\n', content)
1083 # Some editors complain when the file doesn't end in \n.
1084 if not content.endswith('\n'):
1085 content += '\n'
1086 fileobj.write(content)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001087 fileobj.close()
1088
1089 try:
agable92bec4f2016-08-24 09:27:27 -07001090 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001091 if not editor:
1092 return None
1093 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001094 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1095 # Msysgit requires the usage of 'env' to be present.
1096 cmd = 'env ' + cmd
1097 try:
1098 # shell=True to allow the shell to handle all forms of quotes in
1099 # $EDITOR.
1100 subprocess2.check_call(cmd, shell=True)
1101 except subprocess2.CalledProcessError:
1102 return None
1103 return FileRead(filename)
1104 finally:
1105 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001106
1107
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001108def UpgradeToHttps(url):
1109 """Upgrades random urls to https://.
1110
1111 Do not touch unknown urls like ssh:// or git://.
1112 Do not touch http:// urls with a port number,
1113 Fixes invalid GAE url.
1114 """
1115 if not url:
1116 return url
1117 if not re.match(r'[a-z\-]+\://.*', url):
1118 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1119 # relative url and will use http:///foo. Note that it defaults to http://
1120 # for compatibility with naked url like "localhost:8080".
1121 url = 'http://%s' % url
1122 parsed = list(urlparse.urlparse(url))
1123 # Do not automatically upgrade http to https if a port number is provided.
1124 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1125 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001126 return urlparse.urlunparse(parsed)
1127
1128
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001129def ParseCodereviewSettingsContent(content):
1130 """Process a codereview.settings file properly."""
1131 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1132 try:
1133 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1134 except ValueError:
1135 raise Error(
1136 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001137 def fix_url(key):
1138 if keyvals.get(key):
1139 keyvals[key] = UpgradeToHttps(keyvals[key])
1140 fix_url('CODE_REVIEW_SERVER')
1141 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001142 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001143
1144
1145def NumLocalCpus():
1146 """Returns the number of processors.
1147
dnj@chromium.org530523b2015-01-07 19:54:57 +00001148 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1149 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1150 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001151 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001152 # Surround the entire thing in try/except; no failure here should stop gclient
1153 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001154 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001155 # Use multiprocessing to get CPU count. This may raise
1156 # NotImplementedError.
1157 try:
1158 import multiprocessing
1159 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001160 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001161 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001162 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001163 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1164 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1165
1166 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1167 if 'NUMBER_OF_PROCESSORS' in os.environ:
1168 return int(os.environ['NUMBER_OF_PROCESSORS'])
1169 except Exception as e:
1170 logging.exception("Exception raised while probing CPU count: %s", e)
1171
1172 logging.debug('Failed to get CPU count. Defaulting to 1.')
1173 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001174
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001175
szager@chromium.orgfc616382014-03-18 20:32:04 +00001176def DefaultDeltaBaseCacheLimit():
1177 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1178
1179 The primary constraint is the address space of virtual memory. The cache
1180 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1181 parameter is set too high.
1182 """
1183 if platform.architecture()[0].startswith('64'):
1184 return '2g'
1185 else:
1186 return '512m'
1187
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001188
szager@chromium.orgff113292014-03-25 06:02:08 +00001189def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001190 """Return reasonable default values for configuring git-index-pack.
1191
1192 Experiments suggest that higher values for pack.threads don't improve
1193 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001194 cache_limit = DefaultDeltaBaseCacheLimit()
1195 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1196 if url in THREADED_INDEX_PACK_BLACKLIST:
1197 result.extend(['-c', 'pack.threads=1'])
1198 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001199
1200
1201def FindExecutable(executable):
1202 """This mimics the "which" utility."""
1203 path_folders = os.environ.get('PATH').split(os.pathsep)
1204
1205 for path_folder in path_folders:
1206 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001207 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001208 target = os.path.abspath(os.path.expanduser(target))
1209 if os.path.isfile(target) and os.access(target, os.X_OK):
1210 return target
1211 if sys.platform.startswith('win'):
1212 for suffix in ('.bat', '.cmd', '.exe'):
1213 alt_target = target + suffix
1214 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1215 return alt_target
1216 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001217
1218
1219def freeze(obj):
1220 """Takes a generic object ``obj``, and returns an immutable version of it.
1221
1222 Supported types:
1223 * dict / OrderedDict -> FrozenDict
1224 * list -> tuple
1225 * set -> frozenset
1226 * any object with a working __hash__ implementation (assumes that hashable
1227 means immutable)
1228
1229 Will raise TypeError if you pass an object which is not hashable.
1230 """
Raul Tambre6693d092020-02-19 20:36:45 +00001231 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001232 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001233 elif isinstance(obj, (list, tuple)):
1234 return tuple(freeze(i) for i in obj)
1235 elif isinstance(obj, set):
1236 return frozenset(freeze(i) for i in obj)
1237 else:
1238 hash(obj)
1239 return obj
1240
1241
Raul Tambre6693d092020-02-19 20:36:45 +00001242class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001243 """An immutable OrderedDict.
1244
1245 Modified From: http://stackoverflow.com/a/2704866
1246 """
1247 def __init__(self, *args, **kwargs):
1248 self._d = collections.OrderedDict(*args, **kwargs)
1249
1250 # Calculate the hash immediately so that we know all the items are
1251 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001252 self._hash = functools.reduce(
1253 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001254
1255 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001256 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001257 return NotImplemented
1258 if self is other:
1259 return True
1260 if len(self) != len(other):
1261 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001262 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001263 if k not in other or other[k] != v:
1264 return False
1265 return True
1266
1267 def __iter__(self):
1268 return iter(self._d)
1269
1270 def __len__(self):
1271 return len(self._d)
1272
1273 def __getitem__(self, key):
1274 return self._d[key]
1275
1276 def __hash__(self):
1277 return self._hash
1278
1279 def __repr__(self):
1280 return 'FrozenDict(%r)' % (self._d.items(),)