blob: d16e77b9919d5303a0449f0177927c4e110cea5b [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Ben Pastened410c662020-08-26 17:07:03 +000013import errno
Raul Tambreb946b232019-03-26 14:48:46 +000014import functools
15import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000016import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020017import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000019import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000020import platform
msb@chromium.orgac915bb2009-11-13 17:03:01 +000021import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000022import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000023import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000025import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000028import subprocess2
29
Raul Tambreb946b232019-03-26 14:48:46 +000030if sys.version_info.major == 2:
31 from cStringIO import StringIO
Raul Tambre6693d092020-02-19 20:36:45 +000032 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000033 import Queue as queue
34 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000035else:
Raul Tambre6693d092020-02-19 20:36:45 +000036 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000037 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000038 import queue
39 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000040
Josip Sokcevic688adfe2023-03-01 22:46:12 +000041from lib import utils
42
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000043
Josip Sokcevic38d669f2022-09-02 18:08:57 +000044# Git wrapper retries on a transient error, and some callees do retries too,
45# such as GitWrapper.update (doing clone). One retry attempt should be
46# sufficient to help with any transient errors at this level.
47RETRY_MAX = 1
48RETRY_INITIAL_SLEEP = 2 # in seconds
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000049START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000050
51
borenet@google.com6a9b1682014-03-24 18:35:23 +000052_WARNINGS = []
53
54
szager@chromium.orgff113292014-03-25 06:02:08 +000055# These repos are known to cause OOM errors on 32-bit platforms, due the the
56# very large objects they contain. It is not safe to use threaded index-pack
57# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000058THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000059 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
60]
61
Raul Tambreb946b232019-03-26 14:48:46 +000062"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
63if sys.version_info.major == 2:
64 # We have to use exec to avoid a SyntaxError in Python 3.
65 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
66else:
67 def reraise(typ, value, tb=None):
68 if value is None:
69 value = typ()
70 if value.__traceback__ is not tb:
71 raise value.with_traceback(tb)
72 raise value
73
szager@chromium.orgff113292014-03-25 06:02:08 +000074
maruel@chromium.org66c83e62010-09-07 14:18:45 +000075class Error(Exception):
76 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000077 def __init__(self, msg, *args, **kwargs):
78 index = getattr(threading.currentThread(), 'index', 0)
79 if index:
80 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
81 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000082
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000083
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000084def Elapsed(until=None):
85 if until is None:
86 until = datetime.datetime.now()
87 return str(until - START).partition('.')[0]
88
89
borenet@google.com6a9b1682014-03-24 18:35:23 +000090def PrintWarnings():
91 """Prints any accumulated warnings."""
92 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000093 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000094 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000095 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000096
97
98def AddWarning(msg):
99 """Adds the given warning message to the list of accumulated warnings."""
100 _WARNINGS.append(msg)
101
102
Joanna Wang66286612022-06-30 19:59:13 +0000103def FuzzyMatchRepo(repo, candidates):
104 # type: (str, Union[Collection[str], Mapping[str, Any]]) -> Optional[str]
105 """Attempts to find a representation of repo in the candidates.
106
107 Args:
108 repo: a string representation of a repo in the form of a url or the
109 name and path of the solution it represents.
110 candidates: The candidates to look through which may contain `repo` in
111 in any of the forms mentioned above.
112 Returns:
113 The matching string, if any, which may be in a different form from `repo`.
114 """
115 if repo in candidates:
116 return repo
117 if repo.endswith('.git') and repo[:-len('.git')] in candidates:
118 return repo[:-len('.git')]
119 if repo + '.git' in candidates:
120 return repo + '.git'
121 return None
122
123
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000124def SplitUrlRevision(url):
125 """Splits url and returns a two-tuple: url, rev"""
126 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000127 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000128 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000129 components = re.search(regex, url).groups()
130 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000131 components = url.rsplit('@', 1)
132 if re.match(r'^\w+\@', url) and '@' not in components[0]:
133 components = [url]
134
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000135 if len(components) == 1:
136 components += [None]
137 return tuple(components)
138
139
Joanna Wang1a977bd2022-06-02 21:51:17 +0000140def ExtractRefName(remote, full_refs_str):
141 """Returns the ref name if full_refs_str is a valid ref."""
142 result = re.compile(r'^refs(\/.+)?\/((%s)|(heads)|(tags))\/(?P<ref_name>.+)' %
143 remote).match(full_refs_str)
144 if result:
145 return result.group('ref_name')
146 return None
147
148
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000149def IsGitSha(revision):
150 """Returns true if the given string is a valid hex-encoded sha"""
151 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
152
153
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200154def IsFullGitSha(revision):
155 """Returns true if the given string is a valid hex-encoded full sha"""
156 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
157
158
floitsch@google.comeaab7842011-04-28 09:07:58 +0000159def IsDateRevision(revision):
160 """Returns true if the given revision is of the form "{ ... }"."""
161 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
162
163
164def MakeDateRevision(date):
165 """Returns a revision representing the latest revision before the given
166 date."""
167 return "{" + date + "}"
168
169
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000170def SyntaxErrorToError(filename, e):
171 """Raises a gclient_utils.Error exception with the human readable message"""
172 try:
173 # Try to construct a human readable error message
174 if filename:
175 error_message = 'There is a syntax error in %s\n' % filename
176 else:
177 error_message = 'There is a syntax error\n'
178 error_message += 'Line #%s, character %s: "%s"' % (
179 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
180 except:
181 # Something went wrong, re-raise the original exception
182 raise e
183 else:
184 raise Error(error_message)
185
186
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000187class PrintableObject(object):
188 def __str__(self):
189 output = ''
190 for i in dir(self):
191 if i.startswith('__'):
192 continue
193 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
194 return output
195
196
Edward Lesmesae3586b2020-03-23 21:21:14 +0000197def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000198 # Try to load the readline module, so that "elaborate line editing" features
199 # such as backspace work for `raw_input` / `input`.
200 try:
201 import readline
202 except ImportError:
203 # The readline module does not exist in all Python distributions, e.g. on
204 # Windows. Fall back to simple input handling.
205 pass
206
Edward Lesmesae3586b2020-03-23 21:21:14 +0000207 # Use this so that it can be mocked in tests on Python 2 and 3.
208 try:
209 if sys.version_info.major == 2:
210 return raw_input(message)
211 return input(message)
212 except KeyboardInterrupt:
213 # Hide the exception.
214 sys.exit(1)
215
216
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000217# TODO(sokcevic): remove the usage of this
Edward Lemur419c92f2019-10-25 22:17:49 +0000218def FileRead(filename, mode='rbU'):
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000219 return utils.FileRead(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000220
221
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000222# TODO(sokcevic): remove the usage of this
Edward Lemur1773f372020-02-22 00:27:14 +0000223def FileWrite(filename, content, mode='w', encoding='utf-8'):
Josip Sokcevic688adfe2023-03-01 22:46:12 +0000224 return utils.FileWrite(filename, content, mode, encoding)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000225
226
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000227@contextlib.contextmanager
228def temporary_directory(**kwargs):
229 tdir = tempfile.mkdtemp(**kwargs)
230 try:
231 yield tdir
232 finally:
233 if tdir:
234 rmtree(tdir)
235
236
Edward Lemur1773f372020-02-22 00:27:14 +0000237@contextlib.contextmanager
238def temporary_file():
239 """Creates a temporary file.
240
241 On Windows, a file must be closed before it can be opened again. This function
242 allows to write something like:
243
244 with gclient_utils.temporary_file() as tmp:
245 gclient_utils.FileWrite(tmp, foo)
246 useful_stuff(tmp)
247
248 Instead of something like:
249
250 with tempfile.NamedTemporaryFile(delete=False) as tmp:
251 tmp.write(foo)
252 tmp.close()
253 try:
254 useful_stuff(tmp)
255 finally:
256 os.remove(tmp.name)
257 """
258 handle, name = tempfile.mkstemp()
259 os.close(handle)
260 try:
261 yield name
262 finally:
263 os.remove(name)
264
265
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000266def safe_rename(old, new):
267 """Renames a file reliably.
268
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000269 Sometimes os.rename does not work because a dying git process keeps a handle
270 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000271 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000272 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000273 """
274 # roughly 10s
275 retries = 100
276 for i in range(retries):
277 try:
278 os.rename(old, new)
279 break
280 except OSError:
281 if i == (retries - 1):
282 # Give up.
283 raise
284 # retry
285 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
286 time.sleep(0.1)
287
288
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000289def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000290 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000291 os.remove(path)
292 else:
293 rmtree(path)
294
295
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000296def rmtree(path):
297 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000298
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000299 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000300
301 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700302 are read-only. We need to be able to force the files to be writable (i.e.,
303 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000304
305 Even with all this, Windows still sometimes fails to delete a file, citing
306 a permission error (maybe something to do with antivirus scans or disk
307 indexing). The best suggestion any of the user forums had was to wait a
308 bit and try again, so we do that too. It's hand-waving, but sometimes it
309 works. :/
310
311 On POSIX systems, things are a little bit simpler. The modes of the files
312 to be deleted doesn't matter, only the modes of the directories containing
313 them are significant. As the directory tree is traversed, each directory
314 has its mode set appropriately before descending into it. This should
315 result in the entire tree being removed, with the possible exception of
316 *path itself, because nothing attempts to change the mode of its parent.
317 Doing so would be hazardous, as it's not a directory slated for removal.
318 In the ordinary case, this is not a problem: for our purposes, the user
319 will never lack write permission on *path's parent.
320 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000321 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000322 return
323
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000324 if os.path.islink(path) or not os.path.isdir(path):
325 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000326
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000327 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000328 # Give up and use cmd.exe's rd command.
329 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000330 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000331 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
332 if exitcode == 0:
333 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000334
335 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000336 time.sleep(3)
337 raise Exception('Failed to remove path %s' % path)
338
339 # On POSIX systems, we need the x-bit set on the directory to access it,
340 # the r-bit to see its contents, and the w-bit to remove files from it.
341 # The actual modes of the files within the directory is irrelevant.
342 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000344 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000345 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000346
347 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000348 # If fullpath is a symbolic link that points to a directory, isdir will
349 # be True, but we don't want to descend into that as a directory, we just
350 # want to remove the link. Check islink and treat links as ordinary files
351 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000352 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000353 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000354 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000355 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000356 # Recurse.
357 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000358
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000359 remove(os.rmdir, path)
360
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000361
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000362def safe_makedirs(tree):
363 """Creates the directory in a safe manner.
364
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000365 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000366 exception and pass on.
367 """
368 count = 0
369 while not os.path.exists(tree):
370 count += 1
371 try:
372 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000373 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000374 # 17 POSIX, 183 Windows
375 if e.errno not in (17, 183):
376 raise
377 if count > 40:
378 # Give up.
379 raise
380
381
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000382def CommandToStr(args):
383 """Converts an arg list into a shell escaped string."""
384 return ' '.join(pipes.quote(arg) for arg in args)
385
386
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000387class Wrapper(object):
388 """Wraps an object, acting as a transparent proxy for all properties by
389 default.
390 """
391 def __init__(self, wrapped):
392 self._wrapped = wrapped
393
394 def __getattr__(self, name):
395 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000396
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000397
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000398class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000399 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000400 def __init__(self, wrapped, delay):
401 super(AutoFlush, self).__init__(wrapped)
402 if not hasattr(self, 'lock'):
403 self.lock = threading.Lock()
404 self.__last_flushed_at = time.time()
405 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 @property
408 def autoflush(self):
409 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000410
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 def write(self, out, *args, **kwargs):
412 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000413 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000414 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000415 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000417 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000418 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000419 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000420 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000421 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000422 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000423
424
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000425class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000426 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000427 threads with a NN> prefix.
428 """
429 def __init__(self, wrapped, include_zero=False):
430 super(Annotated, self).__init__(wrapped)
431 if not hasattr(self, 'lock'):
432 self.lock = threading.Lock()
433 self.__output_buffers = {}
434 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000435 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000436
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000437 @property
438 def annotated(self):
439 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000440
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000441 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000442 # Store as bytes to ensure Unicode characters get output correctly.
443 if not isinstance(out, bytes):
444 out = out.encode('utf-8')
445
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000446 index = getattr(threading.currentThread(), 'index', 0)
447 if not index and not self.__include_zero:
448 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000449 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000450
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 try:
453 # Use a dummy array to hold the string so the code can be lockless.
454 # Strings are immutable, requiring to keep a lock for the whole dictionary
455 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000456 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000457 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000458 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000459 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000460 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000461 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000462
463 # Continue lockless.
464 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000465 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000466 cr_loc = obj[0].find(b'\r')
467 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000468 if cr_loc == lf_loc == -1:
469 break
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000470
471 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000472 line, remaining = obj[0].split(b'\n', 1)
Josip Sokcevic42c5bbb2022-01-24 21:42:28 +0000473 if line:
474 self._wrapped_write(b'%d>%s\n' % (index, line))
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000475 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000476 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000477 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000478 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000479 obj[0] = remaining
480
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000481 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000482 """Flush buffered output."""
483 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000484 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000485 try:
486 # Detect threads no longer existing.
487 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000488 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000489 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000490 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000491 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000492 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000493 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000494 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000495 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000496
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000497 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000498 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000499 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000500 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000501 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000502
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000503
504def MakeFileAutoFlush(fileobj, delay=10):
505 autoflush = getattr(fileobj, 'autoflush', None)
506 if autoflush:
507 autoflush.delay = delay
508 return fileobj
509 return AutoFlush(fileobj, delay)
510
511
512def MakeFileAnnotated(fileobj, include_zero=False):
513 if getattr(fileobj, 'annotated', None):
514 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000515 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000516
517
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000518GCLIENT_CHILDREN = []
519GCLIENT_CHILDREN_LOCK = threading.Lock()
520
521
522class GClientChildren(object):
523 @staticmethod
524 def add(popen_obj):
525 with GCLIENT_CHILDREN_LOCK:
526 GCLIENT_CHILDREN.append(popen_obj)
527
528 @staticmethod
529 def remove(popen_obj):
530 with GCLIENT_CHILDREN_LOCK:
531 GCLIENT_CHILDREN.remove(popen_obj)
532
533 @staticmethod
534 def _attemptToKillChildren():
535 global GCLIENT_CHILDREN
536 with GCLIENT_CHILDREN_LOCK:
537 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
538
539 for zombie in zombies:
540 try:
541 zombie.kill()
542 except OSError:
543 pass
544
545 with GCLIENT_CHILDREN_LOCK:
546 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
547
548 @staticmethod
549 def _areZombies():
550 with GCLIENT_CHILDREN_LOCK:
551 return bool(GCLIENT_CHILDREN)
552
553 @staticmethod
554 def KillAllRemainingChildren():
555 GClientChildren._attemptToKillChildren()
556
557 if GClientChildren._areZombies():
558 time.sleep(0.5)
559 GClientChildren._attemptToKillChildren()
560
561 with GCLIENT_CHILDREN_LOCK:
562 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000563 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000564 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000565 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000566
567
Edward Lemur24146be2019-08-01 21:44:52 +0000568def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
569 show_header=False, always_show_header=False, retry=False,
570 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000571 """Runs a command and calls back a filter function if needed.
572
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000573 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000574 print_stdout: If True, the command's stdout is forwarded to stdout.
575 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000576 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000577 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000578 show_header: Whether to display a header before the command output.
579 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000580 retry: If the process exits non-zero, sleep for a brief interval and try
581 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000582
583 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000584
585 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000586 """
Edward Lemur24146be2019-08-01 21:44:52 +0000587 def show_header_if_necessary(needs_header, attempt):
588 """Show the header at most once."""
589 if not needs_header[0]:
590 return
591
592 needs_header[0] = False
593 # Automatically generated header. We only prepend a newline if
594 # always_show_header is false, since it usually indicates there's an
595 # external progress display, and it's better not to clobber it in that case.
596 header = '' if always_show_header else '\n'
597 header += '________ running \'%s\' in \'%s\'' % (
598 ' '.join(args), kwargs.get('cwd', '.'))
599 if attempt:
600 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
601 header += '\n'
602
603 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000604 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
605 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000606 if filter_fn:
607 filter_fn(header)
608
609 def filter_line(command_output, line_start):
610 """Extract the last line from command output and filter it."""
611 if not filter_fn or line_start is None:
612 return
613 command_output.seek(line_start)
614 filter_fn(command_output.read().decode('utf-8'))
615
616 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
617 # byte inputs and sys.stdout.buffer must be used instead.
618 if print_stdout:
619 sys.stdout.flush()
620 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
621 else:
622 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000623
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000624 sleep_interval = RETRY_INITIAL_SLEEP
625 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000626
627 # Store the output of the command regardless of the value of print_stdout or
628 # filter_fn.
629 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000630 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000631 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
632 # subprocess when filtering its output. This makes the subproc believe
633 # it was launched from a terminal, which will preserve ANSI color codes.
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000634 os_type = GetOperatingSystem()
Milad Fad949c912020-09-18 00:26:08 +0000635 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000636 pipe_reader, pipe_writer = os.openpty()
637 else:
638 pipe_reader, pipe_writer = os.pipe()
639
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000640 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000641 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000642 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000643 # Close the write end of the pipe once we hand it off to the child proc.
644 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000645
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000646 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000647
Edward Lemur24146be2019-08-01 21:44:52 +0000648 # Passed as a list for "by ref" semantics.
649 needs_header = [show_header]
650 if always_show_header:
651 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000652
653 # Also, we need to forward stdout to prevent weird re-ordering of output.
654 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700655 # normally buffering is done for each line, but if the process requests
656 # input, no end-of-line character is output after the prompt and it would
657 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000658 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000659 line_start = None
660 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000661 try:
662 in_byte = os.read(pipe_reader, 1)
663 except (IOError, OSError) as e:
664 if e.errno == errno.EIO:
665 # An errno.EIO means EOF?
666 in_byte = None
667 else:
668 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000669 is_newline = in_byte in (b'\n', b'\r')
670 if not in_byte:
671 break
672
673 show_header_if_necessary(needs_header, attempt)
674
675 if is_newline:
676 filter_line(command_output, line_start)
677 line_start = None
678 elif line_start is None:
679 line_start = command_output.tell()
680
681 stdout_write(in_byte)
682 command_output.write(in_byte)
683
684 # Flush the rest of buffered output.
685 sys.stdout.flush()
686 if line_start is not None:
687 filter_line(command_output, line_start)
688
Ben Pastened410c662020-08-26 17:07:03 +0000689 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000690 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000691
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000692 # Don't put this in a 'finally,' since the child may still run if we get
693 # an exception.
694 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000695
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000696 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000697 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000698 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000699
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000700 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000701 return command_output.getvalue()
702
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000703 if not retry:
704 break
Edward Lemur24146be2019-08-01 21:44:52 +0000705
Raul Tambreb946b232019-03-26 14:48:46 +0000706 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
707 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000708 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000709 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000710 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000711
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000712 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000713 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000714
715
agable@chromium.org5a306a22014-02-24 22:13:59 +0000716class GitFilter(object):
717 """A filter_fn implementation for quieting down git output messages.
718
719 Allows a custom function to skip certain lines (predicate), and will throttle
720 the output of percentage completed lines to only output every X seconds.
721 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000722 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000723
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000724 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000725 """
726 Args:
727 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
728 XX% complete messages) to only be printed at least |time_throttle|
729 seconds apart.
730 predicate (f(line)): An optional function which is invoked for every line.
731 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000732 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000733 """
Edward Lemur24146be2019-08-01 21:44:52 +0000734 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000735 self.last_time = 0
736 self.time_throttle = time_throttle
737 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000738 self.out_fh = out_fh or sys.stdout
739 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000740
741 def __call__(self, line):
742 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000743 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000744 if esc > -1:
745 line = line[:esc]
746 if self.predicate and not self.predicate(line):
747 return
748 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000749 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000750 if match:
751 if match.group(1) != self.progress_prefix:
752 self.progress_prefix = match.group(1)
753 elif now - self.last_time < self.time_throttle:
754 return
755 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000756 if not self.first_line:
757 self.out_fh.write('[%s] ' % Elapsed())
758 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000759 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000760
761
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000762def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000763 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000764
rcui@google.com13595ff2011-10-13 01:25:07 +0000765 Returns nearest upper-level directory with the passed in file.
766 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767 if not path:
768 path = os.getcwd()
769 path = os.path.realpath(path)
770 while True:
771 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000772 if os.path.exists(file_path):
773 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000774 (new_path, _) = os.path.split(path)
775 if new_path == path:
776 return None
777 path = new_path
778
779
Jonas Termansenbf7eb522023-01-19 17:56:40 +0000780def GetOperatingSystem():
781 """Returns 'mac', 'win', 'linux', or the name of the current platform."""
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000782 if sys.platform.startswith(('cygwin', 'win')):
783 return 'win'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000784
785 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000786 return 'linux'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000787
788 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000789 return 'mac'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000790
791 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000792 return 'aix'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000793
Jonas Termansenbf7eb522023-01-19 17:56:40 +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):
801 """Returns the gclient root and the dict of entries."""
802 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000803 root = FindFileUpwards(config_file, path)
804 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000805 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000806 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000807 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000808 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000809 with open(config_path) as config:
810 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000811 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):
816 """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:
Raul Tambreb946b232019-03-26 14:48:46 +0000822 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000823 raise
824 return method(self, *args, **kwargs)
825 finally:
826 self.lock.release()
827 return inner
828
829
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000830class WorkItem(object):
831 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000832 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
833 # As a workaround, use a single lock. Yep you read it right. Single lock for
834 # all the 100 objects.
835 lock = threading.Lock()
836
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000837 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000838 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000839 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000840 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000841 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700842 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000843
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000844 def run(self, work_queue):
845 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000846 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000847
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000848 @property
849 def name(self):
850 return self._name
851
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000852
853class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000854 """Runs a set of WorkItem that have interdependencies and were WorkItem are
855 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000856
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200857 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000858 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000859
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000860 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000861 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000862 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000863 """jobs specifies the number of concurrent tasks to allow. progress is a
864 Progress instance."""
865 # Set when a thread is done or a new item is enqueued.
866 self.ready_cond = threading.Condition()
867 # Maximum number of concurrent tasks.
868 self.jobs = jobs
869 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 self.queued = []
871 # List of strings representing each Dependency.name that was run.
872 self.ran = []
873 # List of items currently running.
874 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000875 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000876 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000877 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000878 self.progress = progress
879 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000880 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000881
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000882 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000883 self.verbose = verbose
884 self.last_join = None
885 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000886
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000887 def enqueue(self, d):
888 """Enqueue one Dependency to be executed later once its requirements are
889 satisfied.
890 """
891 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000892 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000893 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000894 self.queued.append(d)
895 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000896 if self.jobs == 1:
897 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000898 logging.debug('enqueued(%s)' % d.name)
899 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000900 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000901 self.progress.update(0)
902 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000903 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000904 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000905
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000906 def out_cb(self, _):
907 self.last_subproc_output = datetime.datetime.now()
908 return True
909
910 @staticmethod
911 def format_task_output(task, comment=''):
912 if comment:
913 comment = ' (%s)' % comment
914 if task.start and task.finish:
915 elapsed = ' (Elapsed: %s)' % (
916 str(task.finish - task.start).partition('.')[0])
917 else:
918 elapsed = ''
919 return """
920%s%s%s
921----------------------------------------
922%s
923----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000924 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000925
hinoka885e5b12016-06-08 14:40:09 -0700926 def _is_conflict(self, job):
927 """Checks to see if a job will conflict with another running job."""
928 for running_job in self.running:
929 for used_resource in running_job.item.resources:
930 logging.debug('Checking resource %s' % used_resource)
931 if used_resource in job.resources:
932 return True
933 return False
934
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000935 def flush(self, *args, **kwargs):
936 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000937 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000938 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000939 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000940 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000941 while True:
942 # Check for task to run first, then wait.
943 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000944 if not self.exceptions.empty():
945 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000946 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000947 self._flush_terminated_threads()
948 if (not self.queued and not self.running or
949 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000950 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000951 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000952
953 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000954 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000955 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000956 if (self.ignore_requirements or
957 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700958 if not self._is_conflict(self.queued[i]):
959 # Start one work item: all its requirements are satisfied.
960 self._run_one_task(self.queued.pop(i), args, kwargs)
961 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000962 else:
963 # Couldn't find an item that could run. Break out the outher loop.
964 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000965
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000966 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000967 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000968 break
969 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000970 try:
971 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000972 # If we haven't printed to terminal for a while, but we have received
973 # spew from a suprocess, let the user know we're still progressing.
974 now = datetime.datetime.now()
975 if (now - self.last_join > datetime.timedelta(seconds=60) and
976 self.last_subproc_output > self.last_join):
977 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000978 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000979 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000980 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000981 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000982 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000983 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000984 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000985 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000986 except KeyboardInterrupt:
987 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000988 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000989 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000990 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
991 self.ran), len(self.running)),
992 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000993 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000994 print(
995 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
996 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000997 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000998 print(
999 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +00001000 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001001 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001002 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001003 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +00001004
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001005 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +00001006 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001007 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001008 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001009 # To get back the stack location correctly, the raise a, b, c form must be
1010 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001011 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +00001012 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
1013 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001014 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001015 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001016
maruel@chromium.org3742c842010-09-09 19:27:14 +00001017 def _flush_terminated_threads(self):
1018 """Flush threads that have terminated."""
1019 running = self.running
1020 self.running = []
1021 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +00001022 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +00001023 self.running.append(t)
1024 else:
1025 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001026 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +00001027 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001028 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +00001029 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +00001030 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +00001031 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +00001032 if t.item.name in self.ran:
1033 raise Error(
1034 'gclient is confused, "%s" is already in "%s"' % (
1035 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001036 if not t.item.name in self.ran:
1037 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001038
1039 def _run_one_task(self, task_item, args, kwargs):
1040 if self.jobs > 1:
1041 # Start the thread.
1042 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001043 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001044 self.running.append(new_thread)
1045 new_thread.start()
1046 else:
1047 # Run the 'thread' inside the main thread. Don't try to catch any
1048 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001049 try:
1050 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001051 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001052 task_item.run(*args, **kwargs)
1053 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001054 print(
1055 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001056 self.ran.append(task_item.name)
1057 if self.verbose:
1058 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001059 print('')
1060 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001061 if self.progress:
1062 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1063 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001064 print(
1065 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001066 raise
1067 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001068 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001069 raise
1070
maruel@chromium.org3742c842010-09-09 19:27:14 +00001071
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001072 class _Worker(threading.Thread):
1073 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001074 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001075 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001076 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001077 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001078 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001079 self.args = args
1080 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001081 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001082
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001083 def run(self):
1084 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001085 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001086 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001087 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001088 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001089 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001090 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001091 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001092 print(
1093 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001094 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001095 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001096 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001097 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001098 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001099 except Exception:
1100 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001101 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001102 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001103 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001104 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001105 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001106 work_queue.ready_cond.acquire()
1107 try:
1108 work_queue.ready_cond.notifyAll()
1109 finally:
1110 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001111
1112
agable92bec4f2016-08-24 09:27:27 -07001113def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001114 """Returns the most plausible editor to use.
1115
1116 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001117 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001118 - core.editor git configuration variable (if supplied by git-cl)
1119 - VISUAL environment variable
1120 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001121 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001122
1123 In the case of git-cl, this matches git's behaviour, except that it does not
1124 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001125 """
agable92bec4f2016-08-24 09:27:27 -07001126 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001128 editor = os.environ.get('VISUAL')
1129 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001130 editor = os.environ.get('EDITOR')
1131 if not editor:
1132 if sys.platform.startswith('win'):
1133 editor = 'notepad'
1134 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001135 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001136 return editor
1137
1138
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001139def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001140 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001141 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001142 # Make sure CRLF is handled properly by requiring none.
1143 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001144 print(
1145 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001146 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001147 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001148 content = re.sub('\r?\n', '\n', content)
1149 # Some editors complain when the file doesn't end in \n.
1150 if not content.endswith('\n'):
1151 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001152 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001153 fileobj.close()
1154
1155 try:
agable92bec4f2016-08-24 09:27:27 -07001156 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001157 if not editor:
1158 return None
1159 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
1188 parsed = list(urlparse.urlparse(url))
1189 # 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'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001192 return urlparse.urlunparse(parsed)
1193
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 """
Raul Tambre6693d092020-02-19 20:36:45 +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
Raul Tambre6693d092020-02-19 20:36:45 +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):
Raul Tambre6693d092020-02-19 20:36:45 +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(),)