blob: 6a1659df343e0277437f69050d8dbccca5b7cf8a [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
Josip Sokcevic38d669f2022-09-02 18:08:57 +000042# Git wrapper retries on a transient error, and some callees do retries too,
43# such as GitWrapper.update (doing clone). One retry attempt should be
44# sufficient to help with any transient errors at this level.
45RETRY_MAX = 1
46RETRY_INITIAL_SLEEP = 2 # in seconds
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000047START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000048
49
borenet@google.com6a9b1682014-03-24 18:35:23 +000050_WARNINGS = []
51
52
szager@chromium.orgff113292014-03-25 06:02:08 +000053# These repos are known to cause OOM errors on 32-bit platforms, due the the
54# very large objects they contain. It is not safe to use threaded index-pack
55# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000056THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000057 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
58]
59
Raul Tambreb946b232019-03-26 14:48:46 +000060"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
61if sys.version_info.major == 2:
62 # We have to use exec to avoid a SyntaxError in Python 3.
63 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
64else:
65 def reraise(typ, value, tb=None):
66 if value is None:
67 value = typ()
68 if value.__traceback__ is not tb:
69 raise value.with_traceback(tb)
70 raise value
71
szager@chromium.orgff113292014-03-25 06:02:08 +000072
maruel@chromium.org66c83e62010-09-07 14:18:45 +000073class Error(Exception):
74 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000075 def __init__(self, msg, *args, **kwargs):
76 index = getattr(threading.currentThread(), 'index', 0)
77 if index:
78 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
79 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000080
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000081
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000082def Elapsed(until=None):
83 if until is None:
84 until = datetime.datetime.now()
85 return str(until - START).partition('.')[0]
86
87
borenet@google.com6a9b1682014-03-24 18:35:23 +000088def PrintWarnings():
89 """Prints any accumulated warnings."""
90 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000091 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000092 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000093 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000094
95
96def AddWarning(msg):
97 """Adds the given warning message to the list of accumulated warnings."""
98 _WARNINGS.append(msg)
99
100
Joanna Wang66286612022-06-30 19:59:13 +0000101def FuzzyMatchRepo(repo, candidates):
102 # type: (str, Union[Collection[str], Mapping[str, Any]]) -> Optional[str]
103 """Attempts to find a representation of repo in the candidates.
104
105 Args:
106 repo: a string representation of a repo in the form of a url or the
107 name and path of the solution it represents.
108 candidates: The candidates to look through which may contain `repo` in
109 in any of the forms mentioned above.
110 Returns:
111 The matching string, if any, which may be in a different form from `repo`.
112 """
113 if repo in candidates:
114 return repo
115 if repo.endswith('.git') and repo[:-len('.git')] in candidates:
116 return repo[:-len('.git')]
117 if repo + '.git' in candidates:
118 return repo + '.git'
119 return None
120
121
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000122def SplitUrlRevision(url):
123 """Splits url and returns a two-tuple: url, rev"""
124 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000125 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000126 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000127 components = re.search(regex, url).groups()
128 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000129 components = url.rsplit('@', 1)
130 if re.match(r'^\w+\@', url) and '@' not in components[0]:
131 components = [url]
132
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000133 if len(components) == 1:
134 components += [None]
135 return tuple(components)
136
137
Joanna Wang1a977bd2022-06-02 21:51:17 +0000138def ExtractRefName(remote, full_refs_str):
139 """Returns the ref name if full_refs_str is a valid ref."""
140 result = re.compile(r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
141 remote).match(full_refs_str)
142 if result:
143 return result.group('ref_name')
144 return None
145
146
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000147def IsGitSha(revision):
148 """Returns true if the given string is a valid hex-encoded sha"""
149 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
150
151
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200152def IsFullGitSha(revision):
153 """Returns true if the given string is a valid hex-encoded full sha"""
154 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
155
156
floitsch@google.comeaab7842011-04-28 09:07:58 +0000157def IsDateRevision(revision):
158 """Returns true if the given revision is of the form "{ ... }"."""
159 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
160
161
162def MakeDateRevision(date):
163 """Returns a revision representing the latest revision before the given
164 date."""
165 return "{" + date + "}"
166
167
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000168def SyntaxErrorToError(filename, e):
169 """Raises a gclient_utils.Error exception with the human readable message"""
170 try:
171 # Try to construct a human readable error message
172 if filename:
173 error_message = 'There is a syntax error in %s\n' % filename
174 else:
175 error_message = 'There is a syntax error\n'
176 error_message += 'Line #%s, character %s: "%s"' % (
177 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
178 except:
179 # Something went wrong, re-raise the original exception
180 raise e
181 else:
182 raise Error(error_message)
183
184
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000185class PrintableObject(object):
186 def __str__(self):
187 output = ''
188 for i in dir(self):
189 if i.startswith('__'):
190 continue
191 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
192 return output
193
194
Edward Lesmesae3586b2020-03-23 21:21:14 +0000195def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000196 # Try to load the readline module, so that "elaborate line editing" features
197 # such as backspace work for `raw_input` / `input`.
198 try:
199 import readline
200 except ImportError:
201 # The readline module does not exist in all Python distributions, e.g. on
202 # Windows. Fall back to simple input handling.
203 pass
204
Edward Lesmesae3586b2020-03-23 21:21:14 +0000205 # Use this so that it can be mocked in tests on Python 2 and 3.
206 try:
207 if sys.version_info.major == 2:
208 return raw_input(message)
209 return input(message)
210 except KeyboardInterrupt:
211 # Hide the exception.
212 sys.exit(1)
213
214
Edward Lemur419c92f2019-10-25 22:17:49 +0000215def FileRead(filename, mode='rbU'):
Josip Sokcevic7958e302023-03-01 23:02:21 +0000216 # mode is ignored now; we always return unicode strings.
217 with open(filename, mode='rb') as f:
218 s = f.read()
219 try:
220 return s.decode('utf-8', 'replace')
221 except (UnicodeDecodeError, AttributeError):
222 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000223
224
Edward Lemur1773f372020-02-22 00:27:14 +0000225def FileWrite(filename, content, mode='w', encoding='utf-8'):
Josip Sokcevic7958e302023-03-01 23:02:21 +0000226 with codecs.open(filename, mode=mode, encoding=encoding) as f:
227 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000228
229
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000230@contextlib.contextmanager
231def temporary_directory(**kwargs):
232 tdir = tempfile.mkdtemp(**kwargs)
233 try:
234 yield tdir
235 finally:
236 if tdir:
237 rmtree(tdir)
238
239
Edward Lemur1773f372020-02-22 00:27:14 +0000240@contextlib.contextmanager
241def temporary_file():
242 """Creates a temporary file.
243
244 On Windows, a file must be closed before it can be opened again. This function
245 allows to write something like:
246
247 with gclient_utils.temporary_file() as tmp:
248 gclient_utils.FileWrite(tmp, foo)
249 useful_stuff(tmp)
250
251 Instead of something like:
252
253 with tempfile.NamedTemporaryFile(delete=False) as tmp:
254 tmp.write(foo)
255 tmp.close()
256 try:
257 useful_stuff(tmp)
258 finally:
259 os.remove(tmp.name)
260 """
261 handle, name = tempfile.mkstemp()
262 os.close(handle)
263 try:
264 yield name
265 finally:
266 os.remove(name)
267
268
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000269def safe_rename(old, new):
270 """Renames a file reliably.
271
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000272 Sometimes os.rename does not work because a dying git process keeps a handle
273 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000274 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000275 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000276 """
277 # roughly 10s
278 retries = 100
279 for i in range(retries):
280 try:
281 os.rename(old, new)
282 break
283 except OSError:
284 if i == (retries - 1):
285 # Give up.
286 raise
287 # retry
288 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
289 time.sleep(0.1)
290
291
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000292def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000293 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000294 os.remove(path)
295 else:
296 rmtree(path)
297
298
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000299def rmtree(path):
300 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000301
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000302 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000303
304 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700305 are read-only. We need to be able to force the files to be writable (i.e.,
306 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000307
308 Even with all this, Windows still sometimes fails to delete a file, citing
309 a permission error (maybe something to do with antivirus scans or disk
310 indexing). The best suggestion any of the user forums had was to wait a
311 bit and try again, so we do that too. It's hand-waving, but sometimes it
312 works. :/
313
314 On POSIX systems, things are a little bit simpler. The modes of the files
315 to be deleted doesn't matter, only the modes of the directories containing
316 them are significant. As the directory tree is traversed, each directory
317 has its mode set appropriately before descending into it. This should
318 result in the entire tree being removed, with the possible exception of
319 *path itself, because nothing attempts to change the mode of its parent.
320 Doing so would be hazardous, as it's not a directory slated for removal.
321 In the ordinary case, this is not a problem: for our purposes, the user
322 will never lack write permission on *path's parent.
323 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000324 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000325 return
326
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000327 if os.path.islink(path) or not os.path.isdir(path):
328 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000329
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000330 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000331 # Give up and use cmd.exe's rd command.
332 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000333 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000334 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
335 if exitcode == 0:
336 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000337
338 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000339 time.sleep(3)
340 raise Exception('Failed to remove path %s' % path)
341
342 # On POSIX systems, we need the x-bit set on the directory to access it,
343 # the r-bit to see its contents, and the w-bit to remove files from it.
344 # The actual modes of the files within the directory is irrelevant.
345 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000346
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000347 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000348 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000349
350 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000351 # If fullpath is a symbolic link that points to a directory, isdir will
352 # be True, but we don't want to descend into that as a directory, we just
353 # want to remove the link. Check islink and treat links as ordinary files
354 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000355 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000356 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000357 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000358 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000359 # Recurse.
360 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000361
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000362 remove(os.rmdir, path)
363
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000364
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000365def safe_makedirs(tree):
366 """Creates the directory in a safe manner.
367
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000368 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000369 exception and pass on.
370 """
371 count = 0
372 while not os.path.exists(tree):
373 count += 1
374 try:
375 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000376 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000377 # 17 POSIX, 183 Windows
378 if e.errno not in (17, 183):
379 raise
380 if count > 40:
381 # Give up.
382 raise
383
384
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000385def CommandToStr(args):
386 """Converts an arg list into a shell escaped string."""
387 return ' '.join(pipes.quote(arg) for arg in args)
388
389
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390class Wrapper(object):
391 """Wraps an object, acting as a transparent proxy for all properties by
392 default.
393 """
394 def __init__(self, wrapped):
395 self._wrapped = wrapped
396
397 def __getattr__(self, name):
398 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000399
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000400
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000402 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000403 def __init__(self, wrapped, delay):
404 super(AutoFlush, self).__init__(wrapped)
405 if not hasattr(self, 'lock'):
406 self.lock = threading.Lock()
407 self.__last_flushed_at = time.time()
408 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000409
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000410 @property
411 def autoflush(self):
412 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000413
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 def write(self, out, *args, **kwargs):
415 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000416 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000417 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000418 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000419 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000420 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000422 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000423 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000424 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000426
427
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000428class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000429 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000430 threads with a NN> prefix.
431 """
432 def __init__(self, wrapped, include_zero=False):
433 super(Annotated, self).__init__(wrapped)
434 if not hasattr(self, 'lock'):
435 self.lock = threading.Lock()
436 self.__output_buffers = {}
437 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000438 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000439
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000440 @property
441 def annotated(self):
442 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000443
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000444 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000445 # Store as bytes to ensure Unicode characters get output correctly.
446 if not isinstance(out, bytes):
447 out = out.encode('utf-8')
448
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000449 index = getattr(threading.currentThread(), 'index', 0)
450 if not index and not self.__include_zero:
451 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000452 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000453
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000454 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000455 try:
456 # Use a dummy array to hold the string so the code can be lockless.
457 # Strings are immutable, requiring to keep a lock for the whole dictionary
458 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000459 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000460 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000461 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000462 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000463 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000464 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000465
466 # Continue lockless.
467 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000468 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000469 cr_loc = obj[0].find(b'\r')
470 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000471 if cr_loc == lf_loc == -1:
472 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000473
474 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000475 line, remaining = obj[0].split(b'\n', 1)
Josip Sokcevic42c5bbb2022-01-24 21:42:28 +0000476 if line:
477 self._wrapped_write(b'%d>%s\n' % (index, line))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000478 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000479 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000480 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000481 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000482 obj[0] = remaining
483
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000484 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000485 """Flush buffered output."""
486 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000487 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000488 try:
489 # Detect threads no longer existing.
490 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000491 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000492 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000493 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000494 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000495 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000496 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000497 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000498 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000499
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000500 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000501 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000502 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000503 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000504 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000505
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000506
507def MakeFileAutoFlush(fileobj, delay=10):
508 autoflush = getattr(fileobj, 'autoflush', None)
509 if autoflush:
510 autoflush.delay = delay
511 return fileobj
512 return AutoFlush(fileobj, delay)
513
514
515def MakeFileAnnotated(fileobj, include_zero=False):
516 if getattr(fileobj, 'annotated', None):
517 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000518 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000519
520
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000521GCLIENT_CHILDREN = []
522GCLIENT_CHILDREN_LOCK = threading.Lock()
523
524
525class GClientChildren(object):
526 @staticmethod
527 def add(popen_obj):
528 with GCLIENT_CHILDREN_LOCK:
529 GCLIENT_CHILDREN.append(popen_obj)
530
531 @staticmethod
532 def remove(popen_obj):
533 with GCLIENT_CHILDREN_LOCK:
534 GCLIENT_CHILDREN.remove(popen_obj)
535
536 @staticmethod
537 def _attemptToKillChildren():
538 global GCLIENT_CHILDREN
539 with GCLIENT_CHILDREN_LOCK:
540 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
541
542 for zombie in zombies:
543 try:
544 zombie.kill()
545 except OSError:
546 pass
547
548 with GCLIENT_CHILDREN_LOCK:
549 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
550
551 @staticmethod
552 def _areZombies():
553 with GCLIENT_CHILDREN_LOCK:
554 return bool(GCLIENT_CHILDREN)
555
556 @staticmethod
557 def KillAllRemainingChildren():
558 GClientChildren._attemptToKillChildren()
559
560 if GClientChildren._areZombies():
561 time.sleep(0.5)
562 GClientChildren._attemptToKillChildren()
563
564 with GCLIENT_CHILDREN_LOCK:
565 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000566 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000567 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000568 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000569
570
Edward Lemur24146be2019-08-01 21:44:52 +0000571def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
572 show_header=False, always_show_header=False, retry=False,
573 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000574 """Runs a command and calls back a filter function if needed.
575
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000576 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000577 print_stdout: If True, the command's stdout is forwarded to stdout.
578 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000579 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000580 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000581 show_header: Whether to display a header before the command output.
582 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000583 retry: If the process exits non-zero, sleep for a brief interval and try
584 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000585
586 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000587
588 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000589 """
Edward Lemur24146be2019-08-01 21:44:52 +0000590 def show_header_if_necessary(needs_header, attempt):
591 """Show the header at most once."""
592 if not needs_header[0]:
593 return
594
595 needs_header[0] = False
596 # Automatically generated header. We only prepend a newline if
597 # always_show_header is false, since it usually indicates there's an
598 # external progress display, and it's better not to clobber it in that case.
599 header = '' if always_show_header else '\n'
600 header += '________ running \'%s\' in \'%s\'' % (
601 ' '.join(args), kwargs.get('cwd', '.'))
602 if attempt:
603 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
604 header += '\n'
605
606 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000607 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
608 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000609 if filter_fn:
610 filter_fn(header)
611
612 def filter_line(command_output, line_start):
613 """Extract the last line from command output and filter it."""
614 if not filter_fn or line_start is None:
615 return
616 command_output.seek(line_start)
617 filter_fn(command_output.read().decode('utf-8'))
618
619 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
620 # byte inputs and sys.stdout.buffer must be used instead.
621 if print_stdout:
622 sys.stdout.flush()
623 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
624 else:
625 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000626
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000627 sleep_interval = RETRY_INITIAL_SLEEP
628 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000629
630 # Store the output of the command regardless of the value of print_stdout or
631 # filter_fn.
632 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000633 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000634 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
635 # subprocess when filtering its output. This makes the subproc believe
636 # it was launched from a terminal, which will preserve ANSI color codes.
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000637 os_type = GetOperatingSystem()
Milad Fad949c912020-09-18 00:26:08 +0000638 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000639 pipe_reader, pipe_writer = os.openpty()
640 else:
641 pipe_reader, pipe_writer = os.pipe()
642
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000643 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000644 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000645 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000646 # Close the write end of the pipe once we hand it off to the child proc.
647 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000648
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000649 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000650
Edward Lemur24146be2019-08-01 21:44:52 +0000651 # Passed as a list for "by ref" semantics.
652 needs_header = [show_header]
653 if always_show_header:
654 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000655
656 # Also, we need to forward stdout to prevent weird re-ordering of output.
657 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700658 # normally buffering is done for each line, but if the process requests
659 # input, no end-of-line character is output after the prompt and it would
660 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000661 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000662 line_start = None
663 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000664 try:
665 in_byte = os.read(pipe_reader, 1)
666 except (IOError, OSError) as e:
667 if e.errno == errno.EIO:
668 # An errno.EIO means EOF?
669 in_byte = None
670 else:
671 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000672 is_newline = in_byte in (b'\n', b'\r')
673 if not in_byte:
674 break
675
676 show_header_if_necessary(needs_header, attempt)
677
678 if is_newline:
679 filter_line(command_output, line_start)
680 line_start = None
681 elif line_start is None:
682 line_start = command_output.tell()
683
684 stdout_write(in_byte)
685 command_output.write(in_byte)
686
687 # Flush the rest of buffered output.
688 sys.stdout.flush()
689 if line_start is not None:
690 filter_line(command_output, line_start)
691
Ben Pastened410c662020-08-26 17:07:03 +0000692 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000693 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000694
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000695 # Don't put this in a 'finally,' since the child may still run if we get
696 # an exception.
697 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000698
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000699 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000700 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000701 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000702
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000703 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000704 return command_output.getvalue()
705
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000706 if not retry:
707 break
Edward Lemur24146be2019-08-01 21:44:52 +0000708
Raul Tambreb946b232019-03-26 14:48:46 +0000709 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
710 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000711 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000712 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000713 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000714
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000715 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000716 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000717
718
agable@chromium.org5a306a22014-02-24 22:13:59 +0000719class GitFilter(object):
720 """A filter_fn implementation for quieting down git output messages.
721
722 Allows a custom function to skip certain lines (predicate), and will throttle
723 the output of percentage completed lines to only output every X seconds.
724 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000725 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000726
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000727 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000728 """
729 Args:
730 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
731 XX% complete messages) to only be printed at least |time_throttle|
732 seconds apart.
733 predicate (f(line)): An optional function which is invoked for every line.
734 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000735 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000736 """
Edward Lemur24146be2019-08-01 21:44:52 +0000737 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000738 self.last_time = 0
739 self.time_throttle = time_throttle
740 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000741 self.out_fh = out_fh or sys.stdout
742 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000743
744 def __call__(self, line):
745 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000746 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000747 if esc > -1:
748 line = line[:esc]
749 if self.predicate and not self.predicate(line):
750 return
751 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000752 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000753 if match:
754 if match.group(1) != self.progress_prefix:
755 self.progress_prefix = match.group(1)
756 elif now - self.last_time < self.time_throttle:
757 return
758 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000759 if not self.first_line:
760 self.out_fh.write('[%s] ' % Elapsed())
761 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000762 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000763
764
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000765def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000766 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000767
rcui@google.com13595ff2011-10-13 01:25:07 +0000768 Returns nearest upper-level directory with the passed in file.
769 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000770 if not path:
771 path = os.getcwd()
772 path = os.path.realpath(path)
773 while True:
774 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000775 if os.path.exists(file_path):
776 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000777 (new_path, _) = os.path.split(path)
778 if new_path == path:
779 return None
780 path = new_path
781
782
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000783def GetOperatingSystem():
784 """Returns 'mac', 'win', 'linux', or the name of the current platform."""
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000785 if sys.platform.startswith(('cygwin', 'win')):
786 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000787
788 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000789 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000790
791 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000792 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000793
794 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000795 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000796
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000797 try:
798 return os.uname().sysname.lower()
799 except AttributeError:
800 return sys.platform
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000801
802
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000803def GetGClientRootAndEntries(path=None):
804 """Returns the gclient root and the dict of entries."""
805 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000806 root = FindFileUpwards(config_file, path)
807 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000808 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000809 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000810 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000811 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000812 with open(config_path) as config:
813 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000814 config_dir = os.path.dirname(config_path)
815 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000816
817
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000818def lockedmethod(method):
819 """Method decorator that holds self.lock for the duration of the call."""
820 def inner(self, *args, **kwargs):
821 try:
822 try:
823 self.lock.acquire()
824 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000825 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000826 raise
827 return method(self, *args, **kwargs)
828 finally:
829 self.lock.release()
830 return inner
831
832
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000833class WorkItem(object):
834 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000835 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
836 # As a workaround, use a single lock. Yep you read it right. Single lock for
837 # all the 100 objects.
838 lock = threading.Lock()
839
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000840 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000841 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000842 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000843 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000844 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700845 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000846
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000847 def run(self, work_queue):
848 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000849 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000850
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000851 @property
852 def name(self):
853 return self._name
854
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000855
856class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000857 """Runs a set of WorkItem that have interdependencies and were WorkItem are
858 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000859
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200860 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000861 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000862
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000864 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000865 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000866 """jobs specifies the number of concurrent tasks to allow. progress is a
867 Progress instance."""
868 # Set when a thread is done or a new item is enqueued.
869 self.ready_cond = threading.Condition()
870 # Maximum number of concurrent tasks.
871 self.jobs = jobs
872 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000873 self.queued = []
874 # List of strings representing each Dependency.name that was run.
875 self.ran = []
876 # List of items currently running.
877 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000878 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000879 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000880 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000881 self.progress = progress
882 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000883 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000884
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000885 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000886 self.verbose = verbose
887 self.last_join = None
888 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000889
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000890 def enqueue(self, d):
891 """Enqueue one Dependency to be executed later once its requirements are
892 satisfied.
893 """
894 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000895 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000896 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000897 self.queued.append(d)
898 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000899 if self.jobs == 1:
900 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000901 logging.debug('enqueued(%s)' % d.name)
902 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000903 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000904 self.progress.update(0)
905 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000906 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000907 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000908
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000909 def out_cb(self, _):
910 self.last_subproc_output = datetime.datetime.now()
911 return True
912
913 @staticmethod
914 def format_task_output(task, comment=''):
915 if comment:
916 comment = ' (%s)' % comment
917 if task.start and task.finish:
918 elapsed = ' (Elapsed: %s)' % (
919 str(task.finish - task.start).partition('.')[0])
920 else:
921 elapsed = ''
922 return """
923%s%s%s
924----------------------------------------
925%s
926----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000927 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000928
hinoka885e5b12016-06-08 14:40:09 -0700929 def _is_conflict(self, job):
930 """Checks to see if a job will conflict with another running job."""
931 for running_job in self.running:
932 for used_resource in running_job.item.resources:
933 logging.debug('Checking resource %s' % used_resource)
934 if used_resource in job.resources:
935 return True
936 return False
937
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000938 def flush(self, *args, **kwargs):
939 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000940 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000941 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000942 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000943 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000944 while True:
945 # Check for task to run first, then wait.
946 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000947 if not self.exceptions.empty():
948 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000949 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000950 self._flush_terminated_threads()
951 if (not self.queued and not self.running or
952 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000953 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000954 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000955
956 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000957 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000958 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000959 if (self.ignore_requirements or
960 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700961 if not self._is_conflict(self.queued[i]):
962 # Start one work item: all its requirements are satisfied.
963 self._run_one_task(self.queued.pop(i), args, kwargs)
964 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000965 else:
966 # Couldn't find an item that could run. Break out the outher loop.
967 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000968
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000969 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000970 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000971 break
972 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000973 try:
974 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000975 # If we haven't printed to terminal for a while, but we have received
976 # spew from a suprocess, let the user know we're still progressing.
977 now = datetime.datetime.now()
978 if (now - self.last_join > datetime.timedelta(seconds=60) and
979 self.last_subproc_output > self.last_join):
980 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000981 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000982 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000984 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000985 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000986 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000987 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000988 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000989 except KeyboardInterrupt:
990 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000991 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000992 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000993 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
994 self.ran), len(self.running)),
995 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000996 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000997 print(
998 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
999 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001000 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +00001001 print(
1002 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +00001003 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001004 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001005 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001006 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +00001007
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001008 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +00001009 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001010 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001011 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001012 # To get back the stack location correctly, the raise a, b, c form must be
1013 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001014 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +00001015 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
1016 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001017 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001018 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001019
maruel@chromium.org3742c842010-09-09 19:27:14 +00001020 def _flush_terminated_threads(self):
1021 """Flush threads that have terminated."""
1022 running = self.running
1023 self.running = []
1024 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +00001025 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +00001026 self.running.append(t)
1027 else:
1028 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001029 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +00001030 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001031 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +00001032 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +00001033 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001034 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001035 if t.item.name in self.ran:
1036 raise Error(
1037 'gclient is confused, "%s" is already in "%s"' % (
1038 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001039 if not t.item.name in self.ran:
1040 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001041
1042 def _run_one_task(self, task_item, args, kwargs):
1043 if self.jobs > 1:
1044 # Start the thread.
1045 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001046 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001047 self.running.append(new_thread)
1048 new_thread.start()
1049 else:
1050 # Run the 'thread' inside the main thread. Don't try to catch any
1051 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001052 try:
1053 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001054 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001055 task_item.run(*args, **kwargs)
1056 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001057 print(
1058 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001059 self.ran.append(task_item.name)
1060 if self.verbose:
1061 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001062 print('')
1063 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001064 if self.progress:
1065 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1066 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001067 print(
1068 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001069 raise
1070 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001071 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001072 raise
1073
maruel@chromium.org3742c842010-09-09 19:27:14 +00001074
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001075 class _Worker(threading.Thread):
1076 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001077 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001078 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001079 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001080 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001081 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001082 self.args = args
1083 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001084 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001085
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001086 def run(self):
1087 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001088 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001089 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001090 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001091 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001092 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001093 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001094 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001095 print(
1096 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001097 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001098 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001099 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001100 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001101 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001102 except Exception:
1103 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001104 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001105 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001106 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001107 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001108 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001109 work_queue.ready_cond.acquire()
1110 try:
1111 work_queue.ready_cond.notifyAll()
1112 finally:
1113 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001114
1115
agable92bec4f2016-08-24 09:27:27 -07001116def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001117 """Returns the most plausible editor to use.
1118
1119 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001120 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001121 - core.editor git configuration variable (if supplied by git-cl)
1122 - VISUAL environment variable
1123 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001124 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001125
1126 In the case of git-cl, this matches git's behaviour, except that it does not
1127 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001128 """
agable92bec4f2016-08-24 09:27:27 -07001129 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001130 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001131 editor = os.environ.get('VISUAL')
1132 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001133 editor = os.environ.get('EDITOR')
1134 if not editor:
1135 if sys.platform.startswith('win'):
1136 editor = 'notepad'
1137 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001138 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001139 return editor
1140
1141
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001142def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001143 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001144 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001145 # Make sure CRLF is handled properly by requiring none.
1146 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001147 print(
1148 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001149 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001150 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001151 content = re.sub('\r?\n', '\n', content)
1152 # Some editors complain when the file doesn't end in \n.
1153 if not content.endswith('\n'):
1154 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001155 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001156 fileobj.close()
1157
1158 try:
agable92bec4f2016-08-24 09:27:27 -07001159 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001160 if not editor:
1161 return None
1162 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001163 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1164 # Msysgit requires the usage of 'env' to be present.
1165 cmd = 'env ' + cmd
1166 try:
1167 # shell=True to allow the shell to handle all forms of quotes in
1168 # $EDITOR.
1169 subprocess2.check_call(cmd, shell=True)
1170 except subprocess2.CalledProcessError:
1171 return None
1172 return FileRead(filename)
1173 finally:
1174 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001175
1176
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001177def UpgradeToHttps(url):
1178 """Upgrades random urls to https://.
1179
1180 Do not touch unknown urls like ssh:// or git://.
1181 Do not touch http:// urls with a port number,
1182 Fixes invalid GAE url.
1183 """
1184 if not url:
1185 return url
1186 if not re.match(r'[a-z\-]+\://.*', url):
1187 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1188 # relative url and will use http:///foo. Note that it defaults to http://
1189 # for compatibility with naked url like "localhost:8080".
1190 url = 'http://%s' % url
1191 parsed = list(urlparse.urlparse(url))
1192 # Do not automatically upgrade http to https if a port number is provided.
1193 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1194 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001195 return urlparse.urlunparse(parsed)
1196
1197
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001198def ParseCodereviewSettingsContent(content):
1199 """Process a codereview.settings file properly."""
1200 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1201 try:
1202 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1203 except ValueError:
1204 raise Error(
1205 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001206 def fix_url(key):
1207 if keyvals.get(key):
1208 keyvals[key] = UpgradeToHttps(keyvals[key])
1209 fix_url('CODE_REVIEW_SERVER')
1210 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001211 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001212
1213
1214def NumLocalCpus():
1215 """Returns the number of processors.
1216
dnj@chromium.org530523b2015-01-07 19:54:57 +00001217 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1218 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1219 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001220 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001221 # Surround the entire thing in try/except; no failure here should stop gclient
1222 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001223 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001224 # Use multiprocessing to get CPU count. This may raise
1225 # NotImplementedError.
1226 try:
1227 import multiprocessing
1228 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001229 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001230 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001231 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001232 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1233 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1234
1235 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1236 if 'NUMBER_OF_PROCESSORS' in os.environ:
1237 return int(os.environ['NUMBER_OF_PROCESSORS'])
1238 except Exception as e:
1239 logging.exception("Exception raised while probing CPU count: %s", e)
1240
1241 logging.debug('Failed to get CPU count. Defaulting to 1.')
1242 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001243
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001244
szager@chromium.orgfc616382014-03-18 20:32:04 +00001245def DefaultDeltaBaseCacheLimit():
1246 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1247
1248 The primary constraint is the address space of virtual memory. The cache
1249 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1250 parameter is set too high.
1251 """
1252 if platform.architecture()[0].startswith('64'):
1253 return '2g'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001254
1255 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001256
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001257
szager@chromium.orgff113292014-03-25 06:02:08 +00001258def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001259 """Return reasonable default values for configuring git-index-pack.
1260
1261 Experiments suggest that higher values for pack.threads don't improve
1262 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001263 cache_limit = DefaultDeltaBaseCacheLimit()
1264 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001265 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001266 result.extend(['-c', 'pack.threads=1'])
1267 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001268
1269
1270def FindExecutable(executable):
1271 """This mimics the "which" utility."""
1272 path_folders = os.environ.get('PATH').split(os.pathsep)
1273
1274 for path_folder in path_folders:
1275 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001276 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001277 target = os.path.abspath(os.path.expanduser(target))
1278 if os.path.isfile(target) and os.access(target, os.X_OK):
1279 return target
1280 if sys.platform.startswith('win'):
1281 for suffix in ('.bat', '.cmd', '.exe'):
1282 alt_target = target + suffix
1283 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1284 return alt_target
1285 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001286
1287
1288def freeze(obj):
1289 """Takes a generic object ``obj``, and returns an immutable version of it.
1290
1291 Supported types:
1292 * dict / OrderedDict -> FrozenDict
1293 * list -> tuple
1294 * set -> frozenset
1295 * any object with a working __hash__ implementation (assumes that hashable
1296 means immutable)
1297
1298 Will raise TypeError if you pass an object which is not hashable.
1299 """
Raul Tambre6693d092020-02-19 20:36:45 +00001300 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001301 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001302
1303 if isinstance(obj, (list, tuple)):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001304 return tuple(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001305
1306 if isinstance(obj, set):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001307 return frozenset(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001308
1309 hash(obj)
1310 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001311
1312
Raul Tambre6693d092020-02-19 20:36:45 +00001313class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001314 """An immutable OrderedDict.
1315
1316 Modified From: http://stackoverflow.com/a/2704866
1317 """
1318 def __init__(self, *args, **kwargs):
1319 self._d = collections.OrderedDict(*args, **kwargs)
1320
1321 # Calculate the hash immediately so that we know all the items are
1322 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001323 self._hash = functools.reduce(
1324 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001325
1326 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001327 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001328 return NotImplemented
1329 if self is other:
1330 return True
1331 if len(self) != len(other):
1332 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001333 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001334 if k not in other or other[k] != v:
1335 return False
1336 return True
1337
1338 def __iter__(self):
1339 return iter(self._d)
1340
1341 def __len__(self):
1342 return len(self._d)
1343
1344 def __getitem__(self, key):
1345 return self._d[key]
1346
1347 def __hash__(self):
1348 return self._hash
1349
1350 def __repr__(self):
1351 return 'FrozenDict(%r)' % (self._d.items(),)