blob: 575d021dbb8dd5140c9d3e10528b57d2955ed0b8 [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
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00007import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02008import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00009import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000010import datetime
Ben Pastened410c662020-08-26 17:07:03 +000011import errno
Raul Tambreb946b232019-03-26 14:48:46 +000012import functools
13import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000014import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020015import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000016import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000017import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000018import platform
Gavin Mak65c49b12023-08-24 18:06:42 +000019import queue
msb@chromium.orgac915bb2009-11-13 17:03:01 +000020import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000021import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000022import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000023import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000024import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000025import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000026import time
Gavin Mak65c49b12023-08-24 18:06:42 +000027import urllib.parse
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000028
Gavin Mak65c49b12023-08-24 18:06:42 +000029import subprocess2
Raul Tambreb946b232019-03-26 14:48:46 +000030
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000031
Josip Sokcevic38d669f2022-09-02 18:08:57 +000032# Git wrapper retries on a transient error, and some callees do retries too,
33# such as GitWrapper.update (doing clone). One retry attempt should be
34# sufficient to help with any transient errors at this level.
35RETRY_MAX = 1
36RETRY_INITIAL_SLEEP = 2 # in seconds
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000037START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000038
39
borenet@google.com6a9b1682014-03-24 18:35:23 +000040_WARNINGS = []
41
42
szager@chromium.orgff113292014-03-25 06:02:08 +000043# These repos are known to cause OOM errors on 32-bit platforms, due the the
44# very large objects they contain. It is not safe to use threaded index-pack
45# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000046THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000047 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
48]
49
Gavin Mak65c49b12023-08-24 18:06:42 +000050def reraise(typ, value, tb=None):
51 """To support rethrowing exceptions with tracebacks."""
52 if value is None:
53 value = typ()
54 if value.__traceback__ is not tb:
55 raise value.with_traceback(tb)
56 raise value
Raul Tambreb946b232019-03-26 14:48:46 +000057
szager@chromium.orgff113292014-03-25 06:02:08 +000058
maruel@chromium.org66c83e62010-09-07 14:18:45 +000059class Error(Exception):
60 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000061 def __init__(self, msg, *args, **kwargs):
62 index = getattr(threading.currentThread(), 'index', 0)
63 if index:
64 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
65 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000066
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000067
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000068def Elapsed(until=None):
69 if until is None:
70 until = datetime.datetime.now()
71 return str(until - START).partition('.')[0]
72
73
borenet@google.com6a9b1682014-03-24 18:35:23 +000074def PrintWarnings():
75 """Prints any accumulated warnings."""
76 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000077 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000078 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000079 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000080
81
82def AddWarning(msg):
83 """Adds the given warning message to the list of accumulated warnings."""
84 _WARNINGS.append(msg)
85
86
Joanna Wang66286612022-06-30 19:59:13 +000087def FuzzyMatchRepo(repo, candidates):
88 # type: (str, Union[Collection[str], Mapping[str, Any]]) -> Optional[str]
89 """Attempts to find a representation of repo in the candidates.
90
91 Args:
92 repo: a string representation of a repo in the form of a url or the
93 name and path of the solution it represents.
94 candidates: The candidates to look through which may contain `repo` in
95 in any of the forms mentioned above.
96 Returns:
97 The matching string, if any, which may be in a different form from `repo`.
98 """
99 if repo in candidates:
100 return repo
101 if repo.endswith('.git') and repo[:-len('.git')] in candidates:
102 return repo[:-len('.git')]
103 if repo + '.git' in candidates:
104 return repo + '.git'
105 return None
106
107
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000108def SplitUrlRevision(url):
109 """Splits url and returns a two-tuple: url, rev"""
110 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000111 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000112 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000113 components = re.search(regex, url).groups()
114 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000115 components = url.rsplit('@', 1)
116 if re.match(r'^\w+\@', url) and '@' not in components[0]:
117 components = [url]
118
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000119 if len(components) == 1:
120 components += [None]
121 return tuple(components)
122
123
Joanna Wang1a977bd2022-06-02 21:51:17 +0000124def ExtractRefName(remote, full_refs_str):
125 """Returns the ref name if full_refs_str is a valid ref."""
126 result = re.compile(r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
127 remote).match(full_refs_str)
128 if result:
129 return result.group('ref_name')
130 return None
131
132
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000133def IsGitSha(revision):
134 """Returns true if the given string is a valid hex-encoded sha"""
135 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
136
137
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200138def IsFullGitSha(revision):
139 """Returns true if the given string is a valid hex-encoded full sha"""
140 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
141
142
floitsch@google.comeaab7842011-04-28 09:07:58 +0000143def IsDateRevision(revision):
144 """Returns true if the given revision is of the form "{ ... }"."""
145 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
146
147
148def MakeDateRevision(date):
149 """Returns a revision representing the latest revision before the given
150 date."""
151 return "{" + date + "}"
152
153
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000154def SyntaxErrorToError(filename, e):
155 """Raises a gclient_utils.Error exception with the human readable message"""
156 try:
157 # Try to construct a human readable error message
158 if filename:
159 error_message = 'There is a syntax error in %s\n' % filename
160 else:
161 error_message = 'There is a syntax error\n'
162 error_message += 'Line #%s, character %s: "%s"' % (
163 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
164 except:
165 # Something went wrong, re-raise the original exception
166 raise e
167 else:
168 raise Error(error_message)
169
170
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000171class PrintableObject(object):
172 def __str__(self):
173 output = ''
174 for i in dir(self):
175 if i.startswith('__'):
176 continue
177 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
178 return output
179
180
Edward Lesmesae3586b2020-03-23 21:21:14 +0000181def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000182 # Try to load the readline module, so that "elaborate line editing" features
183 # such as backspace work for `raw_input` / `input`.
184 try:
185 import readline
186 except ImportError:
187 # The readline module does not exist in all Python distributions, e.g. on
188 # Windows. Fall back to simple input handling.
189 pass
190
Gavin Mak65c49b12023-08-24 18:06:42 +0000191 # Use this so that it can be mocked in tests.
Edward Lesmesae3586b2020-03-23 21:21:14 +0000192 try:
Edward Lesmesae3586b2020-03-23 21:21:14 +0000193 return input(message)
194 except KeyboardInterrupt:
195 # Hide the exception.
196 sys.exit(1)
197
198
Edward Lemur419c92f2019-10-25 22:17:49 +0000199def FileRead(filename, mode='rbU'):
Josip Sokcevic7958e302023-03-01 23:02:21 +0000200 # mode is ignored now; we always return unicode strings.
201 with open(filename, mode='rb') as f:
202 s = f.read()
203 try:
204 return s.decode('utf-8', 'replace')
205 except (UnicodeDecodeError, AttributeError):
206 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000207
208
Edward Lemur1773f372020-02-22 00:27:14 +0000209def FileWrite(filename, content, mode='w', encoding='utf-8'):
Josip Sokcevic7958e302023-03-01 23:02:21 +0000210 with codecs.open(filename, mode=mode, encoding=encoding) as f:
211 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000212
213
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000214@contextlib.contextmanager
215def temporary_directory(**kwargs):
216 tdir = tempfile.mkdtemp(**kwargs)
217 try:
218 yield tdir
219 finally:
220 if tdir:
221 rmtree(tdir)
222
223
Edward Lemur1773f372020-02-22 00:27:14 +0000224@contextlib.contextmanager
225def temporary_file():
226 """Creates a temporary file.
227
228 On Windows, a file must be closed before it can be opened again. This function
229 allows to write something like:
230
231 with gclient_utils.temporary_file() as tmp:
232 gclient_utils.FileWrite(tmp, foo)
233 useful_stuff(tmp)
234
235 Instead of something like:
236
237 with tempfile.NamedTemporaryFile(delete=False) as tmp:
238 tmp.write(foo)
239 tmp.close()
240 try:
241 useful_stuff(tmp)
242 finally:
243 os.remove(tmp.name)
244 """
245 handle, name = tempfile.mkstemp()
246 os.close(handle)
247 try:
248 yield name
249 finally:
250 os.remove(name)
251
252
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000253def safe_rename(old, new):
254 """Renames a file reliably.
255
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000256 Sometimes os.rename does not work because a dying git process keeps a handle
257 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000258 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000259 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000260 """
261 # roughly 10s
262 retries = 100
263 for i in range(retries):
264 try:
265 os.rename(old, new)
266 break
267 except OSError:
268 if i == (retries - 1):
269 # Give up.
270 raise
271 # retry
272 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
273 time.sleep(0.1)
274
275
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000276def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000277 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000278 os.remove(path)
279 else:
280 rmtree(path)
281
282
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000283def rmtree(path):
284 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000285
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000286 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287
288 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700289 are read-only. We need to be able to force the files to be writable (i.e.,
290 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000291
292 Even with all this, Windows still sometimes fails to delete a file, citing
293 a permission error (maybe something to do with antivirus scans or disk
294 indexing). The best suggestion any of the user forums had was to wait a
295 bit and try again, so we do that too. It's hand-waving, but sometimes it
296 works. :/
297
298 On POSIX systems, things are a little bit simpler. The modes of the files
299 to be deleted doesn't matter, only the modes of the directories containing
300 them are significant. As the directory tree is traversed, each directory
301 has its mode set appropriately before descending into it. This should
302 result in the entire tree being removed, with the possible exception of
303 *path itself, because nothing attempts to change the mode of its parent.
304 Doing so would be hazardous, as it's not a directory slated for removal.
305 In the ordinary case, this is not a problem: for our purposes, the user
306 will never lack write permission on *path's parent.
307 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000308 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000309 return
310
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000311 if os.path.islink(path) or not os.path.isdir(path):
312 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000313
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000314 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000315 # Give up and use cmd.exe's rd command.
316 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000317 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000318 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
319 if exitcode == 0:
320 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000321
322 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000323 time.sleep(3)
324 raise Exception('Failed to remove path %s' % path)
325
326 # 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
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000331 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000332 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000333
334 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000335 # 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 just
337 # want to remove the link. Check islink and treat links as ordinary files
338 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000339 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000340 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000341 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000342 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000343 # Recurse.
344 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000345
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000346 remove(os.rmdir, path)
347
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000348
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000349def safe_makedirs(tree):
350 """Creates the directory in a safe manner.
351
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 """
355 count = 0
356 while not os.path.exists(tree):
357 count += 1
358 try:
359 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000360 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000361 # 17 POSIX, 183 Windows
362 if e.errno not in (17, 183):
363 raise
364 if count > 40:
365 # Give up.
366 raise
367
368
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000369def CommandToStr(args):
370 """Converts an arg list into a shell escaped string."""
371 return ' '.join(pipes.quote(arg) for arg in args)
372
373
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000374class Wrapper(object):
375 """Wraps an object, acting as a transparent proxy for all properties by
376 default.
377 """
378 def __init__(self, wrapped):
379 self._wrapped = wrapped
380
381 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):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000386 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387 def __init__(self, wrapped, delay):
388 super(AutoFlush, self).__init__(wrapped)
389 if not hasattr(self, 'lock'):
390 self.lock = threading.Lock()
391 self.__last_flushed_at = time.time()
392 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000393
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000394 @property
395 def autoflush(self):
396 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000397
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000398 def write(self, out, *args, **kwargs):
399 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000400 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000401 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000402 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000403 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000404 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000405 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000406 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000408 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000409 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000410
411
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000412class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000413 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 threads with a NN> prefix.
415 """
416 def __init__(self, wrapped, include_zero=False):
417 super(Annotated, self).__init__(wrapped)
418 if not hasattr(self, 'lock'):
419 self.lock = threading.Lock()
420 self.__output_buffers = {}
421 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000422 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000423
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000424 @property
425 def annotated(self):
426 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000427
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000428 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000429 # Store as bytes to ensure Unicode characters get output correctly.
430 if not isinstance(out, bytes):
431 out = out.encode('utf-8')
432
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000433 index = getattr(threading.currentThread(), 'index', 0)
434 if not index and not self.__include_zero:
435 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000437
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000438 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000439 try:
440 # Use a dummy array to hold the string so the code can be lockless.
441 # Strings are immutable, requiring to keep a lock for the whole dictionary
442 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000443 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000444 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000445 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000447 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000448 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000449
450 # Continue lockless.
451 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000452 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000453 cr_loc = obj[0].find(b'\r')
454 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000455 if cr_loc == lf_loc == -1:
456 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000457
458 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000459 line, remaining = obj[0].split(b'\n', 1)
Josip Sokcevic42c5bbb2022-01-24 21:42:28 +0000460 if line:
461 self._wrapped_write(b'%d>%s\n' % (index, line))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000462 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000463 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000464 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000465 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000466 obj[0] = remaining
467
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000468 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000469 """Flush buffered output."""
470 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000471 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000472 try:
473 # Detect threads no longer existing.
474 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000475 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000476 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000477 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000478 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000479 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000480 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000481 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000482 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000483
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000484 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000485 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000486 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000487 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000488 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000489
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000490
491def MakeFileAutoFlush(fileobj, delay=10):
492 autoflush = getattr(fileobj, 'autoflush', None)
493 if autoflush:
494 autoflush.delay = delay
495 return fileobj
496 return AutoFlush(fileobj, delay)
497
498
499def MakeFileAnnotated(fileobj, include_zero=False):
500 if getattr(fileobj, 'annotated', None):
501 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000502 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000503
504
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000505GCLIENT_CHILDREN = []
506GCLIENT_CHILDREN_LOCK = threading.Lock()
507
508
509class GClientChildren(object):
510 @staticmethod
511 def add(popen_obj):
512 with GCLIENT_CHILDREN_LOCK:
513 GCLIENT_CHILDREN.append(popen_obj)
514
515 @staticmethod
516 def remove(popen_obj):
517 with GCLIENT_CHILDREN_LOCK:
518 GCLIENT_CHILDREN.remove(popen_obj)
519
520 @staticmethod
521 def _attemptToKillChildren():
522 global GCLIENT_CHILDREN
523 with GCLIENT_CHILDREN_LOCK:
524 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
525
526 for zombie in zombies:
527 try:
528 zombie.kill()
529 except OSError:
530 pass
531
532 with GCLIENT_CHILDREN_LOCK:
533 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
534
535 @staticmethod
536 def _areZombies():
537 with GCLIENT_CHILDREN_LOCK:
538 return bool(GCLIENT_CHILDREN)
539
540 @staticmethod
541 def KillAllRemainingChildren():
542 GClientChildren._attemptToKillChildren()
543
544 if GClientChildren._areZombies():
545 time.sleep(0.5)
546 GClientChildren._attemptToKillChildren()
547
548 with GCLIENT_CHILDREN_LOCK:
549 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000550 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000551 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000552 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000553
554
Edward Lemur24146be2019-08-01 21:44:52 +0000555def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
556 show_header=False, always_show_header=False, retry=False,
557 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000558 """Runs a command and calls back a filter function if needed.
559
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000560 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000561 print_stdout: If True, the command's stdout is forwarded to stdout.
562 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000563 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000564 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000565 show_header: Whether to display a header before the command output.
566 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000567 retry: If the process exits non-zero, sleep for a brief interval and try
568 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000569
570 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000571
572 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000573 """
Edward Lemur24146be2019-08-01 21:44:52 +0000574 def show_header_if_necessary(needs_header, attempt):
575 """Show the header at most once."""
576 if not needs_header[0]:
577 return
578
579 needs_header[0] = False
580 # Automatically generated header. We only prepend a newline if
581 # always_show_header is false, since it usually indicates there's an
582 # external progress display, and it's better not to clobber it in that case.
583 header = '' if always_show_header else '\n'
584 header += '________ running \'%s\' in \'%s\'' % (
585 ' '.join(args), kwargs.get('cwd', '.'))
586 if attempt:
587 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
588 header += '\n'
589
590 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000591 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
592 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000593 if filter_fn:
594 filter_fn(header)
595
596 def filter_line(command_output, line_start):
597 """Extract the last line from command output and filter it."""
598 if not filter_fn or line_start is None:
599 return
600 command_output.seek(line_start)
601 filter_fn(command_output.read().decode('utf-8'))
602
603 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
604 # byte inputs and sys.stdout.buffer must be used instead.
605 if print_stdout:
606 sys.stdout.flush()
607 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
608 else:
609 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000610
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000611 sleep_interval = RETRY_INITIAL_SLEEP
612 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000613
614 # Store the output of the command regardless of the value of print_stdout or
615 # filter_fn.
616 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000617 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000618 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
619 # subprocess when filtering its output. This makes the subproc believe
620 # it was launched from a terminal, which will preserve ANSI color codes.
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000621 os_type = GetOperatingSystem()
Milad Fad949c912020-09-18 00:26:08 +0000622 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000623 pipe_reader, pipe_writer = os.openpty()
624 else:
625 pipe_reader, pipe_writer = os.pipe()
626
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000627 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000628 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000629 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000630 # Close the write end of the pipe once we hand it off to the child proc.
631 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000632
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000633 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000634
Edward Lemur24146be2019-08-01 21:44:52 +0000635 # Passed as a list for "by ref" semantics.
636 needs_header = [show_header]
637 if always_show_header:
638 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000639
640 # Also, we need to forward stdout to prevent weird re-ordering of output.
641 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700642 # normally buffering is done for each line, but if the process requests
643 # input, no end-of-line character is output after the prompt and it would
644 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000645 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000646 line_start = None
647 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000648 try:
649 in_byte = os.read(pipe_reader, 1)
650 except (IOError, OSError) as e:
651 if e.errno == errno.EIO:
652 # An errno.EIO means EOF?
653 in_byte = None
654 else:
655 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000656 is_newline = in_byte in (b'\n', b'\r')
657 if not in_byte:
658 break
659
660 show_header_if_necessary(needs_header, attempt)
661
662 if is_newline:
663 filter_line(command_output, line_start)
664 line_start = None
665 elif line_start is None:
666 line_start = command_output.tell()
667
668 stdout_write(in_byte)
669 command_output.write(in_byte)
670
671 # Flush the rest of buffered output.
672 sys.stdout.flush()
673 if line_start is not None:
674 filter_line(command_output, line_start)
675
Ben Pastened410c662020-08-26 17:07:03 +0000676 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000677 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000678
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000679 # Don't put this in a 'finally,' since the child may still run if we get
680 # an exception.
681 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000682
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000683 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000684 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000685 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000686
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000687 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000688 return command_output.getvalue()
689
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000690 if not retry:
691 break
Edward Lemur24146be2019-08-01 21:44:52 +0000692
Raul Tambreb946b232019-03-26 14:48:46 +0000693 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
694 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000695 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000696 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000697 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000698
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000699 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000700 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000701
702
agable@chromium.org5a306a22014-02-24 22:13:59 +0000703class GitFilter(object):
704 """A filter_fn implementation for quieting down git output messages.
705
706 Allows a custom function to skip certain lines (predicate), and will throttle
707 the output of percentage completed lines to only output every X seconds.
708 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000709 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000710
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000711 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000712 """
713 Args:
714 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
715 XX% complete messages) to only be printed at least |time_throttle|
716 seconds apart.
717 predicate (f(line)): An optional function which is invoked for every line.
718 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000719 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000720 """
Edward Lemur24146be2019-08-01 21:44:52 +0000721 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000722 self.last_time = 0
723 self.time_throttle = time_throttle
724 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000725 self.out_fh = out_fh or sys.stdout
726 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000727
728 def __call__(self, line):
729 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000730 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000731 if esc > -1:
732 line = line[:esc]
733 if self.predicate and not self.predicate(line):
734 return
735 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000736 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000737 if match:
738 if match.group(1) != self.progress_prefix:
739 self.progress_prefix = match.group(1)
740 elif now - self.last_time < self.time_throttle:
741 return
742 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000743 if not self.first_line:
744 self.out_fh.write('[%s] ' % Elapsed())
745 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000746 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000747
748
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000749def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000750 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000751
rcui@google.com13595ff2011-10-13 01:25:07 +0000752 Returns nearest upper-level directory with the passed in file.
753 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000754 if not path:
755 path = os.getcwd()
756 path = os.path.realpath(path)
757 while True:
758 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000759 if os.path.exists(file_path):
760 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000761 (new_path, _) = os.path.split(path)
762 if new_path == path:
763 return None
764 path = new_path
765
766
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000767def GetOperatingSystem():
768 """Returns 'mac', 'win', 'linux', or the name of the current platform."""
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000769 if sys.platform.startswith(('cygwin', 'win')):
770 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000771
772 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000773 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000774
775 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000776 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000777
778 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000779 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000780
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000781 try:
782 return os.uname().sysname.lower()
783 except AttributeError:
784 return sys.platform
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000785
786
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000787def GetGClientRootAndEntries(path=None):
788 """Returns the gclient root and the dict of entries."""
789 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000790 root = FindFileUpwards(config_file, path)
791 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000792 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000793 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000794 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000795 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000796 with open(config_path) as config:
797 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000798 config_dir = os.path.dirname(config_path)
799 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000800
801
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000802def lockedmethod(method):
803 """Method decorator that holds self.lock for the duration of the call."""
804 def inner(self, *args, **kwargs):
805 try:
806 try:
807 self.lock.acquire()
808 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000809 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000810 raise
811 return method(self, *args, **kwargs)
812 finally:
813 self.lock.release()
814 return inner
815
816
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000817class WorkItem(object):
818 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000819 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
820 # As a workaround, use a single lock. Yep you read it right. Single lock for
821 # all the 100 objects.
822 lock = threading.Lock()
823
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000824 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000825 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000826 self._name = name
Gavin Mak65c49b12023-08-24 18:06:42 +0000827 self.outbuf = io.StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000828 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700829 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000831 def run(self, work_queue):
832 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000833 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000834
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000835 @property
836 def name(self):
837 return self._name
838
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000839
840class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000841 """Runs a set of WorkItem that have interdependencies and were WorkItem are
842 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000843
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200844 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000845 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000846
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000847 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000849 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000850 """jobs specifies the number of concurrent tasks to allow. progress is a
851 Progress instance."""
852 # Set when a thread is done or a new item is enqueued.
853 self.ready_cond = threading.Condition()
854 # Maximum number of concurrent tasks.
855 self.jobs = jobs
856 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000857 self.queued = []
858 # List of strings representing each Dependency.name that was run.
859 self.ran = []
860 # List of items currently running.
861 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000862 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000863 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000864 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000865 self.progress = progress
866 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000867 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000868
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000869 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000870 self.verbose = verbose
871 self.last_join = None
872 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000873
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000874 def enqueue(self, d):
875 """Enqueue one Dependency to be executed later once its requirements are
876 satisfied.
877 """
878 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000879 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000880 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000881 self.queued.append(d)
882 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000883 if self.jobs == 1:
884 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000885 logging.debug('enqueued(%s)' % d.name)
886 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000887 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000888 self.progress.update(0)
889 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000890 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000891 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000892
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000893 def out_cb(self, _):
894 self.last_subproc_output = datetime.datetime.now()
895 return True
896
897 @staticmethod
898 def format_task_output(task, comment=''):
899 if comment:
900 comment = ' (%s)' % comment
901 if task.start and task.finish:
902 elapsed = ' (Elapsed: %s)' % (
903 str(task.finish - task.start).partition('.')[0])
904 else:
905 elapsed = ''
906 return """
907%s%s%s
908----------------------------------------
909%s
910----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000911 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000912
hinoka885e5b12016-06-08 14:40:09 -0700913 def _is_conflict(self, job):
914 """Checks to see if a job will conflict with another running job."""
915 for running_job in self.running:
916 for used_resource in running_job.item.resources:
917 logging.debug('Checking resource %s' % used_resource)
918 if used_resource in job.resources:
919 return True
920 return False
921
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000922 def flush(self, *args, **kwargs):
923 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000924 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000925 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000926 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000927 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000928 while True:
929 # Check for task to run first, then wait.
930 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000931 if not self.exceptions.empty():
932 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000933 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000934 self._flush_terminated_threads()
935 if (not self.queued and not self.running or
936 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000937 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000938 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000939
940 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000941 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000942 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000943 if (self.ignore_requirements or
944 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700945 if not self._is_conflict(self.queued[i]):
946 # Start one work item: all its requirements are satisfied.
947 self._run_one_task(self.queued.pop(i), args, kwargs)
948 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000949 else:
950 # Couldn't find an item that could run. Break out the outher loop.
951 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000952
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000953 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000954 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000955 break
956 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000957 try:
958 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000959 # If we haven't printed to terminal for a while, but we have received
960 # spew from a suprocess, let the user know we're still progressing.
961 now = datetime.datetime.now()
962 if (now - self.last_join > datetime.timedelta(seconds=60) and
963 self.last_subproc_output > self.last_join):
964 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000965 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000966 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000967 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000968 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000969 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000970 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000971 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000972 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000973 except KeyboardInterrupt:
974 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000975 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000976 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000977 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
978 self.ran), len(self.running)),
979 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000980 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000981 print(
982 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
983 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000984 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000985 print(
986 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000987 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000988 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000989 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000990 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000991
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000992 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000993 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000994 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000995 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000996 # To get back the stack location correctly, the raise a, b, c form must be
997 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000998 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000999 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
1000 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001001 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001002 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001003
maruel@chromium.org3742c842010-09-09 19:27:14 +00001004 def _flush_terminated_threads(self):
1005 """Flush threads that have terminated."""
1006 running = self.running
1007 self.running = []
1008 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +00001009 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +00001010 self.running.append(t)
1011 else:
1012 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001013 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +00001014 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001015 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +00001016 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +00001017 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001018 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001019 if t.item.name in self.ran:
1020 raise Error(
1021 'gclient is confused, "%s" is already in "%s"' % (
1022 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001023 if not t.item.name in self.ran:
1024 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001025
1026 def _run_one_task(self, task_item, args, kwargs):
1027 if self.jobs > 1:
1028 # Start the thread.
1029 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001030 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001031 self.running.append(new_thread)
1032 new_thread.start()
1033 else:
1034 # Run the 'thread' inside the main thread. Don't try to catch any
1035 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001036 try:
1037 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001038 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001039 task_item.run(*args, **kwargs)
1040 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001041 print(
1042 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001043 self.ran.append(task_item.name)
1044 if self.verbose:
1045 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001046 print('')
1047 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001048 if self.progress:
1049 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1050 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001051 print(
1052 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001053 raise
1054 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001055 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001056 raise
1057
maruel@chromium.org3742c842010-09-09 19:27:14 +00001058
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001059 class _Worker(threading.Thread):
1060 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001061 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001062 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001063 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001064 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001065 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001066 self.args = args
1067 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001068 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001069
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001070 def run(self):
1071 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001072 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001073 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001074 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001075 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001076 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001077 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001078 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001079 print(
1080 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001081 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001082 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001083 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001084 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001085 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001086 except Exception:
1087 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001088 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001089 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001090 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001091 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001092 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001093 work_queue.ready_cond.acquire()
1094 try:
1095 work_queue.ready_cond.notifyAll()
1096 finally:
1097 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098
1099
agable92bec4f2016-08-24 09:27:27 -07001100def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001101 """Returns the most plausible editor to use.
1102
1103 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001104 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001105 - core.editor git configuration variable (if supplied by git-cl)
1106 - VISUAL environment variable
1107 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001108 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001109
1110 In the case of git-cl, this matches git's behaviour, except that it does not
1111 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001112 """
agable92bec4f2016-08-24 09:27:27 -07001113 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001114 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001115 editor = os.environ.get('VISUAL')
1116 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001117 editor = os.environ.get('EDITOR')
1118 if not editor:
1119 if sys.platform.startswith('win'):
1120 editor = 'notepad'
1121 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001122 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001123 return editor
1124
1125
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001126def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 """Opens up the default editor in the system to get the CL description."""
Robert Iannucci15d9af92023-07-12 21:11:23 +00001128 editor = GetEditor(git_editor=git_editor)
1129 if not editor:
1130 return None
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001131 # Make sure CRLF is handled properly by requiring none.
1132 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001133 print(
1134 '!! Please remove \\r from your change description !!', file=sys.stderr)
Robert Iannucci15d9af92023-07-12 21:11:23 +00001135
1136 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description.')
sokcevic07152802021-08-18 00:06:34 +00001137 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001138 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001139 content = re.sub('\r?\n', '\n', content)
1140 # Some editors complain when the file doesn't end in \n.
1141 if not content.endswith('\n'):
1142 content += '\n'
Robert Iannucci15d9af92023-07-12 21:11:23 +00001143
1144 if 'vim' in editor or editor == 'vi':
1145 # If the user is using vim and has 'modelines' enabled, this will change the
1146 # filetype from a generic auto-detected 'conf' to 'gitcommit', which is used
1147 # to activate proper column wrapping, spell checking, syntax highlighting
1148 # for git footers, etc.
1149 #
1150 # Because of the implementation of GetEditor above, we also check for the
1151 # exact string 'vi' here, to help users get a sane default when they have vi
1152 # symlink'd to vim (or something like vim).
1153 fileobj.write('# vim: ft=gitcommit\n'.encode('utf-8'))
1154
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:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001159 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001160 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1161 # Msysgit requires the usage of 'env' to be present.
1162 cmd = 'env ' + cmd
1163 try:
1164 # shell=True to allow the shell to handle all forms of quotes in
1165 # $EDITOR.
1166 subprocess2.check_call(cmd, shell=True)
1167 except subprocess2.CalledProcessError:
1168 return None
1169 return FileRead(filename)
1170 finally:
1171 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001172
1173
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001174def UpgradeToHttps(url):
1175 """Upgrades random urls to https://.
1176
1177 Do not touch unknown urls like ssh:// or git://.
1178 Do not touch http:// urls with a port number,
1179 Fixes invalid GAE url.
1180 """
1181 if not url:
1182 return url
1183 if not re.match(r'[a-z\-]+\://.*', url):
1184 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1185 # relative url and will use http:///foo. Note that it defaults to http://
1186 # for compatibility with naked url like "localhost:8080".
1187 url = 'http://%s' % url
Gavin Mak65c49b12023-08-24 18:06:42 +00001188 parsed = list(urllib.parse.urlparse(url))
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001189 # Do not automatically upgrade http to https if a port number is provided.
1190 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1191 parsed[0] = 'https'
Gavin Mak65c49b12023-08-24 18:06:42 +00001192 return urllib.parse.urlunparse(parsed)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001193
1194
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001195def ParseCodereviewSettingsContent(content):
1196 """Process a codereview.settings file properly."""
1197 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1198 try:
1199 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1200 except ValueError:
1201 raise Error(
1202 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001203 def fix_url(key):
1204 if keyvals.get(key):
1205 keyvals[key] = UpgradeToHttps(keyvals[key])
1206 fix_url('CODE_REVIEW_SERVER')
1207 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001208 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001209
1210
1211def NumLocalCpus():
1212 """Returns the number of processors.
1213
dnj@chromium.org530523b2015-01-07 19:54:57 +00001214 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1215 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1216 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001217 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001218 # Surround the entire thing in try/except; no failure here should stop gclient
1219 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001220 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001221 # Use multiprocessing to get CPU count. This may raise
1222 # NotImplementedError.
1223 try:
1224 import multiprocessing
1225 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001226 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001227 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001228 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001229 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1230 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1231
1232 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1233 if 'NUMBER_OF_PROCESSORS' in os.environ:
1234 return int(os.environ['NUMBER_OF_PROCESSORS'])
1235 except Exception as e:
1236 logging.exception("Exception raised while probing CPU count: %s", e)
1237
1238 logging.debug('Failed to get CPU count. Defaulting to 1.')
1239 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001240
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001241
szager@chromium.orgfc616382014-03-18 20:32:04 +00001242def DefaultDeltaBaseCacheLimit():
1243 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1244
1245 The primary constraint is the address space of virtual memory. The cache
1246 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1247 parameter is set too high.
1248 """
1249 if platform.architecture()[0].startswith('64'):
1250 return '2g'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001251
1252 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001253
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001254
szager@chromium.orgff113292014-03-25 06:02:08 +00001255def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001256 """Return reasonable default values for configuring git-index-pack.
1257
1258 Experiments suggest that higher values for pack.threads don't improve
1259 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001260 cache_limit = DefaultDeltaBaseCacheLimit()
1261 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001262 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001263 result.extend(['-c', 'pack.threads=1'])
1264 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001265
1266
1267def FindExecutable(executable):
1268 """This mimics the "which" utility."""
1269 path_folders = os.environ.get('PATH').split(os.pathsep)
1270
1271 for path_folder in path_folders:
1272 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001273 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001274 target = os.path.abspath(os.path.expanduser(target))
1275 if os.path.isfile(target) and os.access(target, os.X_OK):
1276 return target
1277 if sys.platform.startswith('win'):
1278 for suffix in ('.bat', '.cmd', '.exe'):
1279 alt_target = target + suffix
1280 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1281 return alt_target
1282 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001283
1284
1285def freeze(obj):
1286 """Takes a generic object ``obj``, and returns an immutable version of it.
1287
1288 Supported types:
1289 * dict / OrderedDict -> FrozenDict
1290 * list -> tuple
1291 * set -> frozenset
1292 * any object with a working __hash__ implementation (assumes that hashable
1293 means immutable)
1294
1295 Will raise TypeError if you pass an object which is not hashable.
1296 """
Gavin Mak65c49b12023-08-24 18:06:42 +00001297 if isinstance(obj, collections.abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001298 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001299
1300 if isinstance(obj, (list, tuple)):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001301 return tuple(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001302
1303 if isinstance(obj, set):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001304 return frozenset(freeze(i) for i in obj)
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00001305
1306 hash(obj)
1307 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001308
1309
Gavin Mak65c49b12023-08-24 18:06:42 +00001310class FrozenDict(collections.abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001311 """An immutable OrderedDict.
1312
1313 Modified From: http://stackoverflow.com/a/2704866
1314 """
1315 def __init__(self, *args, **kwargs):
1316 self._d = collections.OrderedDict(*args, **kwargs)
1317
1318 # Calculate the hash immediately so that we know all the items are
1319 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001320 self._hash = functools.reduce(
1321 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001322
1323 def __eq__(self, other):
Gavin Mak65c49b12023-08-24 18:06:42 +00001324 if not isinstance(other, collections.abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001325 return NotImplemented
1326 if self is other:
1327 return True
1328 if len(self) != len(other):
1329 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001330 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001331 if k not in other or other[k] != v:
1332 return False
1333 return True
1334
1335 def __iter__(self):
1336 return iter(self._d)
1337
1338 def __len__(self):
1339 return len(self._d)
1340
1341 def __getitem__(self, key):
1342 return self._d[key]
1343
1344 def __hash__(self):
1345 return self._hash
1346
1347 def __repr__(self):
1348 return 'FrozenDict(%r)' % (self._d.items(),)