blob: e017f996916d892646a7fe6adb822b1c0056b46a [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.org5aeb7dd2009-11-17 18:09:01 +00004"""Generic utils."""
5
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00006import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02007import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00008import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00009import datetime
Ben Pastened410c662020-08-26 17:07:03 +000010import errno
Raul Tambreb946b232019-03-26 14:48:46 +000011import functools
12import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000013import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020014import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000015import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000016import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000017import platform
Gavin Mak65c49b12023-08-24 18:06:42 +000018import queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000019import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000020import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000021import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000023import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000024import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000025import time
Gavin Mak65c49b12023-08-24 18:06:42 +000026import urllib.parse
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000027
Gavin Mak65c49b12023-08-24 18:06:42 +000028import subprocess2
Raul Tambreb946b232019-03-26 14:48:46 +000029
Josip Sokcevic38d669f2022-09-02 18:08:57 +000030# Git wrapper retries on a transient error, and some callees do retries too,
31# such as GitWrapper.update (doing clone). One retry attempt should be
32# sufficient to help with any transient errors at this level.
33RETRY_MAX = 1
34RETRY_INITIAL_SLEEP = 2 # in seconds
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000035START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000036
borenet@google.com6a9b1682014-03-24 18:35:23 +000037_WARNINGS = []
38
szager@chromium.orgff113292014-03-25 06:02:08 +000039# These repos are known to cause OOM errors on 32-bit platforms, due the the
40# very large objects they contain. It is not safe to use threaded index-pack
41# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000042THREADED_INDEX_PACK_BLOCKLIST = [
Mike Frysinger124bb8e2023-09-06 05:48:55 +000043 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
szager@chromium.orgff113292014-03-25 06:02:08 +000044]
45
Mike Frysinger124bb8e2023-09-06 05:48:55 +000046
Gavin Mak65c49b12023-08-24 18:06:42 +000047def reraise(typ, value, tb=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000048 """To support rethrowing exceptions with tracebacks."""
49 if value is None:
50 value = typ()
51 if value.__traceback__ is not tb:
52 raise value.with_traceback(tb)
53 raise value
Raul Tambreb946b232019-03-26 14:48:46 +000054
szager@chromium.orgff113292014-03-25 06:02:08 +000055
maruel@chromium.org66c83e62010-09-07 14:18:45 +000056class Error(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000057 """gclient exception class."""
58 def __init__(self, msg, *args, **kwargs):
59 index = getattr(threading.currentThread(), 'index', 0)
60 if index:
61 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
62 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000063
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000064
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000065def Elapsed(until=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066 if until is None:
67 until = datetime.datetime.now()
68 return str(until - START).partition('.')[0]
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000069
70
borenet@google.com6a9b1682014-03-24 18:35:23 +000071def PrintWarnings():
Mike Frysinger124bb8e2023-09-06 05:48:55 +000072 """Prints any accumulated warnings."""
73 if _WARNINGS:
74 print('\n\nWarnings:', file=sys.stderr)
75 for warning in _WARNINGS:
76 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000077
78
79def AddWarning(msg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000080 """Adds the given warning message to the list of accumulated warnings."""
81 _WARNINGS.append(msg)
borenet@google.com6a9b1682014-03-24 18:35:23 +000082
83
Joanna Wang66286612022-06-30 19:59:13 +000084def FuzzyMatchRepo(repo, candidates):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000085 # type: (str, Union[Collection[str], Mapping[str, Any]]) -> Optional[str]
86 """Attempts to find a representation of repo in the candidates.
Joanna Wang66286612022-06-30 19:59:13 +000087
88 Args:
89 repo: a string representation of a repo in the form of a url or the
90 name and path of the solution it represents.
91 candidates: The candidates to look through which may contain `repo` in
92 in any of the forms mentioned above.
93 Returns:
94 The matching string, if any, which may be in a different form from `repo`.
95 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +000096 if repo in candidates:
97 return repo
98 if repo.endswith('.git') and repo[:-len('.git')] in candidates:
99 return repo[:-len('.git')]
100 if repo + '.git' in candidates:
101 return repo + '.git'
102 return None
Joanna Wang66286612022-06-30 19:59:13 +0000103
104
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000105def SplitUrlRevision(url):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000106 """Splits url and returns a two-tuple: url, rev"""
107 if url.startswith('ssh:'):
108 # Make sure ssh://user-name@example.com/~/test.git@stable works
109 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
110 components = re.search(regex, url).groups()
111 else:
112 components = url.rsplit('@', 1)
113 if re.match(r'^\w+\@', url) and '@' not in components[0]:
114 components = [url]
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000115
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000116 if len(components) == 1:
117 components += [None]
118 return tuple(components)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000119
120
Joanna Wang1a977bd2022-06-02 21:51:17 +0000121def ExtractRefName(remote, full_refs_str):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000122 """Returns the ref name if full_refs_str is a valid ref."""
123 result = re.compile(
124 r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
125 remote).match(full_refs_str)
126 if result:
127 return result.group('ref_name')
128 return None
Joanna Wang1a977bd2022-06-02 21:51:17 +0000129
130
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000131def IsGitSha(revision):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000132 """Returns true if the given string is a valid hex-encoded sha"""
133 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000134
135
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200136def IsFullGitSha(revision):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000137 """Returns true if the given string is a valid hex-encoded full sha"""
138 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200139
140
floitsch@google.comeaab7842011-04-28 09:07:58 +0000141def IsDateRevision(revision):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000142 """Returns true if the given revision is of the form "{ ... }"."""
143 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
floitsch@google.comeaab7842011-04-28 09:07:58 +0000144
145
146def MakeDateRevision(date):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 """Returns a revision representing the latest revision before the given
floitsch@google.comeaab7842011-04-28 09:07:58 +0000148 date."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000149 return "{" + date + "}"
floitsch@google.comeaab7842011-04-28 09:07:58 +0000150
151
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000152def SyntaxErrorToError(filename, e):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000153 """Raises a gclient_utils.Error exception with the human readable message"""
154 try:
155 # Try to construct a human readable error message
156 if filename:
157 error_message = 'There is a syntax error in %s\n' % filename
158 else:
159 error_message = 'There is a syntax error\n'
160 error_message += 'Line #%s, character %s: "%s"' % (
161 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
162 except:
163 # Something went wrong, re-raise the original exception
164 raise e
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000165 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000166 raise Error(error_message)
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000167
168
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000169class PrintableObject(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 def __str__(self):
171 output = ''
172 for i in dir(self):
173 if i.startswith('__'):
174 continue
175 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
176 return output
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000177
178
Edward Lesmesae3586b2020-03-23 21:21:14 +0000179def AskForData(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 # Try to load the readline module, so that "elaborate line editing" features
181 # such as backspace work for `raw_input` / `input`.
182 try:
183 import readline
184 except ImportError:
185 # The readline module does not exist in all Python distributions, e.g.
186 # on Windows. Fall back to simple input handling.
187 pass
Christian Flache6855432021-12-01 08:10:05 +0000188
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 # Use this so that it can be mocked in tests.
190 try:
191 return input(message)
192 except KeyboardInterrupt:
193 # Hide the exception.
194 sys.exit(1)
Edward Lesmesae3586b2020-03-23 21:21:14 +0000195
196
Edward Lemur419c92f2019-10-25 22:17:49 +0000197def FileRead(filename, mode='rbU'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000198 # mode is ignored now; we always return unicode strings.
199 with open(filename, mode='rb') as f:
200 s = f.read()
201 try:
202 return s.decode('utf-8', 'replace')
203 except (UnicodeDecodeError, AttributeError):
204 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000205
206
Edward Lemur1773f372020-02-22 00:27:14 +0000207def FileWrite(filename, content, mode='w', encoding='utf-8'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000208 with codecs.open(filename, mode=mode, encoding=encoding) as f:
209 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000210
211
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000212@contextlib.contextmanager
213def temporary_directory(**kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000214 tdir = tempfile.mkdtemp(**kwargs)
215 try:
216 yield tdir
217 finally:
218 if tdir:
219 rmtree(tdir)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000220
221
Edward Lemur1773f372020-02-22 00:27:14 +0000222@contextlib.contextmanager
223def temporary_file():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000224 """Creates a temporary file.
Edward Lemur1773f372020-02-22 00:27:14 +0000225
226 On Windows, a file must be closed before it can be opened again. This function
227 allows to write something like:
228
229 with gclient_utils.temporary_file() as tmp:
230 gclient_utils.FileWrite(tmp, foo)
231 useful_stuff(tmp)
232
233 Instead of something like:
234
235 with tempfile.NamedTemporaryFile(delete=False) as tmp:
236 tmp.write(foo)
237 tmp.close()
238 try:
239 useful_stuff(tmp)
240 finally:
241 os.remove(tmp.name)
242 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000243 handle, name = tempfile.mkstemp()
244 os.close(handle)
245 try:
246 yield name
247 finally:
248 os.remove(name)
Edward Lemur1773f372020-02-22 00:27:14 +0000249
250
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000251def safe_rename(old, new):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 """Renames a file reliably.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000253
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000254 Sometimes os.rename does not work because a dying git process keeps a handle
255 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000256 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000257 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000258 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000259 # roughly 10s
260 retries = 100
261 for i in range(retries):
262 try:
263 os.rename(old, new)
264 break
265 except OSError:
266 if i == (retries - 1):
267 # Give up.
268 raise
269 # retry
270 logging.debug("Renaming failed from %s to %s. Retrying ..." %
271 (old, new))
272 time.sleep(0.1)
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000273
274
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000275def rm_file_or_tree(path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000276 if os.path.isfile(path) or os.path.islink(path):
277 os.remove(path)
278 else:
279 rmtree(path)
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000280
281
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000282def rmtree(path):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000284
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000285 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
287 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700288 are read-only. We need to be able to force the files to be writable (i.e.,
289 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000290
291 Even with all this, Windows still sometimes fails to delete a file, citing
292 a permission error (maybe something to do with antivirus scans or disk
293 indexing). The best suggestion any of the user forums had was to wait a
294 bit and try again, so we do that too. It's hand-waving, but sometimes it
295 works. :/
296
297 On POSIX systems, things are a little bit simpler. The modes of the files
298 to be deleted doesn't matter, only the modes of the directories containing
299 them are significant. As the directory tree is traversed, each directory
300 has its mode set appropriately before descending into it. This should
301 result in the entire tree being removed, with the possible exception of
302 *path itself, because nothing attempts to change the mode of its parent.
303 Doing so would be hazardous, as it's not a directory slated for removal.
304 In the ordinary case, this is not a problem: for our purposes, the user
305 will never lack write permission on *path's parent.
306 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000307 if not os.path.exists(path):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000308 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000309
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000310 if os.path.islink(path) or not os.path.isdir(path):
311 raise Error('Called rmtree(%s) in non-directory' % path)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000312
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000313 if sys.platform == 'win32':
314 # Give up and use cmd.exe's rd command.
315 path = os.path.normcase(path)
316 for _ in range(3):
317 exitcode = subprocess.call(
318 ['cmd.exe', '/c', 'rd', '/q', '/s', path])
319 if exitcode == 0:
320 return
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000321
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000322 print('rd exited with code %d' % exitcode, file=sys.stderr)
323 time.sleep(3)
324 raise Exception('Failed to remove path %s' % path)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000325
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000326 # On POSIX systems, we need the x-bit set on the directory to access it,
327 # the r-bit to see its contents, and the w-bit to remove files from it.
328 # The actual modes of the files within the directory is irrelevant.
329 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000330
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000331 def remove(func, subpath):
332 func(subpath)
333
334 for fn in os.listdir(path):
335 # If fullpath is a symbolic link that points to a directory, isdir will
336 # be True, but we don't want to descend into that as a directory, we
337 # just want to remove the link. Check islink and treat links as
338 # ordinary files would be treated regardless of what they reference.
339 fullpath = os.path.join(path, fn)
340 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
341 remove(os.remove, fullpath)
342 else:
343 # Recurse.
344 rmtree(fullpath)
345
346 remove(os.rmdir, path)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000347
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000348
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000349def safe_makedirs(tree):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000350 """Creates the directory in a safe manner.
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000351
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000352 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000353 exception and pass on.
354 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000355 count = 0
356 while not os.path.exists(tree):
357 count += 1
358 try:
359 os.makedirs(tree)
360 except OSError as e:
361 # 17 POSIX, 183 Windows
362 if e.errno not in (17, 183):
363 raise
364 if count > 40:
365 # Give up.
366 raise
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000367
368
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000369def CommandToStr(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000370 """Converts an arg list into a shell escaped string."""
371 return ' '.join(pipes.quote(arg) for arg in args)
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000372
373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374class Wrapper(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000375 """Wraps an object, acting as a transparent proxy for all properties by
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000376 default.
377 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000378 def __init__(self, wrapped):
379 self._wrapped = wrapped
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000380
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000381 def __getattr__(self, name):
382 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000383
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000384
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000385class AutoFlush(Wrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000386 """Creates a file object clone to automatically flush after N seconds."""
387 def __init__(self, wrapped, delay):
388 super(AutoFlush, self).__init__(wrapped)
389 if not hasattr(self, 'lock'):
390 self.lock = threading.Lock()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000391 self.__last_flushed_at = time.time()
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000392 self.delay = delay
393
394 @property
395 def autoflush(self):
396 return self
397
398 def write(self, out, *args, **kwargs):
399 self._wrapped.write(out, *args, **kwargs)
400 should_flush = False
401 self.lock.acquire()
402 try:
403 if self.delay and (time.time() -
404 self.__last_flushed_at) > self.delay:
405 should_flush = True
406 self.__last_flushed_at = time.time()
407 finally:
408 self.lock.release()
409 if should_flush:
410 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000411
412
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000413class Annotated(Wrapper):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000414 """Creates a file object clone to automatically prepends every line in
415 worker threads with a NN> prefix.
416 """
417 def __init__(self, wrapped, include_zero=False):
418 super(Annotated, self).__init__(wrapped)
419 if not hasattr(self, 'lock'):
420 self.lock = threading.Lock()
421 self.__output_buffers = {}
422 self.__include_zero = include_zero
423 self._wrapped_write = getattr(self._wrapped, 'buffer',
424 self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000425
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000426 @property
427 def annotated(self):
428 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000429
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000430 def write(self, out):
431 # Store as bytes to ensure Unicode characters get output correctly.
432 if not isinstance(out, bytes):
433 out = out.encode('utf-8')
Edward Lemurcb1eb482019-10-09 18:03:14 +0000434
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000435 index = getattr(threading.currentThread(), 'index', 0)
436 if not index and not self.__include_zero:
437 # Unindexed threads aren't buffered.
438 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000439
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000440 self.lock.acquire()
441 try:
442 # Use a dummy array to hold the string so the code can be lockless.
443 # Strings are immutable, requiring to keep a lock for the whole
444 # dictionary otherwise. Using an array is faster than using a dummy
445 # object.
446 if not index in self.__output_buffers:
447 obj = self.__output_buffers[index] = [b'']
448 else:
449 obj = self.__output_buffers[index]
450 finally:
451 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000453 # Continue lockless.
454 obj[0] += out
455 while True:
456 cr_loc = obj[0].find(b'\r')
457 lf_loc = obj[0].find(b'\n')
458 if cr_loc == lf_loc == -1:
459 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000460
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000461 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
462 line, remaining = obj[0].split(b'\n', 1)
463 if line:
464 self._wrapped_write(b'%d>%s\n' % (index, line))
465 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
466 line, remaining = obj[0].split(b'\r', 1)
467 if line:
468 self._wrapped_write(b'%d>%s\r' % (index, line))
469 obj[0] = remaining
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000470
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000471 def flush(self):
472 """Flush buffered output."""
473 orphans = []
474 self.lock.acquire()
475 try:
476 # Detect threads no longer existing.
477 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
478 indexes = filter(None, indexes)
479 for index in self.__output_buffers:
480 if not index in indexes:
481 orphans.append((index, self.__output_buffers[index][0]))
482 for orphan in orphans:
483 del self.__output_buffers[orphan[0]]
484 finally:
485 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000486
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000487 # Don't keep the lock while writing. Will append \n when it shouldn't.
488 for orphan in orphans:
489 if orphan[1]:
490 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
491 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000492
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000493
494def MakeFileAutoFlush(fileobj, delay=10):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000495 autoflush = getattr(fileobj, 'autoflush', None)
496 if autoflush:
497 autoflush.delay = delay
498 return fileobj
499 return AutoFlush(fileobj, delay)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000500
501
502def MakeFileAnnotated(fileobj, include_zero=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000503 if getattr(fileobj, 'annotated', None):
504 return fileobj
505 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000506
507
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000508GCLIENT_CHILDREN = []
509GCLIENT_CHILDREN_LOCK = threading.Lock()
510
511
512class GClientChildren(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000513 @staticmethod
514 def add(popen_obj):
515 with GCLIENT_CHILDREN_LOCK:
516 GCLIENT_CHILDREN.append(popen_obj)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000517
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000518 @staticmethod
519 def remove(popen_obj):
520 with GCLIENT_CHILDREN_LOCK:
521 GCLIENT_CHILDREN.remove(popen_obj)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000522
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000523 @staticmethod
524 def _attemptToKillChildren():
525 global GCLIENT_CHILDREN
526 with GCLIENT_CHILDREN_LOCK:
527 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000528
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000529 for zombie in zombies:
530 try:
531 zombie.kill()
532 except OSError:
533 pass
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000534
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000535 with GCLIENT_CHILDREN_LOCK:
536 GCLIENT_CHILDREN = [
537 k for k in GCLIENT_CHILDREN if k.poll() is not None
538 ]
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000539
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000540 @staticmethod
541 def _areZombies():
542 with GCLIENT_CHILDREN_LOCK:
543 return bool(GCLIENT_CHILDREN)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000544
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000545 @staticmethod
546 def KillAllRemainingChildren():
547 GClientChildren._attemptToKillChildren()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000548
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000549 if GClientChildren._areZombies():
550 time.sleep(0.5)
551 GClientChildren._attemptToKillChildren()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000552
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000553 with GCLIENT_CHILDREN_LOCK:
554 if GCLIENT_CHILDREN:
555 print('Could not kill the following subprocesses:',
556 file=sys.stderr)
557 for zombie in GCLIENT_CHILDREN:
558 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000559
560
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000561def CheckCallAndFilter(args,
562 print_stdout=False,
563 filter_fn=None,
564 show_header=False,
565 always_show_header=False,
566 retry=False,
Edward Lemur24146be2019-08-01 21:44:52 +0000567 **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 """Runs a command and calls back a filter function if needed.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000569
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000570 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000571 print_stdout: If True, the command's stdout is forwarded to stdout.
572 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000573 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000574 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000575 show_header: Whether to display a header before the command output.
576 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000577 retry: If the process exits non-zero, sleep for a brief interval and try
578 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000579
580 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000581
582 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000583 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000584 def show_header_if_necessary(needs_header, attempt):
585 """Show the header at most once."""
586 if not needs_header[0]:
587 return
Edward Lemur24146be2019-08-01 21:44:52 +0000588
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000589 needs_header[0] = False
590 # Automatically generated header. We only prepend a newline if
591 # always_show_header is false, since it usually indicates there's an
592 # external progress display, and it's better not to clobber it in that
593 # case.
594 header = '' if always_show_header else '\n'
595 header += '________ running \'%s\' in \'%s\'' % (' '.join(args),
596 kwargs.get('cwd', '.'))
597 if attempt:
598 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
599 header += '\n'
Edward Lemur24146be2019-08-01 21:44:52 +0000600
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000601 if print_stdout:
602 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
603 stdout_write(header.encode())
604 if filter_fn:
605 filter_fn(header)
606
607 def filter_line(command_output, line_start):
608 """Extract the last line from command output and filter it."""
609 if not filter_fn or line_start is None:
610 return
611 command_output.seek(line_start)
612 filter_fn(command_output.read().decode('utf-8'))
613
614 # Initialize stdout writer if needed. On Python 3, sys.stdout does not
615 # accept byte inputs and sys.stdout.buffer must be used instead.
Edward Lemur24146be2019-08-01 21:44:52 +0000616 if print_stdout:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000617 sys.stdout.flush()
618 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
Ben Pastened410c662020-08-26 17:07:03 +0000619 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000620 stdout_write = lambda _: None
Ben Pastened410c662020-08-26 17:07:03 +0000621
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000622 sleep_interval = RETRY_INITIAL_SLEEP
623 run_cwd = kwargs.get('cwd', os.getcwd())
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000624
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000625 # Store the output of the command regardless of the value of print_stdout or
626 # filter_fn.
Josip Sokcevic740825e2021-05-12 18:28:34 +0000627 command_output = io.BytesIO()
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000628 for attempt in range(RETRY_MAX + 1):
629 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
630 # subprocess when filtering its output. This makes the subproc believe
631 # it was launched from a terminal, which will preserve ANSI color codes.
632 os_type = GetOperatingSystem()
633 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
634 pipe_reader, pipe_writer = os.openpty()
635 else:
636 pipe_reader, pipe_writer = os.pipe()
Edward Lemur24146be2019-08-01 21:44:52 +0000637
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000638 kid = subprocess2.Popen(args,
639 bufsize=0,
640 stdout=pipe_writer,
641 stderr=subprocess2.STDOUT,
642 **kwargs)
643 # Close the write end of the pipe once we hand it off to the child proc.
644 os.close(pipe_writer)
645
646 GClientChildren.add(kid)
647
648 # 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)
652
653 # Also, we need to forward stdout to prevent weird re-ordering of
654 # output. This has to be done on a per byte basis to make sure it is not
655 # buffered: normally buffering is done for each line, but if the process
656 # requests input, no end-of-line character is output after the prompt
657 # and it would not show up.
658 try:
659 line_start = None
660 while True:
661 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
669 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
689 os.close(pipe_reader)
690 rv = kid.wait()
691
692 # Don't put this in a 'finally,' since the child may still run if we
693 # get an exception.
694 GClientChildren.remove(kid)
695
696 except KeyboardInterrupt:
697 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
698 raise
699
700 if rv == 0:
701 return command_output.getvalue()
702
703 if not retry:
704 break
705
706 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
707 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
708 command_output = io.BytesIO()
709 time.sleep(sleep_interval)
710 sleep_interval *= 2
711
712 raise subprocess2.CalledProcessError(rv, args, kwargs.get('cwd', None),
713 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):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000717 """A filter_fn implementation for quieting down git output messages.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000718
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 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000722 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000723
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000724 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
725 """
agable@chromium.org5a306a22014-02-24 22:13:59 +0000726 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 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 self.first_line = True
735 self.last_time = 0
736 self.time_throttle = time_throttle
737 self.predicate = predicate
738 self.out_fh = out_fh or sys.stdout
739 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000740
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000741 def __call__(self, line):
742 # git uses an escape sequence to clear the line; elide it.
743 esc = line.find(chr(0o33))
744 if esc > -1:
745 line = line[:esc]
746 if self.predicate and not self.predicate(line):
747 return
748 now = time.time()
749 match = self.PERCENT_RE.match(line)
750 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
756 if not self.first_line:
757 self.out_fh.write('[%s] ' % Elapsed())
758 self.first_line = False
759 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):
Mike Frysinger124bb8e2023-09-06 05:48:55 +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 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +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)
772 if os.path.exists(file_path):
773 return path
774 (new_path, _) = os.path.split(path)
775 if new_path == path:
776 return None
777 path = new_path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000778
779
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000780def GetOperatingSystem():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000781 """Returns 'mac', 'win', 'linux', or the name of the current platform."""
782 if sys.platform.startswith(('cygwin', 'win')):
783 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000784
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000785 if sys.platform.startswith('linux'):
786 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000787
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000788 if sys.platform == 'darwin':
789 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000790
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000791 if sys.platform.startswith('aix'):
792 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000793
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000794 try:
795 return os.uname().sysname.lower()
796 except AttributeError:
797 return sys.platform
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000798
799
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000800def GetGClientRootAndEntries(path=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000801 """Returns the gclient root and the dict of entries."""
802 config_file = '.gclient_entries'
803 root = FindFileUpwards(config_file, path)
804 if not root:
805 print("Can't find %s" % config_file)
806 return None
807 config_path = os.path.join(root, config_file)
808 env = {}
809 with open(config_path) as config:
810 exec(config.read(), env)
811 config_dir = os.path.dirname(config_path)
812 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000813
814
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000815def lockedmethod(method):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000816 """Method decorator that holds self.lock for the duration of the call."""
817 def inner(self, *args, **kwargs):
818 try:
819 try:
820 self.lock.acquire()
821 except KeyboardInterrupt:
822 print('Was deadlocked', file=sys.stderr)
823 raise
824 return method(self, *args, **kwargs)
825 finally:
826 self.lock.release()
827
828 return inner
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000829
830
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000831class WorkItem(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000832 """One work item."""
833 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
834 # As a workaround, use a single lock. Yep you read it right. Single lock for
835 # all the 100 objects.
836 lock = threading.Lock()
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000837
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000838 def __init__(self, name):
839 # A unique string representing this work item.
840 self._name = name
841 self.outbuf = io.StringIO()
842 self.start = self.finish = None
843 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000844
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000845 def run(self, work_queue):
846 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000849 @property
850 def name(self):
851 return self._name
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000852
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000853
854class ExecutionQueue(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000855 """Runs a set of WorkItem that have interdependencies and were WorkItem are
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000856 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000857
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200858 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000860
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000861 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000862 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000863 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
864 """jobs specifies the number of concurrent tasks to allow. progress is a
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 Progress instance."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000866 # Set when a thread is done or a new item is enqueued.
867 self.ready_cond = threading.Condition()
868 # Maximum number of concurrent tasks.
869 self.jobs = jobs
870 # List of WorkItem, for gclient, these are Dependency instances.
871 self.queued = []
872 # List of strings representing each Dependency.name that was run.
873 self.ran = []
874 # List of items currently running.
875 self.running = []
876 # Exceptions thrown if any.
877 self.exceptions = queue.Queue()
878 # Progress status
879 self.progress = progress
880 if self.progress:
881 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000882
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000883 self.ignore_requirements = ignore_requirements
884 self.verbose = verbose
885 self.last_join = None
886 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000887
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000888 def enqueue(self, d):
889 """Enqueue one Dependency to be executed later once its requirements are
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000890 satisfied.
891 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000892 assert isinstance(d, WorkItem)
893 self.ready_cond.acquire()
894 try:
895 self.queued.append(d)
896 total = len(self.queued) + len(self.ran) + len(self.running)
897 if self.jobs == 1:
898 total += 1
899 logging.debug('enqueued(%s)' % d.name)
900 if self.progress:
901 self.progress._total = total
902 self.progress.update(0)
903 self.ready_cond.notifyAll()
904 finally:
905 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000906
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000907 def out_cb(self, _):
908 self.last_subproc_output = datetime.datetime.now()
909 return True
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000910
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000911 @staticmethod
912 def format_task_output(task, comment=''):
913 if comment:
914 comment = ' (%s)' % comment
915 if task.start and task.finish:
916 elapsed = ' (Elapsed: %s)' % (str(task.finish -
917 task.start).partition('.')[0])
918 else:
919 elapsed = ''
920 return """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000921%s%s%s
922----------------------------------------
923%s
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000924----------------------------------------""" % (task.name, comment, elapsed,
925 task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000926
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000927 def _is_conflict(self, job):
928 """Checks to see if a job will conflict with another running job."""
929 for running_job in self.running:
930 for used_resource in running_job.item.resources:
931 logging.debug('Checking resource %s' % used_resource)
932 if used_resource in job.resources:
933 return True
934 return False
hinoka885e5b12016-06-08 14:40:09 -0700935
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000936 def flush(self, *args, **kwargs):
937 """Runs all enqueued items until all are executed."""
938 kwargs['work_queue'] = self
939 self.last_subproc_output = self.last_join = datetime.datetime.now()
940 self.ready_cond.acquire()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000941 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000942 while True:
943 # Check for task to run first, then wait.
944 while True:
945 if not self.exceptions.empty():
946 # Systematically flush the queue when an exception
947 # logged.
948 self.queued = []
949 self._flush_terminated_threads()
950 if (not self.queued and not self.running
951 or self.jobs == len(self.running)):
952 logging.debug(
953 'No more worker threads or can\'t queue anything.')
954 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000955
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000956 # Check for new tasks to start.
957 for i in range(len(self.queued)):
958 # Verify its requirements.
959 if (self.ignore_requirements
960 or not (set(self.queued[i].requirements) -
961 set(self.ran))):
962 if not self._is_conflict(self.queued[i]):
963 # Start one work item: all its requirements are
964 # satisfied.
965 self._run_one_task(self.queued.pop(i), args,
966 kwargs)
967 break
968 else:
969 # Couldn't find an item that could run. Break out the
970 # outher loop.
971 break
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000972
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000973 if not self.queued and not self.running:
974 # We're done.
975 break
976 # We need to poll here otherwise Ctrl-C isn't processed.
977 try:
978 self.ready_cond.wait(10)
979 # If we haven't printed to terminal for a while, but we have
980 # received spew from a suprocess, let the user know we're
981 # still progressing.
982 now = datetime.datetime.now()
983 if (now - self.last_join > datetime.timedelta(seconds=60)
984 and self.last_subproc_output > self.last_join):
985 if self.progress:
986 print('')
987 sys.stdout.flush()
988 elapsed = Elapsed()
989 print('[%s] Still working on:' % elapsed)
990 sys.stdout.flush()
991 for task in self.running:
992 print('[%s] %s' % (elapsed, task.item.name))
993 sys.stdout.flush()
994 except KeyboardInterrupt:
995 # Help debugging by printing some information:
996 print(
997 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
998 'Running: %d') %
999 (self.jobs, len(self.queued), ', '.join(
1000 self.ran), len(self.running)),
1001 file=sys.stderr)
1002 for i in self.queued:
1003 print('%s (not started): %s' %
1004 (i.name, ', '.join(i.requirements)),
1005 file=sys.stderr)
1006 for i in self.running:
1007 print(self.format_task_output(i.item, 'interrupted'),
1008 file=sys.stderr)
1009 raise
1010 # Something happened: self.enqueue() or a thread terminated.
1011 # Loop again.
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001012 finally:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001013 self.ready_cond.release()
1014
1015 assert not self.running, 'Now guaranteed to be single-threaded'
1016 if not self.exceptions.empty():
1017 if self.progress:
1018 print('')
1019 # To get back the stack location correctly, the raise a, b, c form
1020 # must be used, passing a tuple as the first argument doesn't work.
1021 e, task = self.exceptions.get()
1022 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
1023 reraise(e[0], e[1], e[2])
1024 elif self.progress:
1025 self.progress.end()
1026
1027 def _flush_terminated_threads(self):
1028 """Flush threads that have terminated."""
1029 running = self.running
1030 self.running = []
1031 for t in running:
1032 if t.is_alive():
1033 self.running.append(t)
1034 else:
1035 t.join()
1036 self.last_join = datetime.datetime.now()
1037 sys.stdout.flush()
1038 if self.verbose:
1039 print(self.format_task_output(t.item))
1040 if self.progress:
1041 self.progress.update(1, t.item.name)
1042 if t.item.name in self.ran:
1043 raise Error('gclient is confused, "%s" is already in "%s"' %
1044 (t.item.name, ', '.join(self.ran)))
1045 if not t.item.name in self.ran:
1046 self.ran.append(t.item.name)
1047
1048 def _run_one_task(self, task_item, args, kwargs):
1049 if self.jobs > 1:
1050 # Start the thread.
1051 index = len(self.ran) + len(self.running) + 1
1052 new_thread = self._Worker(task_item, index, args, kwargs)
1053 self.running.append(new_thread)
1054 new_thread.start()
1055 else:
1056 # Run the 'thread' inside the main thread. Don't try to catch any
1057 # exception.
1058 try:
1059 task_item.start = datetime.datetime.now()
1060 print('[%s] Started.' % Elapsed(task_item.start),
1061 file=task_item.outbuf)
1062 task_item.run(*args, **kwargs)
1063 task_item.finish = datetime.datetime.now()
1064 print('[%s] Finished.' % Elapsed(task_item.finish),
1065 file=task_item.outbuf)
1066 self.ran.append(task_item.name)
1067 if self.verbose:
1068 if self.progress:
1069 print('')
1070 print(self.format_task_output(task_item))
1071 if self.progress:
1072 self.progress.update(
1073 1, ', '.join(t.item.name for t in self.running))
1074 except KeyboardInterrupt:
1075 print(self.format_task_output(task_item, 'interrupted'),
1076 file=sys.stderr)
1077 raise
1078 except Exception:
1079 print(self.format_task_output(task_item, 'ERROR'),
1080 file=sys.stderr)
1081 raise
1082
1083 class _Worker(threading.Thread):
1084 """One thread to execute one WorkItem."""
1085 def __init__(self, item, index, args, kwargs):
1086 threading.Thread.__init__(self, name=item.name or 'Worker')
1087 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
1088 self.item = item
1089 self.index = index
1090 self.args = args
1091 self.kwargs = kwargs
1092 self.daemon = True
1093
1094 def run(self):
1095 """Runs in its own thread."""
1096 logging.debug('_Worker.run(%s)' % self.item.name)
1097 work_queue = self.kwargs['work_queue']
1098 try:
1099 self.item.start = datetime.datetime.now()
1100 print('[%s] Started.' % Elapsed(self.item.start),
1101 file=self.item.outbuf)
1102 self.item.run(*self.args, **self.kwargs)
1103 self.item.finish = datetime.datetime.now()
1104 print('[%s] Finished.' % Elapsed(self.item.finish),
1105 file=self.item.outbuf)
1106 except KeyboardInterrupt:
1107 logging.info('Caught KeyboardInterrupt in thread %s',
1108 self.item.name)
1109 logging.info(str(sys.exc_info()))
1110 work_queue.exceptions.put((sys.exc_info(), self))
1111 raise
1112 except Exception:
1113 # Catch exception location.
1114 logging.info('Caught exception in thread %s', self.item.name)
1115 logging.info(str(sys.exc_info()))
1116 work_queue.exceptions.put((sys.exc_info(), self))
1117 finally:
1118 logging.info('_Worker.run(%s) done', self.item.name)
1119 work_queue.ready_cond.acquire()
1120 try:
1121 work_queue.ready_cond.notifyAll()
1122 finally:
1123 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001124
1125
agable92bec4f2016-08-24 09:27:27 -07001126def GetEditor(git_editor=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001127 """Returns the most plausible editor to use.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001128
1129 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001130 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001131 - core.editor git configuration variable (if supplied by git-cl)
1132 - VISUAL environment variable
1133 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001134 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001135
1136 In the case of git-cl, this matches git's behaviour, except that it does not
1137 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001138 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001139 editor = os.environ.get('GIT_EDITOR') or git_editor
1140 if not editor:
1141 editor = os.environ.get('VISUAL')
1142 if not editor:
1143 editor = os.environ.get('EDITOR')
1144 if not editor:
1145 if sys.platform.startswith('win'):
1146 editor = 'notepad'
1147 else:
1148 editor = 'vi'
1149 return editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001150
1151
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001152def RunEditor(content, git, git_editor=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001153 """Opens up the default editor in the system to get the CL description."""
1154 editor = GetEditor(git_editor=git_editor)
1155 if not editor:
1156 return None
1157 # Make sure CRLF is handled properly by requiring none.
1158 if '\r' in content:
1159 print('!! Please remove \\r from your change description !!',
1160 file=sys.stderr)
Robert Iannucci15d9af92023-07-12 21:11:23 +00001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 file_handle, filename = tempfile.mkstemp(text=True,
1163 prefix='cl_description.')
1164 fileobj = os.fdopen(file_handle, 'wb')
1165 # Still remove \r if present.
1166 content = re.sub('\r?\n', '\n', content)
1167 # Some editors complain when the file doesn't end in \n.
1168 if not content.endswith('\n'):
1169 content += '\n'
Robert Iannucci15d9af92023-07-12 21:11:23 +00001170
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001171 if 'vim' in editor or editor == 'vi':
1172 # If the user is using vim and has 'modelines' enabled, this will change
1173 # the filetype from a generic auto-detected 'conf' to 'gitcommit', which
1174 # is used to activate proper column wrapping, spell checking, syntax
1175 # highlighting for git footers, etc.
1176 #
1177 # Because of the implementation of GetEditor above, we also check for
1178 # the exact string 'vi' here, to help users get a sane default when they
1179 # have vi symlink'd to vim (or something like vim).
1180 fileobj.write('# vim: ft=gitcommit\n'.encode('utf-8'))
Robert Iannucci15d9af92023-07-12 21:11:23 +00001181
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001182 fileobj.write(content.encode('utf-8'))
1183 fileobj.close()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001184
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001185 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001186 cmd = '%s %s' % (editor, filename)
1187 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1188 # Msysgit requires the usage of 'env' to be present.
1189 cmd = 'env ' + cmd
1190 try:
1191 # shell=True to allow the shell to handle all forms of quotes in
1192 # $EDITOR.
1193 subprocess2.check_call(cmd, shell=True)
1194 except subprocess2.CalledProcessError:
1195 return None
1196 return FileRead(filename)
1197 finally:
1198 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001199
1200
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001201def UpgradeToHttps(url):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001202 """Upgrades random urls to https://.
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001203
1204 Do not touch unknown urls like ssh:// or git://.
1205 Do not touch http:// urls with a port number,
1206 Fixes invalid GAE url.
1207 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001208 if not url:
1209 return url
1210 if not re.match(r'[a-z\-]+\://.*', url):
1211 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1212 # relative url and will use http:///foo. Note that it defaults to
1213 # http:// for compatibility with naked url like "localhost:8080".
1214 url = 'http://%s' % url
1215 parsed = list(urllib.parse.urlparse(url))
1216 # Do not automatically upgrade http to https if a port number is provided.
1217 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1218 parsed[0] = 'https'
1219 return urllib.parse.urlunparse(parsed)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001220
1221
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001222def ParseCodereviewSettingsContent(content):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001223 """Process a codereview.settings file properly."""
1224 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1225 try:
1226 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1227 except ValueError:
1228 raise Error('Failed to process settings, please fix. Content:\n\n%s' %
1229 content)
1230
1231 def fix_url(key):
1232 if keyvals.get(key):
1233 keyvals[key] = UpgradeToHttps(keyvals[key])
1234
1235 fix_url('CODE_REVIEW_SERVER')
1236 fix_url('VIEW_VC')
1237 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001238
1239
1240def NumLocalCpus():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001241 """Returns the number of processors.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001242
dnj@chromium.org530523b2015-01-07 19:54:57 +00001243 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1244 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1245 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001246 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001247 # Surround the entire thing in try/except; no failure here should stop
1248 # gclient from working.
dnj@chromium.org530523b2015-01-07 19:54:57 +00001249 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001250 # Use multiprocessing to get CPU count. This may raise
1251 # NotImplementedError.
1252 try:
1253 import multiprocessing
1254 return multiprocessing.cpu_count()
1255 except NotImplementedError: # pylint: disable=bare-except
1256 # (UNIX) Query 'os.sysconf'.
1257 # pylint: disable=no-member
1258 if hasattr(os,
1259 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1260 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
dnj@chromium.org530523b2015-01-07 19:54:57 +00001261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001262 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1263 if 'NUMBER_OF_PROCESSORS' in os.environ:
1264 return int(os.environ['NUMBER_OF_PROCESSORS'])
1265 except Exception as e:
1266 logging.exception("Exception raised while probing CPU count: %s", e)
dnj@chromium.org530523b2015-01-07 19:54:57 +00001267
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001268 logging.debug('Failed to get CPU count. Defaulting to 1.')
1269 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001270
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001271
szager@chromium.orgfc616382014-03-18 20:32:04 +00001272def DefaultDeltaBaseCacheLimit():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001273 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
szager@chromium.orgfc616382014-03-18 20:32:04 +00001274
1275 The primary constraint is the address space of virtual memory. The cache
1276 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1277 parameter is set too high.
1278 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001279 if platform.architecture()[0].startswith('64'):
1280 return '2g'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001281
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001282 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001283
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001284
szager@chromium.orgff113292014-03-25 06:02:08 +00001285def DefaultIndexPackConfig(url=''):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001286 """Return reasonable default values for configuring git-index-pack.
szager@chromium.orgfc616382014-03-18 20:32:04 +00001287
1288 Experiments suggest that higher values for pack.threads don't improve
1289 performance."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001290 cache_limit = DefaultDeltaBaseCacheLimit()
1291 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
1292 if url in THREADED_INDEX_PACK_BLOCKLIST:
1293 result.extend(['-c', 'pack.threads=1'])
1294 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001295
1296
1297def FindExecutable(executable):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001298 """This mimics the "which" utility."""
1299 path_folders = os.environ.get('PATH').split(os.pathsep)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001300
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001301 for path_folder in path_folders:
1302 target = os.path.join(path_folder, executable)
1303 # Just in case we have some ~/blah paths.
1304 target = os.path.abspath(os.path.expanduser(target))
1305 if os.path.isfile(target) and os.access(target, os.X_OK):
1306 return target
1307 if sys.platform.startswith('win'):
1308 for suffix in ('.bat', '.cmd', '.exe'):
1309 alt_target = target + suffix
1310 if os.path.isfile(alt_target) and os.access(
1311 alt_target, os.X_OK):
1312 return alt_target
1313 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001314
1315
1316def freeze(obj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001317 """Takes a generic object ``obj``, and returns an immutable version of it.
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001318
1319 Supported types:
1320 * dict / OrderedDict -> FrozenDict
1321 * list -> tuple
1322 * set -> frozenset
1323 * any object with a working __hash__ implementation (assumes that hashable
1324 means immutable)
1325
1326 Will raise TypeError if you pass an object which is not hashable.
1327 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001328 if isinstance(obj, collections.abc.Mapping):
1329 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001331 if isinstance(obj, (list, tuple)):
1332 return tuple(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001334 if isinstance(obj, set):
1335 return frozenset(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001337 hash(obj)
1338 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001339
1340
Gavin Mak65c49b12023-08-24 18:06:42 +00001341class FrozenDict(collections.abc.Mapping):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001342 """An immutable OrderedDict.
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001343
1344 Modified From: http://stackoverflow.com/a/2704866
1345 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001346 def __init__(self, *args, **kwargs):
1347 self._d = collections.OrderedDict(*args, **kwargs)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001349 # Calculate the hash immediately so that we know all the items are
1350 # hashable too.
1351 self._hash = functools.reduce(operator.xor,
1352 (hash(i)
1353 for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001355 def __eq__(self, other):
1356 if not isinstance(other, collections.abc.Mapping):
1357 return NotImplemented
1358 if self is other:
1359 return True
1360 if len(self) != len(other):
1361 return False
1362 for k, v in self.items():
1363 if k not in other or other[k] != v:
1364 return False
1365 return True
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001367 def __iter__(self):
1368 return iter(self._d)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001370 def __len__(self):
1371 return len(self._d)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001373 def __getitem__(self, key):
1374 return self._d[key]
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001375
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001376 def __hash__(self):
1377 return self._hash
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001378
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001379 def __repr__(self):
1380 return 'FrozenDict(%r)' % (self._d.items(), )