blob: 3087ed20042734f2882b66a23465aa1ff9ab9220 [file] [log] [blame]
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org06617272010-11-04 13:50:50 +00002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00004
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""Generic utils."""
6
Raul Tambreb946b232019-03-26 14:48:46 +00007from __future__ import print_function
8
maruel@chromium.orgdae209f2012-07-03 16:08:15 +00009import codecs
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020010import collections
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +000011import contextlib
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000012import datetime
Ben Pastened410c662020-08-26 17:07:03 +000013import errno
Raul Tambreb946b232019-03-26 14:48:46 +000014import functools
15import io
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000016import logging
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +020017import operator
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000018import os
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +000019import pipes
szager@chromium.orgfc616382014-03-18 20:32:04 +000020import platform
msb@chromium.orgac915bb2009-11-13 17:03:01 +000021import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000022import stat
borenet@google.com6b4a2ab2013-04-18 15:50:27 +000023import subprocess
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import sys
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000025import tempfile
maruel@chromium.org9e5317a2010-08-13 20:35:11 +000026import threading
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import time
maruel@chromium.orgca0f8392011-09-08 17:15:15 +000028import subprocess2
29
Raul Tambreb946b232019-03-26 14:48:46 +000030if sys.version_info.major == 2:
31 from cStringIO import StringIO
Raul Tambre6693d092020-02-19 20:36:45 +000032 import collections as collections_abc
Edward Lemura8145022020-01-06 18:47:54 +000033 import Queue as queue
34 import urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000035else:
Raul Tambre6693d092020-02-19 20:36:45 +000036 from collections import abc as collections_abc
Raul Tambreb946b232019-03-26 14:48:46 +000037 from io import StringIO
Edward Lemura8145022020-01-06 18:47:54 +000038 import queue
39 import urllib.parse as urlparse
Raul Tambreb946b232019-03-26 14:48:46 +000040
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000041
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000042RETRY_MAX = 3
43RETRY_INITIAL_SLEEP = 0.5
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000044START = datetime.datetime.now()
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +000045
46
borenet@google.com6a9b1682014-03-24 18:35:23 +000047_WARNINGS = []
48
49
szager@chromium.orgff113292014-03-25 06:02:08 +000050# These repos are known to cause OOM errors on 32-bit platforms, due the the
51# very large objects they contain. It is not safe to use threaded index-pack
52# when cloning/fetching them.
Ayu Ishii09858612020-06-26 18:00:52 +000053THREADED_INDEX_PACK_BLOCKLIST = [
szager@chromium.orgff113292014-03-25 06:02:08 +000054 'https://chromium.googlesource.com/chromium/reference_builds/chrome_win.git'
55]
56
Raul Tambreb946b232019-03-26 14:48:46 +000057"""To support rethrowing exceptions with tracebacks on both Py2 and 3."""
58if sys.version_info.major == 2:
59 # We have to use exec to avoid a SyntaxError in Python 3.
60 exec("def reraise(typ, value, tb=None):\n raise typ, value, tb\n")
61else:
62 def reraise(typ, value, tb=None):
63 if value is None:
64 value = typ()
65 if value.__traceback__ is not tb:
66 raise value.with_traceback(tb)
67 raise value
68
szager@chromium.orgff113292014-03-25 06:02:08 +000069
maruel@chromium.org66c83e62010-09-07 14:18:45 +000070class Error(Exception):
71 """gclient exception class."""
szager@chromium.org4a3c17e2013-05-24 23:59:29 +000072 def __init__(self, msg, *args, **kwargs):
73 index = getattr(threading.currentThread(), 'index', 0)
74 if index:
75 msg = '\n'.join('%d> %s' % (index, l) for l in msg.splitlines())
76 super(Error, self).__init__(msg, *args, **kwargs)
maruel@chromium.org66c83e62010-09-07 14:18:45 +000077
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000078
szager@chromium.orgfe0d1902014-04-08 20:50:44 +000079def Elapsed(until=None):
80 if until is None:
81 until = datetime.datetime.now()
82 return str(until - START).partition('.')[0]
83
84
borenet@google.com6a9b1682014-03-24 18:35:23 +000085def PrintWarnings():
86 """Prints any accumulated warnings."""
87 if _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000088 print('\n\nWarnings:', file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000089 for warning in _WARNINGS:
Raul Tambreb946b232019-03-26 14:48:46 +000090 print(warning, file=sys.stderr)
borenet@google.com6a9b1682014-03-24 18:35:23 +000091
92
93def AddWarning(msg):
94 """Adds the given warning message to the list of accumulated warnings."""
95 _WARNINGS.append(msg)
96
97
msb@chromium.orgac915bb2009-11-13 17:03:01 +000098def SplitUrlRevision(url):
99 """Splits url and returns a two-tuple: url, rev"""
100 if url.startswith('ssh:'):
maruel@chromium.org78b8cd12010-10-26 12:47:07 +0000101 # Make sure ssh://user-name@example.com/~/test.git@stable works
kangil.han@samsung.com71b13572013-10-16 17:28:11 +0000102 regex = r'(ssh://(?:[-.\w]+@)?[-\w:\.]+/[-~\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000103 components = re.search(regex, url).groups()
104 else:
scr@chromium.orgf1eccaf2014-04-11 15:51:33 +0000105 components = url.rsplit('@', 1)
106 if re.match(r'^\w+\@', url) and '@' not in components[0]:
107 components = [url]
108
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000109 if len(components) == 1:
110 components += [None]
111 return tuple(components)
112
113
primiano@chromium.org5439ea52014-08-06 17:18:18 +0000114def IsGitSha(revision):
115 """Returns true if the given string is a valid hex-encoded sha"""
116 return re.match('^[a-fA-F0-9]{6,40}$', revision) is not None
117
118
Paweł Hajdan, Jr5ec77132017-08-16 19:21:06 +0200119def IsFullGitSha(revision):
120 """Returns true if the given string is a valid hex-encoded full sha"""
121 return re.match('^[a-fA-F0-9]{40}$', revision) is not None
122
123
floitsch@google.comeaab7842011-04-28 09:07:58 +0000124def IsDateRevision(revision):
125 """Returns true if the given revision is of the form "{ ... }"."""
126 return bool(revision and re.match(r'^\{.+\}$', str(revision)))
127
128
129def MakeDateRevision(date):
130 """Returns a revision representing the latest revision before the given
131 date."""
132 return "{" + date + "}"
133
134
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000135def SyntaxErrorToError(filename, e):
136 """Raises a gclient_utils.Error exception with the human readable message"""
137 try:
138 # Try to construct a human readable error message
139 if filename:
140 error_message = 'There is a syntax error in %s\n' % filename
141 else:
142 error_message = 'There is a syntax error\n'
143 error_message += 'Line #%s, character %s: "%s"' % (
144 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
145 except:
146 # Something went wrong, re-raise the original exception
147 raise e
148 else:
149 raise Error(error_message)
150
151
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000152class PrintableObject(object):
153 def __str__(self):
154 output = ''
155 for i in dir(self):
156 if i.startswith('__'):
157 continue
158 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
159 return output
160
161
Edward Lesmesae3586b2020-03-23 21:21:14 +0000162def AskForData(message):
Christian Flache6855432021-12-01 08:10:05 +0000163 # Try to load the readline module, so that "elaborate line editing" features
164 # such as backspace work for `raw_input` / `input`.
165 try:
166 import readline
167 except ImportError:
168 # The readline module does not exist in all Python distributions, e.g. on
169 # Windows. Fall back to simple input handling.
170 pass
171
Edward Lesmesae3586b2020-03-23 21:21:14 +0000172 # Use this so that it can be mocked in tests on Python 2 and 3.
173 try:
174 if sys.version_info.major == 2:
175 return raw_input(message)
176 return input(message)
177 except KeyboardInterrupt:
178 # Hide the exception.
179 sys.exit(1)
180
181
Edward Lemur419c92f2019-10-25 22:17:49 +0000182def FileRead(filename, mode='rbU'):
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000183 # mode is ignored now; we always return unicode strings.
184 with open(filename, mode='rb') as f:
chrisha@chromium.org2b99d432012-07-12 18:10:28 +0000185 s = f.read()
Josip Sokcevic8b3bc252021-06-04 18:44:11 +0000186 try:
187 return s.decode('utf-8', 'replace')
188 except (UnicodeDecodeError, AttributeError):
Edward Lemur419c92f2019-10-25 22:17:49 +0000189 return s
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000190
191
Edward Lemur1773f372020-02-22 00:27:14 +0000192def FileWrite(filename, content, mode='w', encoding='utf-8'):
193 with codecs.open(filename, mode=mode, encoding=encoding) as f:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000194 f.write(content)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000195
196
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000197@contextlib.contextmanager
198def temporary_directory(**kwargs):
199 tdir = tempfile.mkdtemp(**kwargs)
200 try:
201 yield tdir
202 finally:
203 if tdir:
204 rmtree(tdir)
205
206
Edward Lemur1773f372020-02-22 00:27:14 +0000207@contextlib.contextmanager
208def temporary_file():
209 """Creates a temporary file.
210
211 On Windows, a file must be closed before it can be opened again. This function
212 allows to write something like:
213
214 with gclient_utils.temporary_file() as tmp:
215 gclient_utils.FileWrite(tmp, foo)
216 useful_stuff(tmp)
217
218 Instead of something like:
219
220 with tempfile.NamedTemporaryFile(delete=False) as tmp:
221 tmp.write(foo)
222 tmp.close()
223 try:
224 useful_stuff(tmp)
225 finally:
226 os.remove(tmp.name)
227 """
228 handle, name = tempfile.mkstemp()
229 os.close(handle)
230 try:
231 yield name
232 finally:
233 os.remove(name)
234
235
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000236def safe_rename(old, new):
237 """Renames a file reliably.
238
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000239 Sometimes os.rename does not work because a dying git process keeps a handle
240 on it for a few seconds. An exception is then thrown, which make the program
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000241 give up what it was doing and remove what was deleted.
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000242 The only solution is to catch the exception and try again until it works.
cyrille@nnamrak.orgef509e42013-09-20 13:19:08 +0000243 """
244 # roughly 10s
245 retries = 100
246 for i in range(retries):
247 try:
248 os.rename(old, new)
249 break
250 except OSError:
251 if i == (retries - 1):
252 # Give up.
253 raise
254 # retry
255 logging.debug("Renaming failed from %s to %s. Retrying ..." % (old, new))
256 time.sleep(0.1)
257
258
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000259def rm_file_or_tree(path):
Ben Pastene1906f402019-10-24 15:36:00 +0000260 if os.path.isfile(path) or os.path.islink(path):
loislo@chromium.org67b59e92014-12-25 13:48:37 +0000261 os.remove(path)
262 else:
263 rmtree(path)
264
265
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000266def rmtree(path):
267 """shutil.rmtree() on steroids.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000268
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000269 Recursively removes a directory, even if it's marked read-only.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270
271 shutil.rmtree() doesn't work on Windows if any of the files or directories
agable41e3a6c2016-10-20 11:36:56 -0700272 are read-only. We need to be able to force the files to be writable (i.e.,
273 deletable) as we traverse the tree.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000274
275 Even with all this, Windows still sometimes fails to delete a file, citing
276 a permission error (maybe something to do with antivirus scans or disk
277 indexing). The best suggestion any of the user forums had was to wait a
278 bit and try again, so we do that too. It's hand-waving, but sometimes it
279 works. :/
280
281 On POSIX systems, things are a little bit simpler. The modes of the files
282 to be deleted doesn't matter, only the modes of the directories containing
283 them are significant. As the directory tree is traversed, each directory
284 has its mode set appropriately before descending into it. This should
285 result in the entire tree being removed, with the possible exception of
286 *path itself, because nothing attempts to change the mode of its parent.
287 Doing so would be hazardous, as it's not a directory slated for removal.
288 In the ordinary case, this is not a problem: for our purposes, the user
289 will never lack write permission on *path's parent.
290 """
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000291 if not os.path.exists(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000292 return
293
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000294 if os.path.islink(path) or not os.path.isdir(path):
295 raise Error('Called rmtree(%s) in non-directory' % path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000297 if sys.platform == 'win32':
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000298 # Give up and use cmd.exe's rd command.
299 path = os.path.normcase(path)
Raul Tambrec2f74c12019-03-19 05:55:53 +0000300 for _ in range(3):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000301 exitcode = subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', path])
302 if exitcode == 0:
303 return
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000304
305 print('rd exited with code %d' % exitcode, file=sys.stderr)
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000306 time.sleep(3)
307 raise Exception('Failed to remove path %s' % path)
308
309 # On POSIX systems, we need the x-bit set on the directory to access it,
310 # the r-bit to see its contents, and the w-bit to remove files from it.
311 # The actual modes of the files within the directory is irrelevant.
312 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000313
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000314 def remove(func, subpath):
borenet@google.com6b4a2ab2013-04-18 15:50:27 +0000315 func(subpath)
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000316
317 for fn in os.listdir(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000318 # If fullpath is a symbolic link that points to a directory, isdir will
319 # be True, but we don't want to descend into that as a directory, we just
320 # want to remove the link. Check islink and treat links as ordinary files
321 # would be treated regardless of what they reference.
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000322 fullpath = os.path.join(path, fn)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000323 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000324 remove(os.remove, fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000325 else:
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000326 # Recurse.
327 rmtree(fullpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000328
maruel@chromium.orgf9040722011-03-09 14:47:51 +0000329 remove(os.rmdir, path)
330
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000331
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000332def safe_makedirs(tree):
333 """Creates the directory in a safe manner.
334
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000335 Because multiple threads can create these directories concurrently, trap the
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000336 exception and pass on.
337 """
338 count = 0
339 while not os.path.exists(tree):
340 count += 1
341 try:
342 os.makedirs(tree)
Raul Tambreb946b232019-03-26 14:48:46 +0000343 except OSError as e:
maruel@chromium.org6c48a302011-10-20 23:44:20 +0000344 # 17 POSIX, 183 Windows
345 if e.errno not in (17, 183):
346 raise
347 if count > 40:
348 # Give up.
349 raise
350
351
ilevy@chromium.orgc28d3772013-07-12 19:42:37 +0000352def CommandToStr(args):
353 """Converts an arg list into a shell escaped string."""
354 return ' '.join(pipes.quote(arg) for arg in args)
355
356
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000357class Wrapper(object):
358 """Wraps an object, acting as a transparent proxy for all properties by
359 default.
360 """
361 def __init__(self, wrapped):
362 self._wrapped = wrapped
363
364 def __getattr__(self, name):
365 return getattr(self._wrapped, name)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000366
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000367
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000368class AutoFlush(Wrapper):
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000369 """Creates a file object clone to automatically flush after N seconds."""
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000370 def __init__(self, wrapped, delay):
371 super(AutoFlush, self).__init__(wrapped)
372 if not hasattr(self, 'lock'):
373 self.lock = threading.Lock()
374 self.__last_flushed_at = time.time()
375 self.delay = delay
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000376
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000377 @property
378 def autoflush(self):
379 return self
maruel@chromium.orge0de9cb2010-09-17 15:07:14 +0000380
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000381 def write(self, out, *args, **kwargs):
382 self._wrapped.write(out, *args, **kwargs)
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000383 should_flush = False
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000384 self.lock.acquire()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000385 try:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000386 if self.delay and (time.time() - self.__last_flushed_at) > self.delay:
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000387 should_flush = True
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000388 self.__last_flushed_at = time.time()
maruel@chromium.org9c531262010-09-08 13:41:13 +0000389 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000390 self.lock.release()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000391 if should_flush:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000392 self.flush()
maruel@chromium.orgdb111f72010-09-08 13:36:53 +0000393
394
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000395class Annotated(Wrapper):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000396 """Creates a file object clone to automatically prepends every line in worker
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000397 threads with a NN> prefix.
398 """
399 def __init__(self, wrapped, include_zero=False):
400 super(Annotated, self).__init__(wrapped)
401 if not hasattr(self, 'lock'):
402 self.lock = threading.Lock()
403 self.__output_buffers = {}
404 self.__include_zero = include_zero
Edward Lemurcb1eb482019-10-09 18:03:14 +0000405 self._wrapped_write = getattr(self._wrapped, 'buffer', self._wrapped).write
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000406
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000407 @property
408 def annotated(self):
409 return self
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000410
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000411 def write(self, out):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000412 # Store as bytes to ensure Unicode characters get output correctly.
413 if not isinstance(out, bytes):
414 out = out.encode('utf-8')
415
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000416 index = getattr(threading.currentThread(), 'index', 0)
417 if not index and not self.__include_zero:
418 # Unindexed threads aren't buffered.
Edward Lemurcb1eb482019-10-09 18:03:14 +0000419 return self._wrapped_write(out)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000420
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000421 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000422 try:
423 # Use a dummy array to hold the string so the code can be lockless.
424 # Strings are immutable, requiring to keep a lock for the whole dictionary
425 # otherwise. Using an array is faster than using a dummy object.
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000426 if not index in self.__output_buffers:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000427 obj = self.__output_buffers[index] = [b'']
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000428 else:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000429 obj = self.__output_buffers[index]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000430 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000431 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000432
433 # Continue lockless.
434 obj[0] += out
Raul Tambre25eb8e42019-05-14 16:39:45 +0000435 while True:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000436 cr_loc = obj[0].find(b'\r')
437 lf_loc = obj[0].find(b'\n')
Raul Tambre25eb8e42019-05-14 16:39:45 +0000438 if cr_loc == lf_loc == -1:
439 break
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000440
441 if cr_loc == -1 or (0 <= lf_loc < cr_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000442 line, remaining = obj[0].split(b'\n', 1)
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000443 if line:
444 self._wrapped_write(b'%d>%s\n' % (index, line))
445 elif lf_loc == -1 or (0 <= cr_loc < lf_loc):
Edward Lemurcb1eb482019-10-09 18:03:14 +0000446 line, remaining = obj[0].split(b'\r', 1)
Raul Tambre25eb8e42019-05-14 16:39:45 +0000447 if line:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000448 self._wrapped_write(b'%d>%s\r' % (index, line))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000449 obj[0] = remaining
450
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000451 def flush(self):
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000452 """Flush buffered output."""
453 orphans = []
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000454 self.lock.acquire()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000455 try:
456 # Detect threads no longer existing.
457 indexes = (getattr(t, 'index', None) for t in threading.enumerate())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000458 indexes = filter(None, indexes)
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000459 for index in self.__output_buffers:
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000460 if not index in indexes:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000461 orphans.append((index, self.__output_buffers[index][0]))
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000462 for orphan in orphans:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000463 del self.__output_buffers[orphan[0]]
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000464 finally:
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000465 self.lock.release()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000466
Quinten Yearsley925cedb2020-04-13 17:49:39 +0000467 # Don't keep the lock while writing. Will append \n when it shouldn't.
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000468 for orphan in orphans:
nsylvain@google.come939bb52011-06-01 22:59:15 +0000469 if orphan[1]:
Edward Lemurcb1eb482019-10-09 18:03:14 +0000470 self._wrapped_write(b'%d>%s\n' % (orphan[0], orphan[1]))
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000471 return self._wrapped.flush()
maruel@chromium.org4ed34182010-09-17 15:57:47 +0000472
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000473
474def MakeFileAutoFlush(fileobj, delay=10):
475 autoflush = getattr(fileobj, 'autoflush', None)
476 if autoflush:
477 autoflush.delay = delay
478 return fileobj
479 return AutoFlush(fileobj, delay)
480
481
482def MakeFileAnnotated(fileobj, include_zero=False):
483 if getattr(fileobj, 'annotated', None):
484 return fileobj
Raul Tambre383f6cf2019-09-21 14:40:59 +0000485 return Annotated(fileobj, include_zero)
maruel@chromium.orgcb1e97a2010-09-09 20:09:20 +0000486
487
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000488GCLIENT_CHILDREN = []
489GCLIENT_CHILDREN_LOCK = threading.Lock()
490
491
492class GClientChildren(object):
493 @staticmethod
494 def add(popen_obj):
495 with GCLIENT_CHILDREN_LOCK:
496 GCLIENT_CHILDREN.append(popen_obj)
497
498 @staticmethod
499 def remove(popen_obj):
500 with GCLIENT_CHILDREN_LOCK:
501 GCLIENT_CHILDREN.remove(popen_obj)
502
503 @staticmethod
504 def _attemptToKillChildren():
505 global GCLIENT_CHILDREN
506 with GCLIENT_CHILDREN_LOCK:
507 zombies = [c for c in GCLIENT_CHILDREN if c.poll() is None]
508
509 for zombie in zombies:
510 try:
511 zombie.kill()
512 except OSError:
513 pass
514
515 with GCLIENT_CHILDREN_LOCK:
516 GCLIENT_CHILDREN = [k for k in GCLIENT_CHILDREN if k.poll() is not None]
517
518 @staticmethod
519 def _areZombies():
520 with GCLIENT_CHILDREN_LOCK:
521 return bool(GCLIENT_CHILDREN)
522
523 @staticmethod
524 def KillAllRemainingChildren():
525 GClientChildren._attemptToKillChildren()
526
527 if GClientChildren._areZombies():
528 time.sleep(0.5)
529 GClientChildren._attemptToKillChildren()
530
531 with GCLIENT_CHILDREN_LOCK:
532 if GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000533 print('Could not kill the following subprocesses:', file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000534 for zombie in GCLIENT_CHILDREN:
Raul Tambreb946b232019-03-26 14:48:46 +0000535 print(' ', zombie.pid, file=sys.stderr)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000536
537
Edward Lemur24146be2019-08-01 21:44:52 +0000538def CheckCallAndFilter(args, print_stdout=False, filter_fn=None,
539 show_header=False, always_show_header=False, retry=False,
540 **kwargs):
maruel@chromium.org17d01792010-09-01 18:07:10 +0000541 """Runs a command and calls back a filter function if needed.
542
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000543 Accepts all subprocess2.Popen() parameters plus:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000544 print_stdout: If True, the command's stdout is forwarded to stdout.
545 filter_fn: A function taking a single string argument called with each line
maruel@chromium.org57bf78d2011-09-08 18:57:33 +0000546 of the subprocess2's output. Each line has the trailing newline
maruel@chromium.org17d01792010-09-01 18:07:10 +0000547 character trimmed.
Edward Lemur24146be2019-08-01 21:44:52 +0000548 show_header: Whether to display a header before the command output.
549 always_show_header: Show header even when the command produced no output.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000550 retry: If the process exits non-zero, sleep for a brief interval and try
551 again, up to RETRY_MAX times.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000552
553 stderr is always redirected to stdout.
Edward Lemur24146be2019-08-01 21:44:52 +0000554
555 Returns the output of the command as a binary string.
maruel@chromium.org17d01792010-09-01 18:07:10 +0000556 """
Edward Lemur24146be2019-08-01 21:44:52 +0000557 def show_header_if_necessary(needs_header, attempt):
558 """Show the header at most once."""
559 if not needs_header[0]:
560 return
561
562 needs_header[0] = False
563 # Automatically generated header. We only prepend a newline if
564 # always_show_header is false, since it usually indicates there's an
565 # external progress display, and it's better not to clobber it in that case.
566 header = '' if always_show_header else '\n'
567 header += '________ running \'%s\' in \'%s\'' % (
568 ' '.join(args), kwargs.get('cwd', '.'))
569 if attempt:
570 header += ' attempt %s / %s' % (attempt + 1, RETRY_MAX + 1)
571 header += '\n'
572
573 if print_stdout:
Edward Lemurefce0d12019-09-07 03:36:37 +0000574 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
575 stdout_write(header.encode())
Edward Lemur24146be2019-08-01 21:44:52 +0000576 if filter_fn:
577 filter_fn(header)
578
579 def filter_line(command_output, line_start):
580 """Extract the last line from command output and filter it."""
581 if not filter_fn or line_start is None:
582 return
583 command_output.seek(line_start)
584 filter_fn(command_output.read().decode('utf-8'))
585
586 # Initialize stdout writer if needed. On Python 3, sys.stdout does not accept
587 # byte inputs and sys.stdout.buffer must be used instead.
588 if print_stdout:
589 sys.stdout.flush()
590 stdout_write = getattr(sys.stdout, 'buffer', sys.stdout).write
591 else:
592 stdout_write = lambda _: None
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000593
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000594 sleep_interval = RETRY_INITIAL_SLEEP
595 run_cwd = kwargs.get('cwd', os.getcwd())
Josip Sokcevic740825e2021-05-12 18:28:34 +0000596
597 # Store the output of the command regardless of the value of print_stdout or
598 # filter_fn.
599 command_output = io.BytesIO()
Edward Lemur24146be2019-08-01 21:44:52 +0000600 for attempt in range(RETRY_MAX + 1):
Ben Pastened410c662020-08-26 17:07:03 +0000601 # If our stdout is a terminal, then pass in a psuedo-tty pipe to our
602 # subprocess when filtering its output. This makes the subproc believe
603 # it was launched from a terminal, which will preserve ANSI color codes.
Milad Fad949c912020-09-18 00:26:08 +0000604 os_type = GetMacWinAixOrLinux()
605 if sys.stdout.isatty() and os_type != 'win' and os_type != 'aix':
Ben Pastened410c662020-08-26 17:07:03 +0000606 pipe_reader, pipe_writer = os.openpty()
607 else:
608 pipe_reader, pipe_writer = os.pipe()
609
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000610 kid = subprocess2.Popen(
Ben Pastened410c662020-08-26 17:07:03 +0000611 args, bufsize=0, stdout=pipe_writer, stderr=subprocess2.STDOUT,
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000612 **kwargs)
Ben Pastened410c662020-08-26 17:07:03 +0000613 # Close the write end of the pipe once we hand it off to the child proc.
614 os.close(pipe_writer)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000615
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000616 GClientChildren.add(kid)
chase@chromium.org8ad1cee2010-08-16 19:12:27 +0000617
Edward Lemur24146be2019-08-01 21:44:52 +0000618 # Passed as a list for "by ref" semantics.
619 needs_header = [show_header]
620 if always_show_header:
621 show_header_if_necessary(needs_header, attempt)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000622
623 # Also, we need to forward stdout to prevent weird re-ordering of output.
624 # This has to be done on a per byte basis to make sure it is not buffered:
agable41e3a6c2016-10-20 11:36:56 -0700625 # normally buffering is done for each line, but if the process requests
626 # input, no end-of-line character is output after the prompt and it would
627 # not show up.
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000628 try:
Edward Lemur24146be2019-08-01 21:44:52 +0000629 line_start = None
630 while True:
Ben Pastened410c662020-08-26 17:07:03 +0000631 try:
632 in_byte = os.read(pipe_reader, 1)
633 except (IOError, OSError) as e:
634 if e.errno == errno.EIO:
635 # An errno.EIO means EOF?
636 in_byte = None
637 else:
638 raise e
Edward Lemur24146be2019-08-01 21:44:52 +0000639 is_newline = in_byte in (b'\n', b'\r')
640 if not in_byte:
641 break
642
643 show_header_if_necessary(needs_header, attempt)
644
645 if is_newline:
646 filter_line(command_output, line_start)
647 line_start = None
648 elif line_start is None:
649 line_start = command_output.tell()
650
651 stdout_write(in_byte)
652 command_output.write(in_byte)
653
654 # Flush the rest of buffered output.
655 sys.stdout.flush()
656 if line_start is not None:
657 filter_line(command_output, line_start)
658
Ben Pastened410c662020-08-26 17:07:03 +0000659 os.close(pipe_reader)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000660 rv = kid.wait()
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000661
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000662 # Don't put this in a 'finally,' since the child may still run if we get
663 # an exception.
664 GClientChildren.remove(kid)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +0000665
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000666 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000667 print('Failed while running "%s"' % ' '.join(args), file=sys.stderr)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000668 raise
maruel@chromium.org109cb9d2011-09-14 20:03:11 +0000669
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000670 if rv == 0:
Edward Lemur24146be2019-08-01 21:44:52 +0000671 return command_output.getvalue()
672
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000673 if not retry:
674 break
Edward Lemur24146be2019-08-01 21:44:52 +0000675
Raul Tambreb946b232019-03-26 14:48:46 +0000676 print("WARNING: subprocess '%s' in %s failed; will retry after a short "
677 'nap...' % (' '.join('"%s"' % x for x in args), run_cwd))
Josip Sokcevic740825e2021-05-12 18:28:34 +0000678 command_output = io.BytesIO()
raphael.kubo.da.costa@intel.com91507f72013-10-22 12:18:25 +0000679 time.sleep(sleep_interval)
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000680 sleep_interval *= 2
Edward Lemur24146be2019-08-01 21:44:52 +0000681
szager@chromium.orgf2d7d6b2013-10-17 20:41:43 +0000682 raise subprocess2.CalledProcessError(
Josip Sokcevic740825e2021-05-12 18:28:34 +0000683 rv, args, kwargs.get('cwd', None), command_output.getvalue(), None)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000684
685
agable@chromium.org5a306a22014-02-24 22:13:59 +0000686class GitFilter(object):
687 """A filter_fn implementation for quieting down git output messages.
688
689 Allows a custom function to skip certain lines (predicate), and will throttle
690 the output of percentage completed lines to only output every X seconds.
691 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000692 PERCENT_RE = re.compile('(.*) ([0-9]{1,3})% .*')
agable@chromium.org5a306a22014-02-24 22:13:59 +0000693
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000694 def __init__(self, time_throttle=0, predicate=None, out_fh=None):
agable@chromium.org5a306a22014-02-24 22:13:59 +0000695 """
696 Args:
697 time_throttle (int): GitFilter will throttle 'noisy' output (such as the
698 XX% complete messages) to only be printed at least |time_throttle|
699 seconds apart.
700 predicate (f(line)): An optional function which is invoked for every line.
701 The line will be skipped if predicate(line) returns False.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000702 out_fh: File handle to write output to.
agable@chromium.org5a306a22014-02-24 22:13:59 +0000703 """
Edward Lemur24146be2019-08-01 21:44:52 +0000704 self.first_line = True
agable@chromium.org5a306a22014-02-24 22:13:59 +0000705 self.last_time = 0
706 self.time_throttle = time_throttle
707 self.predicate = predicate
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000708 self.out_fh = out_fh or sys.stdout
709 self.progress_prefix = None
agable@chromium.org5a306a22014-02-24 22:13:59 +0000710
711 def __call__(self, line):
712 # git uses an escape sequence to clear the line; elide it.
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000713 esc = line.find(chr(0o33))
agable@chromium.org5a306a22014-02-24 22:13:59 +0000714 if esc > -1:
715 line = line[:esc]
716 if self.predicate and not self.predicate(line):
717 return
718 now = time.time()
Christian Biesinger1b4c7e92019-08-08 19:33:16 +0000719 match = self.PERCENT_RE.match(line)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000720 if match:
721 if match.group(1) != self.progress_prefix:
722 self.progress_prefix = match.group(1)
723 elif now - self.last_time < self.time_throttle:
724 return
725 self.last_time = now
Edward Lemur24146be2019-08-01 21:44:52 +0000726 if not self.first_line:
727 self.out_fh.write('[%s] ' % Elapsed())
728 self.first_line = False
Raul Tambreb946b232019-03-26 14:48:46 +0000729 print(line, file=self.out_fh)
agable@chromium.org5a306a22014-02-24 22:13:59 +0000730
731
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000732def FindFileUpwards(filename, path=None):
rcui@google.com13595ff2011-10-13 01:25:07 +0000733 """Search upwards from the a directory (default: current) to find a file.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000734
rcui@google.com13595ff2011-10-13 01:25:07 +0000735 Returns nearest upper-level directory with the passed in file.
736 """
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000737 if not path:
738 path = os.getcwd()
739 path = os.path.realpath(path)
740 while True:
741 file_path = os.path.join(path, filename)
rcui@google.com13595ff2011-10-13 01:25:07 +0000742 if os.path.exists(file_path):
743 return path
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000744 (new_path, _) = os.path.split(path)
745 if new_path == path:
746 return None
747 path = new_path
748
749
Milad Fa52fdd1f2020-09-15 21:24:46 +0000750def GetMacWinAixOrLinux():
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000751 """Returns 'mac', 'win', or 'linux', matching the current platform."""
752 if sys.platform.startswith(('cygwin', 'win')):
753 return 'win'
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000754
755 if sys.platform.startswith('linux'):
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000756 return 'linux'
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000757
758 if sys.platform == 'darwin':
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000759 return 'mac'
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000760
761 if sys.platform.startswith('aix'):
Milad Fa52fdd1f2020-09-15 21:24:46 +0000762 return 'aix'
Aravind Vasudevan22bf6052022-01-24 21:11:19 +0000763
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000764 raise Error('Unknown platform: ' + sys.platform)
765
766
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000767def GetGClientRootAndEntries(path=None):
768 """Returns the gclient root and the dict of entries."""
769 config_file = '.gclient_entries'
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000770 root = FindFileUpwards(config_file, path)
771 if not root:
Raul Tambreb946b232019-03-26 14:48:46 +0000772 print("Can't find %s" % config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000773 return None
maruel@chromium.org93a9ee02011-10-18 18:23:58 +0000774 config_path = os.path.join(root, config_file)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000775 env = {}
Raphael Kubo da Costa107c97c2019-10-07 18:04:26 +0000776 with open(config_path) as config:
777 exec(config.read(), env)
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000778 config_dir = os.path.dirname(config_path)
779 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000780
781
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000782def lockedmethod(method):
783 """Method decorator that holds self.lock for the duration of the call."""
784 def inner(self, *args, **kwargs):
785 try:
786 try:
787 self.lock.acquire()
788 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +0000789 print('Was deadlocked', file=sys.stderr)
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000790 raise
791 return method(self, *args, **kwargs)
792 finally:
793 self.lock.release()
794 return inner
795
796
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000797class WorkItem(object):
798 """One work item."""
maruel@chromium.org4901daf2011-10-20 14:34:47 +0000799 # On cygwin, creating a lock throwing randomly when nearing ~100 locks.
800 # As a workaround, use a single lock. Yep you read it right. Single lock for
801 # all the 100 objects.
802 lock = threading.Lock()
803
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000804 def __init__(self, name):
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000805 # A unique string representing this work item.
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000806 self._name = name
Raul Tambreb946b232019-03-26 14:48:46 +0000807 self.outbuf = StringIO()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000808 self.start = self.finish = None
hinoka885e5b12016-06-08 14:40:09 -0700809 self.resources = [] # List of resources this work item requires.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000810
maruel@chromium.org77e4eca2010-09-21 13:23:07 +0000811 def run(self, work_queue):
812 """work_queue is passed as keyword argument so it should be
maruel@chromium.org3742c842010-09-09 19:27:14 +0000813 the last parameters of the function when you override it."""
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000814
maruel@chromium.org6ca8bf82011-09-19 23:04:30 +0000815 @property
816 def name(self):
817 return self._name
818
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000819
820class ExecutionQueue(object):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000821 """Runs a set of WorkItem that have interdependencies and were WorkItem are
822 added as they are processed.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000823
Paweł Hajdan, Jr7e9303b2017-05-23 14:38:27 +0200824 This class manages that all the required dependencies are run
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000825 before running each one.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000826
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000827 Methods of this class are thread safe.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000828 """
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000829 def __init__(self, jobs, progress, ignore_requirements, verbose=False):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000830 """jobs specifies the number of concurrent tasks to allow. progress is a
831 Progress instance."""
832 # Set when a thread is done or a new item is enqueued.
833 self.ready_cond = threading.Condition()
834 # Maximum number of concurrent tasks.
835 self.jobs = jobs
836 # List of WorkItem, for gclient, these are Dependency instances.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000837 self.queued = []
838 # List of strings representing each Dependency.name that was run.
839 self.ran = []
840 # List of items currently running.
841 self.running = []
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000842 # Exceptions thrown if any.
Raul Tambreb946b232019-03-26 14:48:46 +0000843 self.exceptions = queue.Queue()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000844 # Progress status
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000845 self.progress = progress
846 if self.progress:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000847 self.progress.update(0)
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000848
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000849 self.ignore_requirements = ignore_requirements
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000850 self.verbose = verbose
851 self.last_join = None
852 self.last_subproc_output = None
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000853
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000854 def enqueue(self, d):
855 """Enqueue one Dependency to be executed later once its requirements are
856 satisfied.
857 """
858 assert isinstance(d, WorkItem)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000859 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000860 try:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000861 self.queued.append(d)
862 total = len(self.queued) + len(self.ran) + len(self.running)
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000863 if self.jobs == 1:
864 total += 1
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000865 logging.debug('enqueued(%s)' % d.name)
866 if self.progress:
szager@chromium.orge98e04c2014-07-25 00:28:06 +0000867 self.progress._total = total
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000868 self.progress.update(0)
869 self.ready_cond.notifyAll()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000870 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000871 self.ready_cond.release()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000872
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000873 def out_cb(self, _):
874 self.last_subproc_output = datetime.datetime.now()
875 return True
876
877 @staticmethod
878 def format_task_output(task, comment=''):
879 if comment:
880 comment = ' (%s)' % comment
881 if task.start and task.finish:
882 elapsed = ' (Elapsed: %s)' % (
883 str(task.finish - task.start).partition('.')[0])
884 else:
885 elapsed = ''
886 return """
887%s%s%s
888----------------------------------------
889%s
890----------------------------------------""" % (
szager@chromium.org1f4e71b2014-04-09 19:45:40 +0000891 task.name, comment, elapsed, task.outbuf.getvalue().strip())
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000892
hinoka885e5b12016-06-08 14:40:09 -0700893 def _is_conflict(self, job):
894 """Checks to see if a job will conflict with another running job."""
895 for running_job in self.running:
896 for used_resource in running_job.item.resources:
897 logging.debug('Checking resource %s' % used_resource)
898 if used_resource in job.resources:
899 return True
900 return False
901
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000902 def flush(self, *args, **kwargs):
903 """Runs all enqueued items until all are executed."""
maruel@chromium.org3742c842010-09-09 19:27:14 +0000904 kwargs['work_queue'] = self
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000905 self.last_subproc_output = self.last_join = datetime.datetime.now()
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000906 self.ready_cond.acquire()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000907 try:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000908 while True:
909 # Check for task to run first, then wait.
910 while True:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000911 if not self.exceptions.empty():
912 # Systematically flush the queue when an exception logged.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000913 self.queued = []
maruel@chromium.org3742c842010-09-09 19:27:14 +0000914 self._flush_terminated_threads()
915 if (not self.queued and not self.running or
916 self.jobs == len(self.running)):
maruel@chromium.org1333cb32011-10-04 23:40:16 +0000917 logging.debug('No more worker threads or can\'t queue anything.')
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000918 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000919
920 # Check for new tasks to start.
Raul Tambreb946b232019-03-26 14:48:46 +0000921 for i in range(len(self.queued)):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000922 # Verify its requirements.
ilevy@chromium.orgf2ed3fb2012-11-09 23:39:49 +0000923 if (self.ignore_requirements or
924 not (set(self.queued[i].requirements) - set(self.ran))):
hinoka885e5b12016-06-08 14:40:09 -0700925 if not self._is_conflict(self.queued[i]):
926 # Start one work item: all its requirements are satisfied.
927 self._run_one_task(self.queued.pop(i), args, kwargs)
928 break
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000929 else:
930 # Couldn't find an item that could run. Break out the outher loop.
931 break
maruel@chromium.org3742c842010-09-09 19:27:14 +0000932
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000933 if not self.queued and not self.running:
maruel@chromium.org3742c842010-09-09 19:27:14 +0000934 # We're done.
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000935 break
936 # We need to poll here otherwise Ctrl-C isn't processed.
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000937 try:
938 self.ready_cond.wait(10)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000939 # If we haven't printed to terminal for a while, but we have received
940 # spew from a suprocess, let the user know we're still progressing.
941 now = datetime.datetime.now()
942 if (now - self.last_join > datetime.timedelta(seconds=60) and
943 self.last_subproc_output > self.last_join):
944 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000945 print('')
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000946 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000947 elapsed = Elapsed()
Raul Tambreb946b232019-03-26 14:48:46 +0000948 print('[%s] Still working on:' % elapsed)
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000949 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000950 for task in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000951 print('[%s] %s' % (elapsed, task.item.name))
hinoka@google.com4dfb8662014-04-25 22:21:24 +0000952 sys.stdout.flush()
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000953 except KeyboardInterrupt:
954 # Help debugging by printing some information:
Raul Tambreb946b232019-03-26 14:48:46 +0000955 print(
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000956 ('\nAllowed parallel jobs: %d\n# queued: %d\nRan: %s\n'
Raul Tambreb946b232019-03-26 14:48:46 +0000957 'Running: %d') % (self.jobs, len(self.queued), ', '.join(
958 self.ran), len(self.running)),
959 file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000960 for i in self.queued:
Raul Tambreb946b232019-03-26 14:48:46 +0000961 print(
962 '%s (not started): %s' % (i.name, ', '.join(i.requirements)),
963 file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000964 for i in self.running:
Raul Tambreb946b232019-03-26 14:48:46 +0000965 print(
966 self.format_task_output(i.item, 'interrupted'), file=sys.stderr)
maruel@chromium.org485dcab2011-09-14 12:48:47 +0000967 raise
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000968 # Something happened: self.enqueue() or a thread terminated. Loop again.
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000969 finally:
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000970 self.ready_cond.release()
maruel@chromium.org3742c842010-09-09 19:27:14 +0000971
maruel@chromium.org9e5317a2010-08-13 20:35:11 +0000972 assert not self.running, 'Now guaranteed to be single-threaded'
maruel@chromium.org3742c842010-09-09 19:27:14 +0000973 if not self.exceptions.empty():
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000974 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +0000975 print('')
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +0000976 # To get back the stack location correctly, the raise a, b, c form must be
977 # used, passing a tuple as the first argument doesn't work.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000978 e, task = self.exceptions.get()
Raul Tambreb946b232019-03-26 14:48:46 +0000979 print(self.format_task_output(task.item, 'ERROR'), file=sys.stderr)
980 reraise(e[0], e[1], e[2])
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000981 elif self.progress:
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000982 self.progress.end()
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000983
maruel@chromium.org3742c842010-09-09 19:27:14 +0000984 def _flush_terminated_threads(self):
985 """Flush threads that have terminated."""
986 running = self.running
987 self.running = []
988 for t in running:
Edward Lemura877ee62019-09-03 20:23:17 +0000989 if t.is_alive():
maruel@chromium.org3742c842010-09-09 19:27:14 +0000990 self.running.append(t)
991 else:
992 t.join()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000993 self.last_join = datetime.datetime.now()
maruel@chromium.org042f0e72011-10-23 00:04:35 +0000994 sys.stdout.flush()
szager@chromium.orgfe0d1902014-04-08 20:50:44 +0000995 if self.verbose:
Raul Tambreb946b232019-03-26 14:48:46 +0000996 print(self.format_task_output(t.item))
maruel@chromium.org3742c842010-09-09 19:27:14 +0000997 if self.progress:
maruel@chromium.org55a2eb82010-10-06 23:35:18 +0000998 self.progress.update(1, t.item.name)
maruel@chromium.orgf36c0ee2011-09-14 19:16:47 +0000999 if t.item.name in self.ran:
1000 raise Error(
1001 'gclient is confused, "%s" is already in "%s"' % (
1002 t.item.name, ', '.join(self.ran)))
maruel@chromium.orgacc45672010-09-09 21:21:21 +00001003 if not t.item.name in self.ran:
1004 self.ran.append(t.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001005
1006 def _run_one_task(self, task_item, args, kwargs):
1007 if self.jobs > 1:
1008 # Start the thread.
1009 index = len(self.ran) + len(self.running) + 1
maruel@chromium.org77e4eca2010-09-21 13:23:07 +00001010 new_thread = self._Worker(task_item, index, args, kwargs)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001011 self.running.append(new_thread)
1012 new_thread.start()
1013 else:
1014 # Run the 'thread' inside the main thread. Don't try to catch any
1015 # exception.
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001016 try:
1017 task_item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001018 print('[%s] Started.' % Elapsed(task_item.start), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001019 task_item.run(*args, **kwargs)
1020 task_item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001021 print(
1022 '[%s] Finished.' % Elapsed(task_item.finish), file=task_item.outbuf)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001023 self.ran.append(task_item.name)
1024 if self.verbose:
1025 if self.progress:
Raul Tambreb946b232019-03-26 14:48:46 +00001026 print('')
1027 print(self.format_task_output(task_item))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001028 if self.progress:
1029 self.progress.update(1, ', '.join(t.item.name for t in self.running))
1030 except KeyboardInterrupt:
Raul Tambreb946b232019-03-26 14:48:46 +00001031 print(
1032 self.format_task_output(task_item, 'interrupted'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001033 raise
1034 except Exception:
Raul Tambreb946b232019-03-26 14:48:46 +00001035 print(self.format_task_output(task_item, 'ERROR'), file=sys.stderr)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001036 raise
1037
maruel@chromium.org3742c842010-09-09 19:27:14 +00001038
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001039 class _Worker(threading.Thread):
1040 """One thread to execute one WorkItem."""
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001041 def __init__(self, item, index, args, kwargs):
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001042 threading.Thread.__init__(self, name=item.name or 'Worker')
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001043 logging.info('_Worker(%s) reqs:%s' % (item.name, item.requirements))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001044 self.item = item
maruel@chromium.org4ed34182010-09-17 15:57:47 +00001045 self.index = index
maruel@chromium.org3742c842010-09-09 19:27:14 +00001046 self.args = args
1047 self.kwargs = kwargs
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001048 self.daemon = True
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +00001049
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001050 def run(self):
1051 """Runs in its own thread."""
maruel@chromium.org1333cb32011-10-04 23:40:16 +00001052 logging.debug('_Worker.run(%s)' % self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001053 work_queue = self.kwargs['work_queue']
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001054 try:
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001055 self.item.start = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001056 print('[%s] Started.' % Elapsed(self.item.start), file=self.item.outbuf)
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001057 self.item.run(*self.args, **self.kwargs)
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001058 self.item.finish = datetime.datetime.now()
Raul Tambreb946b232019-03-26 14:48:46 +00001059 print(
1060 '[%s] Finished.' % Elapsed(self.item.finish), file=self.item.outbuf)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001061 except KeyboardInterrupt:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001062 logging.info('Caught KeyboardInterrupt in thread %s', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001063 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001064 work_queue.exceptions.put((sys.exc_info(), self))
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001065 raise
maruel@chromium.orgc8d064b2010-08-16 16:46:14 +00001066 except Exception:
1067 # Catch exception location.
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001068 logging.info('Caught exception in thread %s', self.item.name)
maruel@chromium.org3742c842010-09-09 19:27:14 +00001069 logging.info(str(sys.exc_info()))
szager@chromium.orgfe0d1902014-04-08 20:50:44 +00001070 work_queue.exceptions.put((sys.exc_info(), self))
maruel@chromium.org9e5317a2010-08-13 20:35:11 +00001071 finally:
xusydoc@chromium.orgc144e062013-05-03 23:23:53 +00001072 logging.info('_Worker.run(%s) done', self.item.name)
xusydoc@chromium.org2fd6c3f2013-05-03 21:57:55 +00001073 work_queue.ready_cond.acquire()
1074 try:
1075 work_queue.ready_cond.notifyAll()
1076 finally:
1077 work_queue.ready_cond.release()
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001078
1079
agable92bec4f2016-08-24 09:27:27 -07001080def GetEditor(git_editor=None):
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001081 """Returns the most plausible editor to use.
1082
1083 In order of preference:
agable41e3a6c2016-10-20 11:36:56 -07001084 - GIT_EDITOR environment variable
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001085 - core.editor git configuration variable (if supplied by git-cl)
1086 - VISUAL environment variable
1087 - EDITOR environment variable
bratell@opera.com65621c72013-12-09 15:05:32 +00001088 - vi (non-Windows) or notepad (Windows)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001089
1090 In the case of git-cl, this matches git's behaviour, except that it does not
1091 include dumb terminal detection.
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001092 """
agable92bec4f2016-08-24 09:27:27 -07001093 editor = os.environ.get('GIT_EDITOR') or git_editor
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001094 if not editor:
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001095 editor = os.environ.get('VISUAL')
1096 if not editor:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001097 editor = os.environ.get('EDITOR')
1098 if not editor:
1099 if sys.platform.startswith('win'):
1100 editor = 'notepad'
1101 else:
bratell@opera.com65621c72013-12-09 15:05:32 +00001102 editor = 'vi'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001103 return editor
1104
1105
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001106def RunEditor(content, git, git_editor=None):
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001107 """Opens up the default editor in the system to get the CL description."""
maruel@chromium.orgcbd760f2013-07-23 13:02:48 +00001108 file_handle, filename = tempfile.mkstemp(text=True, prefix='cl_description')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001109 # Make sure CRLF is handled properly by requiring none.
1110 if '\r' in content:
Raul Tambreb946b232019-03-26 14:48:46 +00001111 print(
1112 '!! Please remove \\r from your change description !!', file=sys.stderr)
sokcevic07152802021-08-18 00:06:34 +00001113 fileobj = os.fdopen(file_handle, 'wb')
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001114 # Still remove \r if present.
gab@chromium.orga3fe2902016-04-20 18:31:37 +00001115 content = re.sub('\r?\n', '\n', content)
1116 # Some editors complain when the file doesn't end in \n.
1117 if not content.endswith('\n'):
1118 content += '\n'
sokcevic07152802021-08-18 00:06:34 +00001119 fileobj.write(content.encode('utf-8'))
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001120 fileobj.close()
1121
1122 try:
agable92bec4f2016-08-24 09:27:27 -07001123 editor = GetEditor(git_editor=git_editor)
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001124 if not editor:
1125 return None
1126 cmd = '%s %s' % (editor, filename)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 if sys.platform == 'win32' and os.environ.get('TERM') == 'msys':
1128 # Msysgit requires the usage of 'env' to be present.
1129 cmd = 'env ' + cmd
1130 try:
1131 # shell=True to allow the shell to handle all forms of quotes in
1132 # $EDITOR.
1133 subprocess2.check_call(cmd, shell=True)
1134 except subprocess2.CalledProcessError:
1135 return None
1136 return FileRead(filename)
1137 finally:
1138 os.remove(filename)
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001139
1140
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001141def UpgradeToHttps(url):
1142 """Upgrades random urls to https://.
1143
1144 Do not touch unknown urls like ssh:// or git://.
1145 Do not touch http:// urls with a port number,
1146 Fixes invalid GAE url.
1147 """
1148 if not url:
1149 return url
1150 if not re.match(r'[a-z\-]+\://.*', url):
1151 # Make sure it is a valid uri. Otherwise, urlparse() will consider it a
1152 # relative url and will use http:///foo. Note that it defaults to http://
1153 # for compatibility with naked url like "localhost:8080".
1154 url = 'http://%s' % url
1155 parsed = list(urlparse.urlparse(url))
1156 # Do not automatically upgrade http to https if a port number is provided.
1157 if parsed[0] == 'http' and not re.match(r'^.+?\:\d+$', parsed[1]):
1158 parsed[0] = 'https'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001159 return urlparse.urlunparse(parsed)
1160
1161
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001162def ParseCodereviewSettingsContent(content):
1163 """Process a codereview.settings file properly."""
1164 lines = (l for l in content.splitlines() if not l.strip().startswith("#"))
1165 try:
1166 keyvals = dict([x.strip() for x in l.split(':', 1)] for l in lines if l)
1167 except ValueError:
1168 raise Error(
1169 'Failed to process settings, please fix. Content:\n\n%s' % content)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001170 def fix_url(key):
1171 if keyvals.get(key):
1172 keyvals[key] = UpgradeToHttps(keyvals[key])
1173 fix_url('CODE_REVIEW_SERVER')
1174 fix_url('VIEW_VC')
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001175 return keyvals
ilevy@chromium.org13691502012-10-16 04:26:37 +00001176
1177
1178def NumLocalCpus():
1179 """Returns the number of processors.
1180
dnj@chromium.org530523b2015-01-07 19:54:57 +00001181 multiprocessing.cpu_count() is permitted to raise NotImplementedError, and
1182 is known to do this on some Windows systems and OSX 10.6. If we can't get the
1183 CPU count, we will fall back to '1'.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001184 """
dnj@chromium.org530523b2015-01-07 19:54:57 +00001185 # Surround the entire thing in try/except; no failure here should stop gclient
1186 # from working.
ilevy@chromium.org13691502012-10-16 04:26:37 +00001187 try:
dnj@chromium.org530523b2015-01-07 19:54:57 +00001188 # Use multiprocessing to get CPU count. This may raise
1189 # NotImplementedError.
1190 try:
1191 import multiprocessing
1192 return multiprocessing.cpu_count()
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001193 except NotImplementedError: # pylint: disable=bare-except
dnj@chromium.org530523b2015-01-07 19:54:57 +00001194 # (UNIX) Query 'os.sysconf'.
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08001195 # pylint: disable=no-member
dnj@chromium.org530523b2015-01-07 19:54:57 +00001196 if hasattr(os, 'sysconf') and 'SC_NPROCESSORS_ONLN' in os.sysconf_names:
1197 return int(os.sysconf('SC_NPROCESSORS_ONLN'))
1198
1199 # (Windows) Query 'NUMBER_OF_PROCESSORS' environment variable.
1200 if 'NUMBER_OF_PROCESSORS' in os.environ:
1201 return int(os.environ['NUMBER_OF_PROCESSORS'])
1202 except Exception as e:
1203 logging.exception("Exception raised while probing CPU count: %s", e)
1204
1205 logging.debug('Failed to get CPU count. Defaulting to 1.')
1206 return 1
szager@chromium.orgfc616382014-03-18 20:32:04 +00001207
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001208
szager@chromium.orgfc616382014-03-18 20:32:04 +00001209def DefaultDeltaBaseCacheLimit():
1210 """Return a reasonable default for the git config core.deltaBaseCacheLimit.
1211
1212 The primary constraint is the address space of virtual memory. The cache
1213 size limit is per-thread, and 32-bit systems can hit OOM errors if this
1214 parameter is set too high.
1215 """
1216 if platform.architecture()[0].startswith('64'):
1217 return '2g'
Aravind Vasudevan22bf6052022-01-24 21:11:19 +00001218
1219 return '512m'
szager@chromium.orgfc616382014-03-18 20:32:04 +00001220
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001221
szager@chromium.orgff113292014-03-25 06:02:08 +00001222def DefaultIndexPackConfig(url=''):
szager@chromium.orgfc616382014-03-18 20:32:04 +00001223 """Return reasonable default values for configuring git-index-pack.
1224
1225 Experiments suggest that higher values for pack.threads don't improve
1226 performance."""
szager@chromium.orgff113292014-03-25 06:02:08 +00001227 cache_limit = DefaultDeltaBaseCacheLimit()
1228 result = ['-c', 'core.deltaBaseCacheLimit=%s' % cache_limit]
Ayu Ishii09858612020-06-26 18:00:52 +00001229 if url in THREADED_INDEX_PACK_BLOCKLIST:
szager@chromium.orgff113292014-03-25 06:02:08 +00001230 result.extend(['-c', 'pack.threads=1'])
1231 return result
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001232
1233
1234def FindExecutable(executable):
1235 """This mimics the "which" utility."""
1236 path_folders = os.environ.get('PATH').split(os.pathsep)
1237
1238 for path_folder in path_folders:
1239 target = os.path.join(path_folder, executable)
Quinten Yearsley925cedb2020-04-13 17:49:39 +00001240 # Just in case we have some ~/blah paths.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00001241 target = os.path.abspath(os.path.expanduser(target))
1242 if os.path.isfile(target) and os.access(target, os.X_OK):
1243 return target
1244 if sys.platform.startswith('win'):
1245 for suffix in ('.bat', '.cmd', '.exe'):
1246 alt_target = target + suffix
1247 if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
1248 return alt_target
1249 return None
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001250
1251
1252def freeze(obj):
1253 """Takes a generic object ``obj``, and returns an immutable version of it.
1254
1255 Supported types:
1256 * dict / OrderedDict -> FrozenDict
1257 * list -> tuple
1258 * set -> frozenset
1259 * any object with a working __hash__ implementation (assumes that hashable
1260 means immutable)
1261
1262 Will raise TypeError if you pass an object which is not hashable.
1263 """
Raul Tambre6693d092020-02-19 20:36:45 +00001264 if isinstance(obj, collections_abc.Mapping):
Raul Tambreb946b232019-03-26 14:48:46 +00001265 return FrozenDict((freeze(k), freeze(v)) for k, v in obj.items())
Aravind Vasudevan22bf6052022-01-24 21:11:19 +00001266
1267 if isinstance(obj, (list, tuple)):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001268 return tuple(freeze(i) for i in obj)
Aravind Vasudevan22bf6052022-01-24 21:11:19 +00001269
1270 if isinstance(obj, set):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001271 return frozenset(freeze(i) for i in obj)
Aravind Vasudevan22bf6052022-01-24 21:11:19 +00001272
1273 hash(obj)
1274 return obj
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001275
1276
Raul Tambre6693d092020-02-19 20:36:45 +00001277class FrozenDict(collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001278 """An immutable OrderedDict.
1279
1280 Modified From: http://stackoverflow.com/a/2704866
1281 """
1282 def __init__(self, *args, **kwargs):
1283 self._d = collections.OrderedDict(*args, **kwargs)
1284
1285 # Calculate the hash immediately so that we know all the items are
1286 # hashable too.
Raul Tambreb946b232019-03-26 14:48:46 +00001287 self._hash = functools.reduce(
1288 operator.xor, (hash(i) for i in enumerate(self._d.items())), 0)
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001289
1290 def __eq__(self, other):
Raul Tambre6693d092020-02-19 20:36:45 +00001291 if not isinstance(other, collections_abc.Mapping):
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001292 return NotImplemented
1293 if self is other:
1294 return True
1295 if len(self) != len(other):
1296 return False
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001297 for k, v in self.items():
Paweł Hajdan, Jr7e502612017-06-12 16:58:38 +02001298 if k not in other or other[k] != v:
1299 return False
1300 return True
1301
1302 def __iter__(self):
1303 return iter(self._d)
1304
1305 def __len__(self):
1306 return len(self._d)
1307
1308 def __getitem__(self, key):
1309 return self._d[key]
1310
1311 def __hash__(self):
1312 return self._hash
1313
1314 def __repr__(self):
1315 return 'FrozenDict(%r)' % (self._d.items(),)