blob: af085fa97d963b8507a692bdd92e2296eb29712a [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
Ben Pastened410c662020-08-26 17:07:03 +000013import errno
Raul Tambreb946b232019-03-26 14:48:46 +000014import functools
15import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000016import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020017import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000019import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000020import platform
msb@chromium.orgac915bb2009-11-13 17:03:01 +000021import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000022import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000023import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000025import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000028import subprocess2
29
Raul Tambreb946b232019-03-26 14:48:46 +000030if sys.version_info.major == 2:
31 from cStringIO import StringIO
Raul Tambre6693d092020-02-19 20:36:45 +000032 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000033 import Queue as queue
34 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000035else:
Raul Tambre6693d092020-02-19 20:36:45 +000036 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000037 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000038 import queue
39 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000040
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000041
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000042RETRY_MAX = 3
43RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000044START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000045
46
borenet@google.com6a9b1682014-03-24 18:35:23 +000047_WARNINGS = []
48
49
szager@chromium.orgff113292014-03-25 06:02:08 +000050# These repos are known to cause OOM errors on 32-bit platforms, due the the
51# very large objects they contain. It is not safe to use threaded index-pack
52# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000053THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000054 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
55]
56
Raul Tambreb946b232019-03-26 14:48:46 +000057"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
58if sys.version_info.major == 2:
59 # We have to use exec to avoid a SyntaxError in Python 3.
60 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
61else:
62 def reraise(typ, value, tb=None):
63 if value is None:
64 value = typ()
65 if value.__traceback__ is not tb:
66 raise value.with_traceback(tb)
67 raise value
68
szager@chromium.orgff113292014-03-25 06:02:08 +000069
maruel@chromium.org66c83e62010-09-07 14:18:45 +000070class Error(Exception):
71 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000072 def __init__(self, msg, *args, **kwargs):
73 index = getattr(threading.currentThread(), 'index', 0)
74 if index:
75 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
76 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000077
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000078
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000079def Elapsed(until=None):
80 if until is None:
81 until = datetime.datetime.now()
82 return str(until - START).partition('.')[0]
83
84
borenet@google.com6a9b1682014-03-24 18:35:23 +000085def PrintWarnings():
86 """Prints any accumulated warnings."""
87 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000088 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000089 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000090 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000091
92
93def AddWarning(msg):
94 """Adds the given warning message to the list of accumulated warnings."""
95 _WARNINGS.append(msg)
96
97
Joanna Wang66286612022-06-30 19:59:13 +000098def FuzzyMatchRepo(repo, candidates):
99 # type: (str, Union[Collection[str], Mapping[str, Any]]) -> Optional[str]
100 """Attempts to find a representation of repo in the candidates.
101
102 Args:
103 repo: a string representation of a repo in the form of a url or the
104 name and path of the solution it represents.
105 candidates: The candidates to look through which may contain `repo` in
106 in any of the forms mentioned above.
107 Returns:
108 The matching string, if any, which may be in a different form from `repo`.
109 """
110 if repo in candidates:
111 return repo
112 if repo.endswith('.git') and repo[:-len('.git')] in candidates:
113 return repo[:-len('.git')]
114 if repo + '.git' in candidates:
115 return repo + '.git'
116 return None
117
118
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000119def SplitUrlRevision(url):
120 """Splits url and returns a two-tuple: url, rev"""
121 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000122 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000123 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000124 components = re.search(regex, url).groups()
125 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000126 components = url.rsplit('@', 1)
127 if re.match(r'^\w+\@', url) and '@' not in components[0]:
128 components = [url]
129
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000130 if len(components) == 1:
131 components += [None]
132 return tuple(components)
133
134
Joanna Wang1a977bd2022-06-02 21:51:17 +0000135def ExtractRefName(remote, full_refs_str):
136 """Returns the ref name if full_refs_str is a valid ref."""
137 result = re.compile(r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
138 remote).match(full_refs_str)
139 if result:
140 return result.group('ref_name')
141 return None
142
143
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000144def IsGitSha(revision):
145 """Returns true if the given string is a valid hex-encoded sha"""
146 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
147
148
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200149def IsFullGitSha(revision):
150 """Returns true if the given string is a valid hex-encoded full sha"""
151 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
152
153
floitsch@google.comeaab7842011-04-28 09:07:58 +0000154def IsDateRevision(revision):
155 """Returns true if the given revision is of the form "{ ... }"."""
156 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
157
158
159def MakeDateRevision(date):
160 """Returns a revision representing the latest revision before the given
161 date."""
162 return "{" + date + "}"
163
164
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000165def SyntaxErrorToError(filename, e):
166 """Raises a gclient_utils.Error exception with the human readable message"""
167 try:
168 # Try to construct a human readable error message
169 if filename:
170 error_message = 'There is a syntax error in %s\n' % filename
171 else:
172 error_message = 'There is a syntax error\n'
173 error_message += 'Line #%s, character %s: "%s"' % (
174 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
175 except:
176 # Something went wrong, re-raise the original exception
177 raise e
178 else:
179 raise Error(error_message)
180
181
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182class PrintableObject(object):
183 def __str__(self):
184 output = ''
185 for i in dir(self):
186 if i.startswith('__'):
187 continue
188 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
189 return output
190
191
Edward Lesmesae3586b2020-03-23 21:21:14 +0000192def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000193 # Try to load the readline module, so that "elaborate line editing" features
194 # such as backspace work for `raw_input` / `input`.
195 try:
196 import readline
197 except ImportError:
198 # The readline module does not exist in all Python distributions, e.g. on
199 # Windows. Fall back to simple input handling.
200 pass
201
Edward Lesmesae3586b2020-03-23 21:21:14 +0000202 # Use this so that it can be mocked in tests on Python 2 and 3.
203 try:
204 if sys.version_info.major == 2:
205 return raw_input(message)
206 return input(message)
207 except KeyboardInterrupt:
208 # Hide the exception.
209 sys.exit(1)
210
211
Edward Lemur419c92f2019-10-25 22:17:49 +0000212def FileRead(filename, mode='rbU'):
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000213 # mode is ignored now; we always return unicode strings.
214 with open(filename, mode='rb') as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000215 s = f.read()
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000216 try:
217 return s.decode('utf-8', 'replace')
218 except (UnicodeDecodeError, AttributeError):
Edward Lemur419c92f2019-10-25 22:17:49 +0000219 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
221
Edward Lemur1773f372020-02-22 00:27:14 +0000222def FileWrite(filename, content, mode='w', encoding='utf-8'):
223 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225
226
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000227@contextlib.contextmanager
228def temporary_directory(**kwargs):
229 tdir = tempfile.mkdtemp(**kwargs)
230 try:
231 yield tdir
232 finally:
233 if tdir:
234 rmtree(tdir)
235
236
Edward Lemur1773f372020-02-22 00:27:14 +0000237@contextlib.contextmanager
238def temporary_file():
239 """Creates a temporary file.
240
241 On Windows, a file must be closed before it can be opened again. This function
242 allows to write something like:
243
244 with gclient_utils.temporary_file() as tmp:
245 gclient_utils.FileWrite(tmp, foo)
246 useful_stuff(tmp)
247
248 Instead of something like:
249
250 with tempfile.NamedTemporaryFile(delete=False) as tmp:
251 tmp.write(foo)
252 tmp.close()
253 try:
254 useful_stuff(tmp)
255 finally:
256 os.remove(tmp.name)
257 """
258 handle, name = tempfile.mkstemp()
259 os.close(handle)
260 try:
261 yield name
262 finally:
263 os.remove(name)
264
265
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000266def safe_rename(old, new):
267 """Renames a file reliably.
268
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000269 Sometimes os.rename does not work because a dying git process keeps a handle
270 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000271 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000272 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000273 """
274 # roughly 10s
275 retries = 100
276 for i in range(retries):
277 try:
278 os.rename(old, new)
279 break
280 except OSError:
281 if i == (retries - 1):
282 # Give up.
283 raise
284 # retry
285 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
286 time.sleep(0.1)
287
288
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000289def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000290 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000291 os.remove(path)
292 else:
293 rmtree(path)
294
295
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000296def rmtree(path):
297 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000298
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000299 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000300
301 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700302 are read-only. We need to be able to force the files to be writable (i.e.,
303 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000304
305 Even with all this, Windows still sometimes fails to delete a file, citing
306 a permission error (maybe something to do with antivirus scans or disk
307 indexing). The best suggestion any of the user forums had was to wait a
308 bit and try again, so we do that too. It's hand-waving, but sometimes it
309 works. :/
310
311 On POSIX systems, things are a little bit simpler. The modes of the files
312 to be deleted doesn't matter, only the modes of the directories containing
313 them are significant. As the directory tree is traversed, each directory
314 has its mode set appropriately before descending into it. This should
315 result in the entire tree being removed, with the possible exception of
316 *path itself, because nothing attempts to change the mode of its parent.
317 Doing so would be hazardous, as it's not a directory slated for removal.
318 In the ordinary case, this is not a problem: for our purposes, the user
319 will never lack write permission on *path's parent.
320 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000321 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000322 return
323
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000324 if os.path.islink(path) or not os.path.isdir(path):
325 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000326
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000327 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000328 # Give up and use cmd.exe's rd command.
329 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000330 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000331 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
332 if exitcode == 0:
333 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000334
335 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000336 time.sleep(3)
337 raise Exception('Failed to remove path %s' % path)
338
339 # On POSIX systems, we need the x-bit set on the directory to access it,
340 # the r-bit to see its contents, and the w-bit to remove files from it.
341 # The actual modes of the files within the directory is irrelevant.
342 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000344 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000345 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000346
347 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000348 # If fullpath is a symbolic link that points to a directory, isdir will
349 # be True, but we don't want to descend into that as a directory, we just
350 # want to remove the link. Check islink and treat links as ordinary files
351 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000352 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000353 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000354 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000355 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000356 # Recurse.
357 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000358
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000359 remove(os.rmdir, path)
360
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000361
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000362def safe_makedirs(tree):
363 """Creates the directory in a safe manner.
364
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000365 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000366 exception and pass on.
367 """
368 count = 0
369 while not os.path.exists(tree):
370 count += 1
371 try:
372 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000373 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000374 # 17 POSIX, 183 Windows
375 if e.errno not in (17, 183):
376 raise
377 if count > 40:
378 # Give up.
379 raise
380
381
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000382def CommandToStr(args):
383 """Converts an arg list into a shell escaped string."""
384 return ' '.join(pipes.quote(arg) for arg in args)
385
386
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387class Wrapper(object):
388 """Wraps an object, acting as a transparent proxy for all properties by
389 default.
390 """
391 def __init__(self, wrapped):
392 self._wrapped = wrapped
393
394 def __getattr__(self, name):
395 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000396
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000397
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000398class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000399 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000400 def __init__(self, wrapped, delay):
401 super(AutoFlush, self).__init__(wrapped)
402 if not hasattr(self, 'lock'):
403 self.lock = threading.Lock()
404 self.__last_flushed_at = time.time()
405 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 @property
408 def autoflush(self):
409 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000410
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 def write(self, out, *args, **kwargs):
412 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000413 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000415 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000417 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000418 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000419 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000421 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000423
424
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000426 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000427 threads with a NN> prefix.
428 """
429 def __init__(self, wrapped, include_zero=False):
430 super(Annotated, self).__init__(wrapped)
431 if not hasattr(self, 'lock'):
432 self.lock = threading.Lock()
433 self.__output_buffers = {}
434 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000435 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000436
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000437 @property
438 def annotated(self):
439 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000440
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000441 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000442 # Store as bytes to ensure Unicode characters get output correctly.
443 if not isinstance(out, bytes):
444 out = out.encode('utf-8')
445
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 index = getattr(threading.currentThread(), 'index', 0)
447 if not index and not self.__include_zero:
448 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000449 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000450
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 try:
453 # Use a dummy array to hold the string so the code can be lockless.
454 # Strings are immutable, requiring to keep a lock for the whole dictionary
455 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000456 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000457 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000459 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000460 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000461 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000462
463 # Continue lockless.
464 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000465 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000466 cr_loc = obj[0].find(b'\r')
467 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000468 if cr_loc == lf_loc == -1:
469 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000470
471 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000472 line, remaining = obj[0].split(b'\n', 1)
Josip Sokcevic42c5bbb2022-01-24 21:42:28 +0000473 if line:
474 self._wrapped_write(b'%d>%s\n' % (index, line))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000475 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000476 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000477 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000478 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000479 obj[0] = remaining
480
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000481 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000482 """Flush buffered output."""
483 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000484 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000485 try:
486 # Detect threads no longer existing.
487 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000488 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000489 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000490 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000491 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000492 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000493 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000494 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000495 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000496
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000497 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000498 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000499 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000500 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000501 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000502
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000503
504def MakeFileAutoFlush(fileobj, delay=10):
505 autoflush = getattr(fileobj, 'autoflush', None)
506 if autoflush:
507 autoflush.delay = delay
508 return fileobj
509 return AutoFlush(fileobj, delay)
510
511
512def MakeFileAnnotated(fileobj, include_zero=False):
513 if getattr(fileobj, 'annotated', None):
514 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000515 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000516
517
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000518GCLIENT_CHILDREN = []
519GCLIENT_CHILDREN_LOCK = threading.Lock()
520
521
522class GClientChildren(object):
523 @staticmethod
524 def add(popen_obj):
525 with GCLIENT_CHILDREN_LOCK:
526 GCLIENT_CHILDREN.append(popen_obj)
527
528 @staticmethod
529 def remove(popen_obj):
530 with GCLIENT_CHILDREN_LOCK:
531 GCLIENT_CHILDREN.remove(popen_obj)
532
533 @staticmethod
534 def _attemptToKillChildren():
535 global GCLIENT_CHILDREN
536 with GCLIENT_CHILDREN_LOCK:
537 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
538
539 for zombie in zombies:
540 try:
541 zombie.kill()
542 except OSError:
543 pass
544
545 with GCLIENT_CHILDREN_LOCK:
546 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
547
548 @staticmethod
549 def _areZombies():
550 with GCLIENT_CHILDREN_LOCK:
551 return bool(GCLIENT_CHILDREN)
552
553 @staticmethod
554 def KillAllRemainingChildren():
555 GClientChildren._attemptToKillChildren()
556
557 if GClientChildren._areZombies():
558 time.sleep(0.5)
559 GClientChildren._attemptToKillChildren()
560
561 with GCLIENT_CHILDREN_LOCK:
562 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000563 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000564 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000565 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000566
567
Edward Lemur24146be2019-08-01 21:44:52 +0000568def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
569 show_header=False, always_show_header=False, retry=False,
570 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000571 """Runs a command and calls back a filter function if needed.
572
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000573 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000574 print_stdout: If True, the command's stdout is forwarded to stdout.
575 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000576 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000577 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000578 show_header: Whether to display a header before the command output.
579 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000580 retry: If the process exits non-zero, sleep for a brief interval and try
581 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000582
583 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000584
585 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000586 """
Edward Lemur24146be2019-08-01 21:44:52 +0000587 def show_header_if_necessary(needs_header, attempt):
588 """Show the header at most once."""
589 if not needs_header[0]:
590 return
591
592 needs_header[0] = False
593 # Automatically generated header. We only prepend a newline if
594 # always_show_header is false, since it usually indicates there's an
595 # external progress display, and it's better not to clobber it in that case.
596 header = '' if always_show_header else '\n'
597 header += '________ running \'%s\' in \'%s\'' % (
598 ' '.join(args), kwargs.get('cwd', '.'))
599 if attempt:
600 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
601 header += '\n'
602
603 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000604 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
605 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000606 if filter_fn:
607 filter_fn(header)
608
609 def filter_line(command_output, line_start):
610 """Extract the last line from command output and filter it."""
611 if not filter_fn or line_start is None:
612 return
613 command_output.seek(line_start)
614 filter_fn(command_output.read().decode('utf-8'))
615
616 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
617 # byte inputs and sys.stdout.buffer must be used instead.
618 if print_stdout:
619 sys.stdout.flush()
620 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
621 else:
622 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000623
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000624 sleep_interval = RETRY_INITIAL_SLEEP
625 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000626
627 # Store the output of the command regardless of the value of print_stdout or
628 # filter_fn.
629 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000630 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000631 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
632 # subprocess when filtering its output. This makes the subproc believe
633 # it was launched from a terminal, which will preserve ANSI color codes.
Milad Fad949c912020-09-18 00:26:08 +0000634 os_type = GetMacWinAixOrLinux()
635 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000636 pipe_reader, pipe_writer = os.openpty()
637 else:
638 pipe_reader, pipe_writer = os.pipe()
639
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000640 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000641 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000642 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000643 # Close the write end of the pipe once we hand it off to the child proc.
644 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000645
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000646 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000647
Edward Lemur24146be2019-08-01 21:44:52 +0000648 # Passed as a list for "by ref" semantics.
649 needs_header = [show_header]
650 if always_show_header:
651 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000652
653 # Also, we need to forward stdout to prevent weird re-ordering of output.
654 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700655 # normally buffering is done for each line, but if the process requests
656 # input, no end-of-line character is output after the prompt and it would
657 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000658 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000659 line_start = None
660 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000661 try:
662 in_byte = os.read(pipe_reader, 1)
663 except (IOError, OSError) as e:
664 if e.errno == errno.EIO:
665 # An errno.EIO means EOF?
666 in_byte = None
667 else:
668 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000669 is_newline = in_byte in (b'\n', b'\r')
670 if not in_byte:
671 break
672
673 show_header_if_necessary(needs_header, attempt)
674
675 if is_newline:
676 filter_line(command_output, line_start)
677 line_start = None
678 elif line_start is None:
679 line_start = command_output.tell()
680
681 stdout_write(in_byte)
682 command_output.write(in_byte)
683
684 # Flush the rest of buffered output.
685 sys.stdout.flush()
686 if line_start is not None:
687 filter_line(command_output, line_start)
688
Ben Pastened410c662020-08-26 17:07:03 +0000689 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000690 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000691
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000692 # Don't put this in a 'finally,' since the child may still run if we get
693 # an exception.
694 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000695
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000696 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000697 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000698 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000699
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000700 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000701 return command_output.getvalue()
702
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000703 if not retry:
704 break
Edward Lemur24146be2019-08-01 21:44:52 +0000705
Raul Tambreb946b232019-03-26 14:48:46 +0000706 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
707 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000708 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000709 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000710 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000711
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000712 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000713 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000714
715
agable@chromium.org5a306a22014-02-24 22:13:59 +0000716class GitFilter(object):
717 """A filter_fn implementation for quieting down git output messages.
718
719 Allows a custom function to skip certain lines (predicate), and will throttle
720 the output of percentage completed lines to only output every X seconds.
721 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000722 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000723
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000724 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000725 """
726 Args:
727 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
728 XX% complete messages) to only be printed at least |time_throttle|
729 seconds apart.
730 predicate (f(line)): An optional function which is invoked for every line.
731 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000732 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000733 """
Edward Lemur24146be2019-08-01 21:44:52 +0000734 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000735 self.last_time = 0
736 self.time_throttle = time_throttle
737 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000738 self.out_fh = out_fh or sys.stdout
739 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000740
741 def __call__(self, line):
742 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000743 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000744 if esc > -1:
745 line = line[:esc]
746 if self.predicate and not self.predicate(line):
747 return
748 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000749 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000750 if match:
751 if match.group(1) != self.progress_prefix:
752 self.progress_prefix = match.group(1)
753 elif now - self.last_time < self.time_throttle:
754 return
755 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000756 if not self.first_line:
757 self.out_fh.write('[%s] ' % Elapsed())
758 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000759 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000760
761
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000762def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000763 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000764
rcui@google.com13595ff2011-10-13 01:25:07 +0000765 Returns nearest upper-level directory with the passed in file.
766 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767 if not path:
768 path = os.getcwd()
769 path = os.path.realpath(path)
770 while True:
771 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000772 if os.path.exists(file_path):
773 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000774 (new_path, _) = os.path.split(path)
775 if new_path == path:
776 return None
777 path = new_path
778
779
Milad Fa52fdd1f2020-09-15 21:24:46 +0000780def GetMacWinAixOrLinux():
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000781 """Returns 'mac', 'win', or 'linux', matching the current platform."""
782 if sys.platform.startswith(('cygwin', 'win')):
783 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000784
785 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000786 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000787
788 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000789 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000790
791 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000792 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000793
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000794 raise Error('Unknown platform: ' + sys.platform)
795
796
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000797def GetGClientRootAndEntries(path=None):
798 """Returns the gclient root and the dict of entries."""
799 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000800 root = FindFileUpwards(config_file, path)
801 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000802 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000803 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000804 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000805 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000806 with open(config_path) as config:
807 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000808 config_dir = os.path.dirname(config_path)
809 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000810
811
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000812def lockedmethod(method):
813 """Method decorator that holds self.lock for the duration of the call."""
814 def inner(self, *args, **kwargs):
815 try:
816 try:
817 self.lock.acquire()
818 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000819 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000820 raise
821 return method(self, *args, **kwargs)
822 finally:
823 self.lock.release()
824 return inner
825
826
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000827class WorkItem(object):
828 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000829 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
830 # As a workaround, use a single lock. Yep you read it right. Single lock for
831 # all the 100 objects.
832 lock = threading.Lock()
833
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000834 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000835 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000836 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000837 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000838 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700839 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000840
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000841 def run(self, work_queue):
842 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000843 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000845 @property
846 def name(self):
847 return self._name
848
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000849
850class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000851 """Runs a set of WorkItem that have interdependencies and were WorkItem are
852 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000853
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200854 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000855 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000856
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000857 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000858 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000859 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 """jobs specifies the number of concurrent tasks to allow. progress is a
861 Progress instance."""
862 # Set when a thread is done or a new item is enqueued.
863 self.ready_cond = threading.Condition()
864 # Maximum number of concurrent tasks.
865 self.jobs = jobs
866 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000867 self.queued = []
868 # List of strings representing each Dependency.name that was run.
869 self.ran = []
870 # List of items currently running.
871 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000872 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000873 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000874 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000875 self.progress = progress
876 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000877 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000878
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000879 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000880 self.verbose = verbose
881 self.last_join = None
882 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000883
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000884 def enqueue(self, d):
885 """Enqueue one Dependency to be executed later once its requirements are
886 satisfied.
887 """
888 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000889 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000890 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000891 self.queued.append(d)
892 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000893 if self.jobs == 1:
894 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 logging.debug('enqueued(%s)' % d.name)
896 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000897 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 self.progress.update(0)
899 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000900 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000901 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000902
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000903 def out_cb(self, _):
904 self.last_subproc_output = datetime.datetime.now()
905 return True
906
907 @staticmethod
908 def format_task_output(task, comment=''):
909 if comment:
910 comment = ' (%s)' % comment
911 if task.start and task.finish:
912 elapsed = ' (Elapsed: %s)' % (
913 str(task.finish - task.start).partition('.')[0])
914 else:
915 elapsed = ''
916 return """
917%s%s%s
918----------------------------------------
919%s
920----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000921 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000922
hinoka885e5b12016-06-08 14:40:09 -0700923 def _is_conflict(self, job):
924 """Checks to see if a job will conflict with another running job."""
925 for running_job in self.running:
926 for used_resource in running_job.item.resources:
927 logging.debug('Checking resource %s' % used_resource)
928 if used_resource in job.resources:
929 return True
930 return False
931
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000932 def flush(self, *args, **kwargs):
933 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000934 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000935 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000936 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000937 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000938 while True:
939 # Check for task to run first, then wait.
940 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000941 if not self.exceptions.empty():
942 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000943 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000944 self._flush_terminated_threads()
945 if (not self.queued and not self.running or
946 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000947 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000948 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000949
950 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000951 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000952 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000953 if (self.ignore_requirements or
954 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700955 if not self._is_conflict(self.queued[i]):
956 # Start one work item: all its requirements are satisfied.
957 self._run_one_task(self.queued.pop(i), args, kwargs)
958 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000959 else:
960 # Couldn't find an item that could run. Break out the outher loop.
961 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000962
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000963 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000964 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000965 break
966 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000967 try:
968 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000969 # If we haven't printed to terminal for a while, but we have received
970 # spew from a suprocess, let the user know we're still progressing.
971 now = datetime.datetime.now()
972 if (now - self.last_join > datetime.timedelta(seconds=60) and
973 self.last_subproc_output > self.last_join):
974 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000975 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000976 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000977 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000978 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000979 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000981 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000982 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000983 except KeyboardInterrupt:
984 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000985 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000986 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000987 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
988 self.ran), len(self.running)),
989 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000990 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000991 print(
992 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
993 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000994 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000995 print(
996 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000997 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000998 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000999 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001000 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +00001001
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001002 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +00001003 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001004 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001005 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001006 # To get back the stack location correctly, the raise a, b, c form must be
1007 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001008 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +00001009 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
1010 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001011 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001012 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001013
maruel@chromium.org3742c842010-09-09 19:27:14 +00001014 def _flush_terminated_threads(self):
1015 """Flush threads that have terminated."""
1016 running = self.running
1017 self.running = []
1018 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +00001019 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +00001020 self.running.append(t)
1021 else:
1022 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001023 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +00001024 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001025 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +00001026 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +00001027 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001028 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001029 if t.item.name in self.ran:
1030 raise Error(
1031 'gclient is confused, "%s" is already in "%s"' % (
1032 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001033 if not t.item.name in self.ran:
1034 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001035
1036 def _run_one_task(self, task_item, args, kwargs):
1037 if self.jobs > 1:
1038 # Start the thread.
1039 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001040 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001041 self.running.append(new_thread)
1042 new_thread.start()
1043 else:
1044 # Run the 'thread' inside the main thread. Don't try to catch any
1045 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001046 try:
1047 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001048 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001049 task_item.run(*args, **kwargs)
1050 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001051 print(
1052 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001053 self.ran.append(task_item.name)
1054 if self.verbose:
1055 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001056 print('')
1057 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001058 if self.progress:
1059 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1060 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001061 print(
1062 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001063 raise
1064 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001065 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001066 raise
1067
maruel@chromium.org3742c842010-09-09 19:27:14 +00001068
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001069 class _Worker(threading.Thread):
1070 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001071 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001072 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001073 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001074 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001075 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001076 self.args = args
1077 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001078 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001079
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001080 def run(self):
1081 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001082 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001083 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001084 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001085 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001086 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001087 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001088 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001089 print(
1090 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001091 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001092 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001093 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001094 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001095 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001096 except Exception:
1097 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001098 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001099 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001100 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001101 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001102 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001103 work_queue.ready_cond.acquire()
1104 try:
1105 work_queue.ready_cond.notifyAll()
1106 finally:
1107 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001108
1109
agable92bec4f2016-08-24 09:27:27 -07001110def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001111 """Returns the most plausible editor to use.
1112
1113 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001114 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001115 - core.editor git configuration variable (if supplied by git-cl)
1116 - VISUAL environment variable
1117 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001118 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001119
1120 In the case of git-cl, this matches git's behaviour, except that it does not
1121 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001122 """
agable92bec4f2016-08-24 09:27:27 -07001123 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001124 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001125 editor = os.environ.get('VISUAL')
1126 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 editor = os.environ.get('EDITOR')
1128 if not editor:
1129 if sys.platform.startswith('win'):
1130 editor = 'notepad'
1131 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001132 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001133 return editor
1134
1135
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001136def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001137 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001138 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001139 # Make sure CRLF is handled properly by requiring none.
1140 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001141 print(
1142 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001143 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001144 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001145 content = re.sub('\r?\n', '\n', content)
1146 # Some editors complain when the file doesn't end in \n.
1147 if not content.endswith('\n'):
1148 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001149 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001150 fileobj.close()
1151
1152 try:
agable92bec4f2016-08-24 09:27:27 -07001153 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001154 if not editor:
1155 return None
1156 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001157 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1158 # Msysgit requires the usage of 'env' to be present.
1159 cmd = 'env ' + cmd
1160 try:
1161 # shell=True to allow the shell to handle all forms of quotes in
1162 # $EDITOR.
1163 subprocess2.check_call(cmd, shell=True)
1164 except subprocess2.CalledProcessError:
1165 return None
1166 return FileRead(filename)
1167 finally:
1168 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001169
1170
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001171def UpgradeToHttps(url):
1172 """Upgrades random urls to https://.
1173
1174 Do not touch unknown urls like ssh:// or git://.
1175 Do not touch http:// urls with a port number,
1176 Fixes invalid GAE url.
1177 """
1178 if not url:
1179 return url
1180 if not re.match(r'[a-z\-]+\://.*', url):
1181 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1182 # relative url and will use http:///foo. Note that it defaults to http://
1183 # for compatibility with naked url like "localhost:8080".
1184 url = 'http://%s' % url
1185 parsed = list(urlparse.urlparse(url))
1186 # Do not automatically upgrade http to https if a port number is provided.
1187 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1188 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001189 return urlparse.urlunparse(parsed)
1190
1191
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001192def ParseCodereviewSettingsContent(content):
1193 """Process a codereview.settings file properly."""
1194 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1195 try:
1196 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1197 except ValueError:
1198 raise Error(
1199 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001200 def fix_url(key):
1201 if keyvals.get(key):
1202 keyvals[key] = UpgradeToHttps(keyvals[key])
1203 fix_url('CODE_REVIEW_SERVER')
1204 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001205 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001206
1207
1208def NumLocalCpus():
1209 """Returns the number of processors.
1210
dnj@chromium.org530523b2015-01-07 19:54:57 +00001211 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1212 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1213 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001214 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001215 # Surround the entire thing in try/except; no failure here should stop gclient
1216 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001217 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001218 # Use multiprocessing to get CPU count. This may raise
1219 # NotImplementedError.
1220 try:
1221 import multiprocessing
1222 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001223 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001224 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001225 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001226 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1227 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1228
1229 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1230 if 'NUMBER_OF_PROCESSORS' in os.environ:
1231 return int(os.environ['NUMBER_OF_PROCESSORS'])
1232 except Exception as e:
1233 logging.exception("Exception raised while probing CPU count: %s", e)
1234
1235 logging.debug('Failed to get CPU count. Defaulting to 1.')
1236 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001237
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001238
szager@chromium.orgfc616382014-03-18 20:32:04 +00001239def DefaultDeltaBaseCacheLimit():
1240 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1241
1242 The primary constraint is the address space of virtual memory. The cache
1243 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1244 parameter is set too high.
1245 """
1246 if platform.architecture()[0].startswith('64'):
1247 return '2g'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001248
1249 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001250
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001251
szager@chromium.orgff113292014-03-25 06:02:08 +00001252def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001253 """Return reasonable default values for configuring git-index-pack.
1254
1255 Experiments suggest that higher values for pack.threads don't improve
1256 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001257 cache_limit = DefaultDeltaBaseCacheLimit()
1258 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001259 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001260 result.extend(['-c', 'pack.threads=1'])
1261 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001262
1263
1264def FindExecutable(executable):
1265 """This mimics the "which" utility."""
1266 path_folders = os.environ.get('PATH').split(os.pathsep)
1267
1268 for path_folder in path_folders:
1269 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001270 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001271 target = os.path.abspath(os.path.expanduser(target))
1272 if os.path.isfile(target) and os.access(target, os.X_OK):
1273 return target
1274 if sys.platform.startswith('win'):
1275 for suffix in ('.bat', '.cmd', '.exe'):
1276 alt_target = target + suffix
1277 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1278 return alt_target
1279 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001280
1281
1282def freeze(obj):
1283 """Takes a generic object ``obj``, and returns an immutable version of it.
1284
1285 Supported types:
1286 * dict / OrderedDict -> FrozenDict
1287 * list -> tuple
1288 * set -> frozenset
1289 * any object with a working __hash__ implementation (assumes that hashable
1290 means immutable)
1291
1292 Will raise TypeError if you pass an object which is not hashable.
1293 """
Raul Tambre6693d092020-02-19 20:36:45 +00001294 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001295 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001296
1297 if isinstance(obj, (list, tuple)):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001298 return tuple(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001299
1300 if isinstance(obj, set):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001301 return frozenset(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001302
1303 hash(obj)
1304 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001305
1306
Raul Tambre6693d092020-02-19 20:36:45 +00001307class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001308 """An immutable OrderedDict.
1309
1310 Modified From: http://stackoverflow.com/a/2704866
1311 """
1312 def __init__(self, *args, **kwargs):
1313 self._d = collections.OrderedDict(*args, **kwargs)
1314
1315 # Calculate the hash immediately so that we know all the items are
1316 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001317 self._hash = functools.reduce(
1318 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001319
1320 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001321 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001322 return NotImplemented
1323 if self is other:
1324 return True
1325 if len(self) != len(other):
1326 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001327 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001328 if k not in other or other[k] != v:
1329 return False
1330 return True
1331
1332 def __iter__(self):
1333 return iter(self._d)
1334
1335 def __len__(self):
1336 return len(self._d)
1337
1338 def __getitem__(self, key):
1339 return self._d[key]
1340
1341 def __hash__(self):
1342 return self._hash
1343
1344 def __repr__(self):
1345 return 'FrozenDict(%r)' % (self._d.items(),)